Repository: stargate/data-api
Pull request #2089: [Tables]  filter logical operator support
Author: login: Yuqi-Du
Created: 2025-05-14T20:34:00Z | Merged: 2025-05-16T17:07:14Z

This PR is to add filter logical operator support for tables feature.

Note, since this requires java driver ability to build AND/OR relation in queryBuilder, but driver did not support it yet since the cassandra community does not have AND/OR support yet, only in DSE based C*. So classes added in cqldriver/override are necessary and should be removed when driver merged its PR.See <a href="https://issues.apache.org/jira/browse/CASSJAVA-47">Ticket CASSJAVA-47</a>.

**Checklist**
- [ ] Changes manually tested
- [ ] Automated Tests added/updated
- [ ] Documentation added/updated
- [ ] CLA Signed: [DataStax CLA](https://cla.datastax.com/)



Diff:
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/CommandQueryExecutor.java b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/CommandQueryExecutor.java
index b60ad2cfb6..808f5b3284 100644
--- a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/CommandQueryExecutor.java
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/executor/CommandQueryExecutor.java
@@ -74,7 +74,6 @@ public CommandQueryExecutor(
 
   public Uni<AsyncResultSet> executeRead(SimpleStatement statement) {
     Objects.requireNonNull(statement, "statement must not be null");
-
     statement = withExecutionProfile(statement, QueryType.READ);
     return executeAndWrap(statement);
   }
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/DefaultSubConditionRelation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/DefaultSubConditionRelation.java
new file mode 100644
index 0000000000..1770f5ae99
--- /dev/null
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/DefaultSubConditionRelation.java
@@ -0,0 +1,170 @@
+package io.stargate.sgv2.jsonapi.service.cqldriver.override;
+
+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.CheckReturnValue;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is to add AND/OR ability to driver where clauses, and it is a temporary override
+ * solution, should be removed once the driver supports it natively. Note, although AND/OR has been
+ * implemented in DataStax Astra, it is not yet in Apache Cassandra. So the community Java driver
+ * does not support this as of now. See <a
+ * href="https://issues.apache.org/jira/browse/CASSJAVA-47">Ticket CASSJAVA-47</a>.
+ */
+public class DefaultSubConditionRelation
+    implements OngoingWhereClause<DefaultSubConditionRelation>, BuildableQuery, Relation {
+
+  private final List<Relation> 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<Relation> additionalRelations) {
+    for (Relation relation : additionalRelations) {
+      relations.add(relation);
+    }
+    return this;
+  }
+
+  @NonNull
+  public DefaultSubConditionRelation withRelations(@NonNull List<Relation> 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<Relation> 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 extends Relation> T lookAheadNextRelation(
+      List<Relation> relations, int position, Class<T> 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<String, Object> namedValues) {
+    SimpleStatementBuilder builder = builder();
+    for (Map.Entry<String, Object> 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;
+  }
+
+  /** Adds conjunction clause. Next relation is logically joined with AND. */
+  public OngoingWhereClause<DefaultSubConditionRelation> and() {
+    return where(LogicalRelation.AND);
+  }
+
+  /** Adds alternative clause. Next relation is logically joined with OR. */
+  @NonNull
+  @CheckReturnValue
+  public OngoingWhereClause<DefaultSubConditionRelation> or() {
+    return where(LogicalRelation.OR);
+  }
+
+  /** Creates new sub-condition in the WHERE clause, surrounded by parenthesis. */
+  @NonNull
+  public static DefaultSubConditionRelation subCondition() {
+    return new DefaultSubConditionRelation(true);
+  }
+}
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedSelect.java b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedSelect.java
new file mode 100644
index 0000000000..b993118b58
--- /dev/null
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/ExtendedSelect.java
@@ -0,0 +1,90 @@
+package io.stargate.sgv2.jsonapi.service.cqldriver.override;
+
+import com.datastax.oss.driver.api.core.CqlIdentifier;
+import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder;
+import com.datastax.oss.driver.api.querybuilder.BindMarker;
+import com.datastax.oss.driver.api.querybuilder.select.SelectFrom;
+import com.datastax.oss.driver.internal.querybuilder.CqlHelper;
+import com.datastax.oss.driver.internal.querybuilder.select.DefaultSelect;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Map;
+
+/**
+ * This class is to add AND/OR ability to driver DefaultSelect, and it is a temporary override
+ * solution, should be removed once the driver supports it natively. Note, although AND/OR has been
+ * implemented in DataStax Astra, it is not yet in Apache Cassandra. So the community Java driver
+ * does not support this as of now. See <a
+ * href="https://issues.apache.org/jira/browse/CASSJAVA-47">Ticket CASSJAVA-47</a>.
+ */
+public class ExtendedSelect extends DefaultSelect {
+
+  public ExtendedSelect(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) {
+    super(keyspace, table);
+  }
+
+  @NonNull
+  public static SelectFrom selectFrom(
+      @Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) {
+    return new ExtendedSelect(keyspace, table);
+  }
+
+  @NonNull
+  @Override
+  public String asCql() {
+    StringBuilder builder = new StringBuilder();
+
+    builder.append("SELECT");
+    if (isJson()) {
+      builder.append(" JSON");
+    }
+    if (isDistinct()) {
+      builder.append(" DISTINCT");
+    }
+
+    CqlHelper.append(getSelectors(), builder, " ", ",", null);
+
+    builder.append(" FROM ");
+    CqlHelper.qualify(getKeyspace(), getTable(), builder);
+
+    // Note, this is the specific override to apply AND/OR to the where clause
+    DefaultSubConditionRelation.appendWhereClause(builder, getRelations(), false);
+
+    CqlHelper.append(getGroupByClauses(), builder, " GROUP BY ", ",", null);
+
+    boolean first = true;
+    for (Map.Entry<CqlIdentifier, ClusteringOrder> entry : getOrderings().entrySet()) {
+      if (first) {
+        builder.append(" ORDER BY ");
+        first = false;
+      } else {
+        builder.append(",");
+      }
+      builder.append(entry.getKey().asCql(true)).append(" ").append(entry.getValue().name());
+    }
+
+    if (getLimit() != null) {
+      builder.append(" LIMIT ");
+      if (getLimit() instanceof BindMarker) {
+        ((BindMarker) getLimit()).appendTo(builder);
+      } else {
+        builder.append(getLimit());
+      }
+    }
+
+    if (getPerPartitionLimit() != null) {
+      builder.append(" PER PARTITION LIMIT ");
+      if (getPerPartitionLimit() instanceof BindMarker) {
+        ((BindMarker) getPerPartitionLimit()).appendTo(builder);
+      } else {
+        builder.append(getPerPartitionLimit());
+      }
+    }
+
+    if (allowsFiltering()) {
+      builder.append(" ALLOW FILTERING");
+    }
+
+    return builder.toString();
+  }
+}
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/LogicalRelation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/LogicalRelation.java
new file mode 100644
index 0000000000..627a296b68
--- /dev/null
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/cqldriver/override/LogicalRelation.java
@@ -0,0 +1,36 @@
+package io.stargate.sgv2.jsonapi.service.cqldriver.override;
+
+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;
+
+/**
+ * This class is to add AND/OR relation support to java driver, and it is a temporary override
+ * solution, should be removed once the driver supports it natively. Note, although AND/OR has been
+ * implemented in DataStax Astra, it is not yet in Apache Cassandra. So the community Java driver
+ * does not support this as of now. See <a
+ * href="https://issues.apache.org/jira/browse/CASSJAVA-47">Ticket CASSJAVA-47</a>.
+ */
+@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/src/main/java/io/stargate/sgv2/jsonapi/service/operation/ReadDBTask.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/ReadDBTask.java
index b7c3fa9cbd..31079bb7f1 100644
--- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/ReadDBTask.java
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/ReadDBTask.java
@@ -15,6 +15,7 @@
 import io.stargate.sgv2.jsonapi.api.model.command.table.definition.ColumnsDescContainer;
 import io.stargate.sgv2.jsonapi.exception.WarningException;
 import io.stargate.sgv2.jsonapi.service.cqldriver.executor.*;
+import io.stargate.sgv2.jsonapi.service.cqldriver.override.ExtendedSelect;
 import io.stargate.sgv2.jsonapi.service.operation.query.*;
 import io.stargate.sgv2.jsonapi.service.operation.tasks.DBTask;
 import io.stargate.sgv2.jsonapi.service.operation.tasks.TaskRetryPolicy;
@@ -171,7 +172,10 @@ protected SimpleStatement buildReadStatement() {
 
     List<Object> positionalValues = new ArrayList<>();
 
-    var selectFrom = selectFrom(schemaObject.keyspaceName(), schemaObject.tableName());
+    // Note, use ExtendedSelect to support AND/OR in where clause, see details in
+    // ExtendedSelect.java.
+    var selectFrom =
+        ExtendedSelect.selectFrom(schemaObject.keyspaceName(), schemaObject.tableName());
     var select = applySelect(selectFrom, positionalValues);
     // these are options that go on the query builder, such as limit or allow filtering
     var bindableQuery = applyOptions(select);
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/filters/table/InTableFilter.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/filters/table/InTableFilter.java
index 93c86d526f..70b1a7e51b 100644
--- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/filters/table/InTableFilter.java
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/filters/table/InTableFilter.java
@@ -6,7 +6,6 @@
 
 import com.datastax.oss.driver.api.core.CqlIdentifier;
 import com.datastax.oss.driver.api.querybuilder.relation.ColumnRelationBuilder;
-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.term.Term;
 import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.ValueComparisonOperator;
@@ -52,10 +51,7 @@ public InTableFilter(Operator operator, String path, List<Object> arrayValue) {
   }
 
   @Override
-  public <StmtT extends OngoingWhereClause<StmtT>> StmtT apply(
-      TableSchemaObject tableSchemaObject,
-      StmtT ongoingWhereClause,
-      List<Object> positionalValues) {
+  public Relation apply(TableSchemaObject tableSchemaObject, List<Object> positionalValues) {
 
     List<Term> bindMarkers = new ArrayList<>();
 
@@ -112,8 +108,7 @@ public <StmtT extends OngoingWhereClause<StmtT>> StmtT apply(
       }
     }
 
-    return ongoingWhereClause.where(
-        applyInOperator(Relation.column(getPathAsCqlIdentifier()), bindMarkers));
+    return applyInOperator(Relation.column(getPathAsCqlIdentifier()), bindMarkers);
   }
 
   /**
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/filters/table/NativeTypeTableFilter.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/filters/table/NativeTypeTableFilter.java
index 54b75e3438..7cd5cafc0d 100644
--- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/filters/table/NativeTypeTableFilter.java
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/filters/table/NativeTypeTableFilter.java
@@ -3,7 +3,6 @@
 import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker;
 import static io.stargate.sgv2.jsonapi.exception.ErrorFormatters.*;
 
-import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause;
 import com.datastax.oss.driver.api.querybuilder.relation.Relation;
 import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.ValueComparisonOperator;
 import io.stargate.sgv2.jsonapi.exception.DocumentException;
@@ -104,10 +103,7 @@ public BuiltCondition get() {
   }
 
   @Override
-  public <StmtT extends OngoingWhereClause<StmtT>> StmtT apply(
-      TableSchemaObject tableSchemaObject,
-      StmtT ongoingWhereClause,
-      List<Object> positionalValues) {
+  public Relation apply(TableSchemaObject tableSchemaObject, List<Object> positionalValues) {
 
     try {
       var codec =
@@ -149,8 +145,7 @@ public <StmtT extends OngoingWhereClause<StmtT>> StmtT apply(
               }));
     }
 
-    return ongoingWhereClause.where(
-        Relation.column(getPathAsCqlIdentifier()).build(operator.predicate.cql, bindMarker()));
+    return Relation.column(getPathAsCqlIdentifier()).build(operator.predicate.cql, bindMarker());
   }
 
   public Recordable.DataRecorder recordTo(Recordable.DataRecorder dataRecorder) {
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/DBLogicalExpression.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/DBLogicalExpression.java
index 2da5c70800..2156106506 100644
--- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/DBLogicalExpression.java
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/DBLogicalExpression.java
@@ -65,16 +65,23 @@ public <T> void visitAllFilters(Class<T> filterClass, Consumer<T> analyzeRule) {
   }
 
   /**
-   * Add a sub dbLogicalExpression as subExpression to current caller dbLogicalExpression
-   *
-   * @param DBLogicalExpression subExpression
-   * @return subExpression
+   * Add a sub dbLogicalExpression as subExpression to current caller dbLogicalExpression. Return
+   * the passing sub dbLogicalExpression.
    */
-  public DBLogicalExpression addSubExpression(DBLogicalExpression subExpression) {
+  public DBLogicalExpression addSubExpressionReturnSub(DBLogicalExpression subExpression) {
     subExpressions.add(Objects.requireNonNull(subExpression, "subExpressions cannot be null"));
     return subExpression;
   }
 
+  /**
+   * Add a sub dbLogicalExpression as subExpression to current caller dbLogicalExpression. Return
+   * the current caller dbLogicalExpression.
+   */
+  public DBLogicalExpression addSubExpressionReturnCurrent(DBLogicalExpression subExpression) {
+    subExpressions.add(Objects.requireNonNull(subExpression, "subExpressions cannot be null"));
+    return this;
+  }
+
   /**
    * Add a dbFilter to current caller dbLogicalExpression. This new DBFilter will be added in the
    * dbFilter List, it will be in the relation context of this dbLogicalExpression.
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/TableFilter.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/TableFilter.java
index 56bffaf07c..24775a8fb4 100644
--- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/TableFilter.java
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/TableFilter.java
@@ -3,7 +3,7 @@
 import static io.stargate.sgv2.jsonapi.util.CqlIdentifierUtil.cqlIdentifierFromUserInput;
 
 import com.datastax.oss.driver.api.core.CqlIdentifier;
-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.Select;
 import io.stargate.sgv2.jsonapi.service.cqldriver.executor.IndexUsage;
 import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject;
@@ -67,19 +67,15 @@ public boolean filterIsSlice() {
    * java driver.
    *
    * @param tableSchemaObject The table the filter is being applied to.
-   * @param ongoingWhereClause The class from the Java Driver that implements the {@link
-   *     OngoingWhereClause} that is used to build the WHERE in a CQL clause. This is the type of
-   *     the statement the where is being added to such {@link Select} or {@link
-   *     com.datastax.oss.driver.api.querybuilder.update.Update}
    * @param positionalValues Mutable array of values that are used when the {@link
    *     com.datastax.oss.driver.api.querybuilder.QueryBuilder#bindMarker()} method is used, the
    *     values are added to the select statement using {@link Select#build(Object...)}
-   * @return The {@link Select} to use to continue building the query. NOTE: the query builder is a
-   *     fluent builder that returns immutable that are used in a chain, see the
+   * @return The {@link Relation} to use to continue building the query. NOTE: the query builder is
+   *     a fluent builder that returns immutable that are used in a chain, see the
    *     https://docs.datastax.com/en/developer/java-driver/4.3/manual/query_builder/index.html
    */
-  public abstract <StmtT extends OngoingWhereClause<StmtT>> StmtT apply(
-      TableSchemaObject tableSchemaObject, StmtT ongoingWhereClause, List<Object> positionalValues);
+  public abstract Relation apply(
+      TableSchemaObject tableSchemaObject, List<Object> positionalValues);
 
   /** Default "NO OP" implementation to keep the code above cleaner. */
   private static final FilterBehaviour UNSUPPORTED_BEHAVIOUR =
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
index 4e82caba6a..b33d15e4bc 100644
--- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
@@ -2,12 +2,15 @@
 
 import com.datastax.oss.driver.api.querybuilder.delete.Delete;
 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.Select;
 import com.datastax.oss.driver.api.querybuilder.update.Update;
 import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.LogicalExpression;
 import io.stargate.sgv2.jsonapi.exception.WithWarnings;
 import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject;
+import io.stargate.sgv2.jsonapi.service.cqldriver.override.DefaultSubConditionRelation;
 import io.stargate.sgv2.jsonapi.service.operation.query.*;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 
@@ -20,9 +23,9 @@
  * <p>NOTE: Using a class so the ctor can be made private to force use fo the static factories that
  * solve the generic typing needed for the {@link OngoingWhereClause}.
  *
- * @param <T> The type of Query Builder stament that the where clause is being added to, use the
- *     static factory methods like {@link #forSelect(TableSchemaObject, DBLogicalExpression)} to get
- *     the correct type.
+ * @param <T> The type of Query Builder statement that the where clause is being added to, use the
+ *     static factory methods like {@link #forSelect(TableSchemaObject, WithWarnings)} to get the
+ *     correct type.
  */
 public class TableWhereCQLClause<T extends OngoingWhereClause<T>> implements WhereCQLClause<T> {
 
@@ -39,11 +42,9 @@ private TableWhereCQLClause(
   /**
    * Build an instance to add the where clause to a {@link Select}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static WithWarnings<TableWhereCQLClause<Select>> forSelect(
       TableSchemaObject table, WithWarnings<DBLogicalExpression> dbLogicalExpression) {
@@ -54,11 +55,9 @@ public static WithWarnings<TableWhereCQLClause<Select>> forSelect(
   /**
    * Build an instance to add the where clause to a {@link Update}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
       TableSchemaObject table, WithWarnings<DBLogicalExpression> dbLogicalExpression) {
@@ -69,34 +68,88 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
+  public T apply(T tOngoingWhereClause, List<Object> objects) {
+
+    // If there is no filter in the entire logicalExpression tree
+    // Just return the ongoingWhereClause without apply anything.
+    // This could happen when user provides no filter or empty filter in the request.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
+    }
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
   }
 
-  @Override
-  public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
-    var tableFilters =
-        dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
-
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+  /**
+   * Method to recursively resolve the DBLogicalExpression into regular relation {@link Relation} or
+   * AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR, E.G.
+    // implicit and: {"name": "John"} -> WHERE (name=?)
+    // implicit and with explicit or: {"$or": [{"name": "John"}, {"age": 30}]} -> WHERE ((name=? OR
+    // age=?))
+    // Ideally, we don't need the parenthesis in the root level
+    // but this is how the driver override works current, see DefaultSubConditionRelation.java
+    // We can not remove the root level parenthesis currently to build the logical relation
+    var relationWhere = DefaultSubConditionRelation.subCondition();
+
+    List<Relation> relations = new ArrayList<>();
+    // First, add all simple filters
+    currentLogicalExpression
+        .filters()
+        .forEach(filter -> relations.add(((TableFilter) filter).apply(tableSchemaObject, objects)));
+
+    // Then, recursively build relations from sub_levels
+    currentLogicalExpression
+        .subExpressions()
+        .forEach(subExpr -> relations.add(applyLogicalRelation(subExpr, objects)));
+
+    // if current logical operator is AND, construct and() relation
+    if (currentLogicalExpression.operator() == DBLogicalExpression.DBLogicalOperator.AND
+        && !relations.isEmpty()) {
+      relationWhere = relationWhere.where(relations.getFirst());
+      for (int i = 1; i < relations.size(); i++) {
+        relationWhere = relationWhere.and().where(relations.get(i));
+      }
     }
-    return tOngoingWhereClause;
+
+    // if current logical operator is AND, construct and() relation
+    if (currentLogicalExpression.operator() == DBLogicalExpression.DBLogicalOperator.OR
+        && !relations.isEmpty()) {
+      relationWhere = relationWhere.where(relations.getFirst());
+      for (int i = 1; i < relations.size(); i++) {
+        relationWhere = relationWhere.or().where(relations.get(i));
+      }
+    }
+
+    return relationWhere;
   }
 
   @Override
@@ -123,4 +176,9 @@ public boolean selectsSinglePartition(TableSchemaObject tableSchemaObject) {
     }
     return true;
   }
+
+  @Override
+  public DBLogicalExpression getLogicalExpression() {
+    return dbLogicalExpression;
+  }
 }
diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/matcher/CaptureGroups.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/matcher/CaptureGroups.java
index 86194a89b1..39961ddf0a 100644
--- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/matcher/CaptureGroups.java
+++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/matcher/CaptureGroups.java
@@ -103,7 +103,7 @@ public void consumeAll(
       // represents the logical
       // relation in the API query, so we need to create a logical expression for each CaptureGroups
       DBLogicalExpression subDBLogicalExpression =
-          currentDbLogicalExpression.addSubExpression(
+          currentDbLogicalExpression.addSubExpressionReturnSub(
               new DBLogicalExpression(captureGroups.getLogicalOperator()));
       captureGroups.consumeAll(subDBLogicalExpression, consumer);
     }
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/LogicalFilterIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/LogicalFilterIntegrationTest.java
new file mode 100644
index 0000000000..cd6aa8f577
--- /dev/null
+++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/LogicalFilterIntegrationTest.java
@@ -0,0 +1,166 @@
+package io.stargate.sgv2.jsonapi.api.v1.tables;
+
+import static io.stargate.sgv2.jsonapi.api.v1.util.DataApiCommandSenders.assertNamespaceCommand;
+import static io.stargate.sgv2.jsonapi.api.v1.util.DataApiCommandSenders.assertTableCommand;
+
+import io.quarkus.test.common.WithTestResource;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.stargate.sgv2.jsonapi.exception.WarningException;
+import io.stargate.sgv2.jsonapi.testresource.DseTestResource;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.ClassOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestClassOrder;
+
+@QuarkusIntegrationTest
+@WithTestResource(value = DseTestResource.class, restrictToAnnotatedClass = false)
+@TestClassOrder(ClassOrderer.OrderAnnotation.class)
+public class LogicalFilterIntegrationTest extends AbstractTableIntegrationTestBase {
+
+  static final String TABLE_WITH_COLUMN_TYPES_INDEXED = "logical_table_indexed";
+
+  static final Map<String, Object> ALL_COLUMNS =
+      Map.ofEntries(
+          Map.entry("id", Map.of("type", "text")),
+          Map.entry("age", Map.of("type", "int")),
+          Map.entry("name", Map.of("type", "text")),
+          Map.entry("is_active", Map.of("type", "boolean")),
+          Map.entry("total_views", Map.of("type", "bigint")));
+
+  static final Map<String, Map<String, String>> COLUMNS_CAN_BE_SAI_INDEXED =
+      Map.ofEntries(
+          // Map.entry("id", Map.of("type", "text")),
+          Map.entry("age", Map.of("type", "int")),
+          Map.entry("name", Map.of("type", "text")),
+          Map.entry("is_active", Map.of("type", "boolean")),
+          Map.entry("total_views", Map.of("type", "bigint")));
+
+  static final String SAMPLE_ROW_JSON_1 =
+      """
+            {
+                "id": "1",
+                "age": 25,
+                "name": "John Doe",
+                "is_active": true,
+                "total_views": 1000
+            }
+            """;
+  static final String SAMPLE_ROW_JSON_2 =
+      """
+            {
+                "id": "2",
+                "age": 30,
+                "name": "Jane Smith",
+                "is_active": false,
+                "total_views": 2000
+            }
+            """;
+  static final String SAMPLE_ROW_JSON_3 =
+      """
+            {
+                "id": "3",
+                "age": 35,
+                "name": "Alice Johnson",
+                "is_active": true,
+                "total_views": 3000
+            }
+            """;
+
+  @BeforeAll
+  public final void createDefaultTables() {
+    // create table
+    assertNamespaceCommand(keyspaceName)
+        .templated()
+        .createTable(TABLE_WITH_COLUMN_TYPES_INDEXED, ALL_COLUMNS, "id")
+        .wasSuccessful();
+    // create index on indexable columns
+    for (String columnName : COLUMNS_CAN_BE_SAI_INDEXED.keySet()) {
+      assertTableCommand(keyspaceName, TABLE_WITH_COLUMN_TYPES_INDEXED)
+          .templated()
+          .createIndex(TABLE_WITH_COLUMN_TYPES_INDEXED + "_" + columnName, columnName)
+          .wasSuccessful();
+    }
+    // insert 3 rows
+    assertTableCommand(keyspaceName, TABLE_WITH_COLUMN_TYPES_INDEXED)
+        .templated()
+        .insertMany(SAMPLE_ROW_JSON_1, SAMPLE_ROW_JSON_2, SAMPLE_ROW_JSON_3)
+        .wasSuccessful()
+        .hasInsertedIdCount(3);
+  }
+
+  @Test
+  public void simpleOr() {
+    var filter =
+        """
+        {
+              "filter": {
+                  "$or": [
+                      {"age": {"$eq" : 25}},
+                      {"name": "Alice Johnson"}
+                  ]
+              }
+        }
+        """;
+    assertTableCommand(keyspaceName, TABLE_WITH_COLUMN_TYPES_INDEXED)
+        .postFind(filter)
+        .hasDocuments(2)
+        .hasDocumentUnknowingPosition(SAMPLE_ROW_JSON_1)
+        .hasDocumentUnknowingPosition(SAMPLE_ROW_JSON_3)
+        .hasNoWarnings()
+        .hasNoErrors();
+  }
+
+  @Test
+  public void oneLevelNestedAndOr() {
+    var filter =
+        """
+            {
+                  "filter": {
+                      "is_active": {"$eq" : true},
+                      "$or": [
+                          {"age": {"$lt" : 26}},
+                          {"total_views": {"$gt" : 2999}}
+                      ]
+                  }
+            }
+            """;
+    assertTableCommand(keyspaceName, TABLE_WITH_COLUMN_TYPES_INDEXED)
+        .postFind(filter)
+        .hasDocuments(2)
+        .hasDocumentUnknowingPosition(SAMPLE_ROW_JSON_1)
+        .hasDocumentUnknowingPosition(SAMPLE_ROW_JSON_3)
+        .hasNoWarnings()
+        .hasNoErrors();
+  }
+
+  @Test
+  public void twoLevelNestedAndOr() {
+    var filter =
+        """
+            {
+                  "filter": {
+                      "is_active": {"$eq" : true},
+                      "$or": [
+                          {
+                            "$and": [
+                              {"age": {"$lt" : 26} },
+                              {"name": {"$ne" : "John Doe"}}
+                            ]
+                          },
+                          {"total_views": {"$gt" : 2999}}
+                      ]
+                  }
+            }
+            """;
+    assertTableCommand(keyspaceName, TABLE_WITH_COLUMN_TYPES_INDEXED)
+        .postFind(filter)
+        .hasDocuments(1)
+        .hasDocumentUnknowingPosition(SAMPLE_ROW_JSON_3)
+        .hasWarning(
+            0,
+            WarningException.Code.NOT_EQUALS_UNSUPPORTED_BY_INDEXING,
+            "The filter uses $ne (not equals) on columns that, while indexed, are still inefficient to filter on using not equals.")
+        .hasNoErrors();
+  }
+}
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiResponseValidator.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiResponseValidator.java
index 89b89d479b..101cfc194a 100644
--- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiResponseValidator.java
+++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/util/DataApiResponseValidator.java
@@ -353,6 +353,10 @@ public DataApiResponseValidator hasDocumentInPosition(int position, String docum
     return body("data.documents[%s]".formatted(position), jsonEquals(documentJSON));
   }
 
+  public DataApiResponseValidator hasDocumentUnknowingPosition(String documentJSON) {
+    return body("data.documents", hasItem(jsonEquals(documentJSON)));
+  }
+
   public DataApiResponseValidator mayFoundSingleDocumentIdByFindOne(
       FilterException.Code expectedFilterException, String sampleId) {
     if (expectedFilterException != null) {
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/fixtures/tables/TableLogicalRelationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/tables/TableLogicalRelationTest.java
new file mode 100644
index 0000000000..a6313e787f
--- /dev/null
+++ b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/tables/TableLogicalRelationTest.java
@@ -0,0 +1,199 @@
+package io.stargate.sgv2.jsonapi.service.operation.tables;
+
+import io.stargate.sgv2.jsonapi.fixtures.testdata.TestData;
+import io.stargate.sgv2.jsonapi.fixtures.testdata.TestDataNames;
+import io.stargate.sgv2.jsonapi.service.operation.query.DBLogicalExpression;
+import io.stargate.sgv2.jsonapi.service.operation.query.TableFilter;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link TableWhereCQLClause} to build correct logicalRelation statement */
+public class TableLogicalRelationTest {
+
+  private static final TestData TEST_DATA = new TestData();
+
+  private TestDataNames names() {
+    return TEST_DATA.names;
+  }
+
+  /** The filter is empty so only an implicitAnd, so the where clause should be empty as well. */
+  @Test
+  public void emptyFilter() {
+    var fixture = TEST_DATA.tableWhereCQLClause().tableWithAllDataTypes("AND()");
+    var expBuilder = fixture.expressionBuilder;
+    var implicitAnd = expBuilder.rootImplicitAnd;
+
+    fixture
+        .expressionBuilder()
+        .replaceRootDBLogicalExpression(implicitAnd)
+        .applyAndGetOnGoingWhereClause()
+        .assertNoWhereClause()
+        .assertNoPositionalValues();
+  }
+
+  /**
+   * Implicit AND with two eq filters, so two filters should be AND together in the where clause.
+   */
+  @Test
+  public void simpleAnd() {
+    var fixture = TEST_DATA.tableWhereCQLClause().tableWithAllDataTypes("AND(eq,eq)");
+    var expBuilder = fixture.expressionBuilder;
+    var implicitAnd = expBuilder.rootImplicitAnd;
+    var dbLogicalExpression =
+        addFilters(
+            implicitAnd,
+            expBuilder.eq(names().CQL_INT_COLUMN),
+            expBuilder.eq(names().CQL_TEXT_COLUMN));
+    fixture
+        .expressionBuilder()
+        .replaceRootDBLogicalExpression(dbLogicalExpression)
+        .applyAndGetOnGoingWhereClause()
+        .assertWhereCQL(
+            "WHERE (%s=? AND %s=?)".formatted(names().CQL_INT_COLUMN, names().CQL_TEXT_COLUMN))
+        .assertWherePositionalValues(List.of(names().CQL_INT_COLUMN, names().CQL_TEXT_COLUMN));
+  }
+
+  /** Implicit AND with an explicit OR includes two eq filters. */
+  @Test
+  public void simpleOR() {
+    var fixture = TEST_DATA.tableWhereCQLClause().tableWithAllDataTypes("AND(OR(eq,eq))");
+    var expBuilder = fixture.expressionBuilder;
+    var implicitAnd = expBuilder.rootImplicitAnd;
+    var dbLogicalExpression =
+        implicitAnd.addSubExpressionReturnCurrent(
+            or(expBuilder.eq(names().CQL_INT_COLUMN), expBuilder.eq(names().CQL_TEXT_COLUMN)));
+    fixture
+        .expressionBuilder()
+        .replaceRootDBLogicalExpression(dbLogicalExpression)
+        .applyAndGetOnGoingWhereClause()
+        .assertWhereCQL(
+            "WHERE ((%s=? OR %s=?))".formatted(names().CQL_INT_COLUMN, names().CQL_TEXT_COLUMN))
+        .assertWherePositionalValues(List.of(names().CQL_INT_COLUMN, names().CQL_TEXT_COLUMN));
+  }
+
+  /** Implicit AND with a root-level filter and an explicit OR includes two other eq filters. */
+  @Test
+  public void tableFilterWithLogicalExpressionOR() {
+    var fixture = TEST_DATA.tableWhereCQLClause().tableWithAllDataTypes("AND(eq, OR(eq,eq))");
+    var expBuilder = fixture.expressionBuilder;
+    var implicitAnd = expBuilder.rootImplicitAnd;
+    var dbLogicalExpression =
+        addFilters(implicitAnd, expBuilder.eq(names().CQL_DATE_COLUMN))
+            .addSubExpressionReturnCurrent(
+                or(expBuilder.eq(names().CQL_INT_COLUMN), expBuilder.eq(names().CQL_TEXT_COLUMN)));
+    fixture
+        .expressionBuilder()
+        .replaceRootDBLogicalExpression(dbLogicalExpression)
+        .applyAndGetOnGoingWhereClause()
+        .assertWhereCQL(
+            "WHERE (%s=? AND (%s=? OR %s=?))"
+                .formatted(
+                    names().CQL_DATE_COLUMN, names().CQL_INT_COLUMN, names().CQL_TEXT_COLUMN))
+        .assertWherePositionalValues(
+            List.of(names().CQL_DATE_COLUMN, names().CQL_INT_COLUMN, names().CQL_TEXT_COLUMN));
+  }
+
+  /**
+   * Implicit AND with a root-level filter and an explicit empty OR. However, the OR is empty, so
+   * just empty parentheses are added.
+   */
+  @Test
+  public void tableFilterWithPartialEmptyOR() {
+    var fixture = TEST_DATA.tableWhereCQLClause().tableWithAllDataTypes("AND(eq, OR())");
+    var expBuilder = fixture.expressionBuilder;
+    var implicitAnd = expBuilder.rootImplicitAnd;
+    var dbLogicalExpression =
+        addFilters(implicitAnd, expBuilder.eq(names().CQL_DATE_COLUMN))
+            .addSubExpressionReturnCurrent(or());
+    fixture
+        .expressionBuilder()
+        .replaceRootDBLogicalExpression(dbLogicalExpression)
+        .applyAndGetOnGoingWhereClause()
+        .assertWhereCQL("WHERE (%s=? AND ())".formatted(names().CQL_DATE_COLUMN))
+        .assertWherePositionalValues(List.of(names().CQL_DATE_COLUMN));
+  }
+
+  /** Implicit AND with two explicit ORs, each with two eq filters. */
+  @Test
+  public void twoLogicalExpressionOR() {
+    var fixture =
+        TEST_DATA.tableWhereCQLClause().tableWithAllDataTypes("AND(OR(eq,eq), OR(eq,eq))");
+    var expBuilder = fixture.expressionBuilder;
+    var implicitAnd = expBuilder.rootImplicitAnd;
+    var dbLogicalExpression =
+        implicitAnd
+            .addSubExpressionReturnCurrent(
+                or(expBuilder.eq(names().CQL_INT_COLUMN), expBuilder.eq(names().CQL_TEXT_COLUMN)))
+            .addSubExpressionReturnCurrent(
+                or(
+                    expBuilder.eq(names().CQL_DATE_COLUMN),
+                    expBuilder.eq(names().CQL_BOOLEAN_COLUMN)));
+    fixture
+        .expressionBuilder()
+        .replaceRootDBLogicalExpression(dbLogicalExpression)
+        .applyAndGetOnGoingWhereClause()
+        .assertWhereCQL(
+            "WHERE ((%s=? OR %s=?) AND (%s=? OR %s=?))"
+                .formatted(
+                    names().CQL_INT_COLUMN,
+                    names().CQL_TEXT_COLUMN,
+                    names().CQL_DATE_COLUMN,
+                    names().CQL_BOOLEAN_COLUMN))
+        .assertWherePositionalValues(
+            List.of(
+                names().CQL_INT_COLUMN,
+                names().CQL_TEXT_COLUMN,
+                names().CQL_DATE_COLUMN,
+                names().CQL_BOOLEAN_COLUMN));
+  }
+
+  /** Implicit AND with two explicit ORs, The second OR is nested inside the first OR. */
+  @Test
+  public void nested_AND_OR() {
+    var fixture = TEST_DATA.tableWhereCQLClause().tableWithAllDataTypes("AND(OR(OR(eq,eq)))");
+    var expBuilder = fixture.expressionBuilder;
+    var implicitAnd = expBuilder.rootImplicitAnd;
+    var dbLogicalExpression =
+        implicitAnd.addSubExpressionReturnCurrent(
+            or().addSubExpressionReturnCurrent(
+                    or(
+                        expBuilder.eq(names().CQL_INT_COLUMN),
+                        expBuilder.eq(names().CQL_TEXT_COLUMN))));
+    fixture
+        .expressionBuilder()
+        .replaceRootDBLogicalExpression(dbLogicalExpression)
+        .applyAndGetOnGoingWhereClause()
+        .assertWhereCQL(
+            "WHERE (((%s=? OR %s=?)))".formatted(names().CQL_INT_COLUMN, names().CQL_TEXT_COLUMN))
+        .assertWherePositionalValues(List.of(names().CQL_INT_COLUMN, names().CQL_TEXT_COLUMN));
+  }
+
+  // ==================================================================================================================
+  // Methods below are created to help construct DBLogicalExpression in unit tests
+  public DBLogicalExpression and() {
+    return new DBLogicalExpression(DBLogicalExpression.DBLogicalOperator.AND);
+  }
+
+  public DBLogicalExpression and(TableFilter... filters) {
+    var and = and();
+    Arrays.stream(filters).forEach(and::addFilter);
+    return and;
+  }
+
+  public DBLogicalExpression or() {
+    return new DBLogicalExpression(DBLogicalExpression.DBLogicalOperator.OR);
+  }
+
+  public DBLogicalExpression or(TableFilter... filters) {
+    var or = or();
+    Arrays.stream(filters).forEach(or::addFilter);
+    return or;
+  }
+
+  public DBLogicalExpression addFilters(
+      DBLogicalExpression dbLogicalExpression, TableFilter... filters) {
+    Arrays.stream(filters).forEach(dbLogicalExpression::addFilter);
+    return dbLogicalExpression;
+  }
+}
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/LogicalExpressionTestData.java b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/LogicalExpressionTestData.java
index d757d95579..18847a38ec 100644
--- a/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/LogicalExpressionTestData.java
+++ b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/LogicalExpressionTestData.java
@@ -8,7 +8,12 @@
 import com.datastax.oss.driver.api.core.type.DataTypes;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import io.stargate.sgv2.jsonapi.exception.checked.MissingJSONCodecException;
+import io.stargate.sgv2.jsonapi.exception.checked.ToCQLCodecException;
+import io.stargate.sgv2.jsonapi.exception.checked.UnknownColumnException;
 import io.stargate.sgv2.jsonapi.service.operation.filters.table.*;
+import io.stargate.sgv2.jsonapi.service.operation.filters.table.codecs.JSONCodec;
+import io.stargate.sgv2.jsonapi.service.operation.filters.table.codecs.JSONCodecRegistries;
 import io.stargate.sgv2.jsonapi.service.operation.query.DBLogicalExpression;
 import io.stargate.sgv2.jsonapi.service.operation.query.TableFilter;
 import io.stargate.sgv2.jsonapi.util.CqlIdentifierUtil;
@@ -16,53 +21,62 @@
 import java.math.BigInteger;
 import java.util.List;
 import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class LogicalExpressionTestData extends TestDataSuplier {
 
+  private static final Logger log = LoggerFactory.getLogger(LogicalExpressionTestData.class);
+
   private static final ObjectMapper MAPPER = new ObjectMapper();
 
   public LogicalExpressionTestData(TestData testData) {
     super(testData);
   }
 
-  public DBLogicalExpression andExpression(TableMetadata tableMetadata) {
+  public DBLogicalExpression implicitAndExpression(TableMetadata tableMetadata) {
     return new DBLogicalExpression(DBLogicalExpression.DBLogicalOperator.AND);
   }
 
   public static class ExpressionBuilder<FixtureT> {
-    public final DBLogicalExpression expression;
+    public DBLogicalExpression rootImplicitAnd;
     private final TableMetadata tableMetadata;
     private final FixtureT fixture;
 
     public ExpressionBuilder(
-        FixtureT fixture, DBLogicalExpression expression, TableMetadata tableMetadata) {
+        FixtureT fixture, DBLogicalExpression rootImplicitAnd, TableMetadata tableMetadata) {
       this.fixture = fixture;
-      this.expression = expression;
+      this.rootImplicitAnd = rootImplicitAnd;
       this.tableMetadata = tableMetadata;
     }
 
+    public FixtureT replaceRootDBLogicalExpression(DBLogicalExpression dbLogicalExpression) {
+      this.rootImplicitAnd = dbLogicalExpression;
+      return fixture;
+    }
+
     public FixtureT eqOn(CqlIdentifier column) {
-      expression.addFilter(eq(tableMetadata.getColumn(column).orElseThrow()));
+      rootImplicitAnd.addFilter(eq(tableMetadata.getColumn(column).orElseThrow()));
       return fixture;
     }
 
     public FixtureT notEqOn(CqlIdentifier column) {
-      expression.addFilter(notEq(tableMetadata.getColumn(column).orElseThrow()));
+      rootImplicitAnd.addFilter(notEq(tableMetadata.getColumn(column).orElseThrow()));
       return fixture;
     }
 
     public FixtureT gtOn(CqlIdentifier column) {
-      expression.addFilter(gt(tableMetadata.getColumn(column).orElseThrow()));
+      rootImplicitAnd.addFilter(gt(tableMetadata.getColumn(column).orElseThrow()));
       return fixture;
     }
 
     public FixtureT inOn(CqlIdentifier column) {
-      expression.addFilter(in(tableMetadata.getColumn(column).orElseThrow()));
+      rootImplicitAnd.addFilter(in(tableMetadata.getColumn(column).orElseThrow()));
       return fixture;
     }
 
     public FixtureT notInOn(CqlIdentifier column) {
-      expression.addFilter(nin(tableMetadata.getColumn(column).orElseThrow()));
+      rootImplicitAnd.addFilter(nin(tableMetadata.getColumn(column).orElseThrow()));
       return fixture;
     }
 
@@ -71,23 +85,12 @@ public FixtureT eqAllPrimaryKeys() {
       return eqAllClusteringKeys();
     }
 
-    public FixtureT inOnOnePartitionKey(
-        InTableFilter.Operator inFilterOperator, ColumnMetadata firstPartitionKey) {
-      if (inFilterOperator == InTableFilter.Operator.IN) {
-        expression.addFilter(in(firstPartitionKey));
-      }
-      if (inFilterOperator == InTableFilter.Operator.NIN) {
-        expression.addFilter(nin(firstPartitionKey));
-      }
-      return fixture;
-    }
-
     public FixtureT eqAllPartitionKeys() {
       tableMetadata
           .getPartitionKey()
           .forEach(
               columnMetadata -> {
-                expression.addFilter(eq(columnMetadata));
+                rootImplicitAnd.addFilter(eq(columnMetadata));
               });
       return fixture;
     }
@@ -103,7 +106,7 @@ public FixtureT eqSkipOnePartitionKeys(int skipIndex) {
         if (index == skipIndex) {
           continue;
         }
-        expression.addFilter(eq(columnMetadata));
+        rootImplicitAnd.addFilter(eq(columnMetadata));
       }
       return fixture;
     }
@@ -114,11 +117,22 @@ public FixtureT eqAllClusteringKeys() {
           .keySet()
           .forEach(
               columnMetadata -> {
-                expression.addFilter(eq(columnMetadata));
+                rootImplicitAnd.addFilter(eq(columnMetadata));
               });
       return fixture;
     }
 
+    public FixtureT inOnOnePartitionKey(
+        InTableFilter.Operator inFilterOperator, ColumnMetadata firstPartitionKey) {
+      if (inFilterOperator == InTableFilter.Operator.IN) {
+        rootImplicitAnd.addFilter(in(firstPartitionKey));
+      }
+      if (inFilterOperator == InTableFilter.Operator.NIN) {
+        rootImplicitAnd.addFilter(nin(firstPartitionKey));
+      }
+      return fixture;
+    }
+
     public FixtureT eqSkipOneClusteringKeys(int skipIndex) {
 
       if (skipIndex >= tableMetadata.getClusteringColumns().size()) {
@@ -131,7 +145,7 @@ public FixtureT eqSkipOneClusteringKeys(int skipIndex) {
         if (index == skipIndex) {
           continue;
         }
-        expression.addFilter(eq(columnMetadata));
+        rootImplicitAnd.addFilter(eq(columnMetadata));
       }
       return fixture;
     }
@@ -140,7 +154,7 @@ public FixtureT eqOnlyOneClusteringKey(int index) {
 
       ColumnMetadata columnMetadata =
           tableMetadata.getClusteringColumns().keySet().stream().toList().get(index);
-      expression.addFilter(eq(columnMetadata));
+      rootImplicitAnd.addFilter(eq(columnMetadata));
       return fixture;
     }
 
@@ -157,7 +171,7 @@ public FixtureT eqFirstNonPKOrIndexed() {
           .filter(columnMetadata -> !allIndexTargets.contains(columnMetadata.getName()))
           .findFirst()
           .ifPresentOrElse(
-              columnMetadata -> expression.addFilter(eq(columnMetadata)),
+              columnMetadata -> rootImplicitAnd.addFilter(eq(columnMetadata)),
               () -> {
                 throw new IllegalArgumentException(
                     "Table don't have a column that is NOT on the SAI table to generate test data");
@@ -173,6 +187,10 @@ public static TableFilter eq(ColumnMetadata columnMetadata) {
           value(columnMetadata.getType()));
     }
 
+    public TableFilter eq(CqlIdentifier columnCqlIdentifier) {
+      return eq(tableMetadata.getColumn(columnCqlIdentifier).orElseThrow());
+    }
+
     public static TableFilter notEq(ColumnMetadata columnMetadata) {
       return filter(
           columnMetadata.getName(),
@@ -295,7 +313,7 @@ public static Object value(DataType type) {
         return "P1H30M"; // Handle duration as a string
       }
       if (type.equals(DataTypes.INT)) {
-        return 25;
+        return 25L;
       }
       if (type.equals(DataTypes.BIGINT)) {
         return 1000000L;
@@ -408,5 +426,26 @@ public static JsonNode jsonNodeValue(DataType dataType) {
       throw new IllegalArgumentException(
           "Did not understand type %s to convert into JsonNode".formatted(dataType));
     }
+
+    // Get CqlValue of type that Driver expects
+    public Object CqlValue(CqlIdentifier column) {
+      var cqlDataType = tableMetadata.getColumn(column).orElseThrow().getType();
+      var javaValue = value(cqlDataType);
+      JSONCodec codec = null;
+      try {
+        codec =
+            JSONCodecRegistries.DEFAULT_REGISTRY.codecToCQL(
+                tableMetadata,
+                column,
+                ExpressionBuilder.value(tableMetadata.getColumn(column).orElseThrow().getType()));
+      } catch (UnknownColumnException | ToCQLCodecException | MissingJSONCodecException e) {
+        throw new IllegalArgumentException(e);
+      }
+      try {
+        return codec.toCQL(javaValue);
+      } catch (ToCQLCodecException e) {
+        throw new RuntimeException(e);
+      }
+    }
   }
 }
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/TableWhereCQLClauseTestData.java b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/TableWhereCQLClauseTestData.java
new file mode 100644
index 0000000000..efb15d8946
--- /dev/null
+++ b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/TableWhereCQLClauseTestData.java
@@ -0,0 +1,128 @@
+package io.stargate.sgv2.jsonapi.fixtures.testdata;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import com.datastax.oss.driver.api.core.CqlIdentifier;
+import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata;
+import com.datastax.oss.driver.api.querybuilder.select.Select;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.stargate.sgv2.jsonapi.exception.WithWarnings;
+import io.stargate.sgv2.jsonapi.service.cqldriver.executor.TableSchemaObject;
+import io.stargate.sgv2.jsonapi.service.cqldriver.override.ExtendedSelect;
+import io.stargate.sgv2.jsonapi.service.operation.query.DBLogicalExpression;
+import io.stargate.sgv2.jsonapi.service.operation.tables.TableWhereCQLClause;
+import io.stargate.sgv2.jsonapi.util.recordable.Recordable;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TableWhereCQLClauseTestData extends TestDataSuplier {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(TableWhereCQLClauseTestData.class);
+
+  public TableWhereCQLClauseTestData(TestData testData) {
+    super(testData);
+  }
+
+  public TableWhereCQLClauseTestData.TableWhereCQLClauseFixture tableWithAllDataTypes(
+      String message) {
+    var tableMetaData = testData.tableMetadata().tableAllDatatypesIndexed();
+    return new TableWhereCQLClauseTestData.TableWhereCQLClauseFixture(
+        message, tableMetaData, testData.logicalExpression().implicitAndExpression(tableMetaData));
+  }
+
+  public static class TableWhereCQLClauseFixture implements Recordable {
+
+    private final String message;
+    private final TableMetadata tableMetadata;
+    private final TableSchemaObject tableSchemaObject;
+    public final LogicalExpressionTestData.ExpressionBuilder<TableWhereCQLClauseFixture>
+        expressionBuilder;
+
+    private TableWhereCQLClause<Select> tableWhereCQLClause = null;
+    private final List<Object> positionalValues = new ArrayList<>();
+    private Select onGoingWhereClause = null;
+    public Throwable exception = null;
+
+    public LogicalExpressionTestData.ExpressionBuilder<TableWhereCQLClauseFixture>
+        expressionBuilder() {
+      return expressionBuilder;
+    }
+
+    public TableWhereCQLClauseFixture(
+        String message, TableMetadata tableMetadata, DBLogicalExpression expressionBuilder) {
+      this.message = message;
+      this.tableMetadata = tableMetadata;
+      this.tableSchemaObject = TableSchemaObject.from(tableMetadata, new ObjectMapper());
+      this.expressionBuilder =
+          new LogicalExpressionTestData.ExpressionBuilder<>(this, expressionBuilder, tableMetadata);
+    }
+
+    public TableWhereCQLClauseFixture applyAndGetOnGoingWhereClause() {
+      this.tableWhereCQLClause =
+          TableWhereCQLClause.forSelect(
+                  tableSchemaObject, WithWarnings.of(expressionBuilder.rootImplicitAnd))
+              .target();
+      assertDoesNotThrow(this::callApply, "Error when: %s".formatted(message));
+      assertThat(onGoingWhereClause)
+          .as("apply result is not null when: %s".formatted(message))
+          .isNotNull();
+      return this;
+    }
+
+    public TableWhereCQLClauseFixture assertWhereCQL(String expectedWhereCQL) {
+      LOGGER.warn("Expected where clause CQL: {}\n", expectedWhereCQL);
+      LOGGER.warn("Actual whole CQL: {}\n", onGoingWhereClause.asCql());
+      assertThat(onGoingWhereClause.asCql()).contains(expectedWhereCQL);
+      return this;
+    }
+
+    public TableWhereCQLClauseFixture assertNoWhereClause() {
+      LOGGER.warn("Expected NO where clause in CQL statement. \n");
+      LOGGER.warn("Actual whole CQL: {}\n", onGoingWhereClause.asCql());
+      assertThat(onGoingWhereClause.asCql()).doesNotContain("WHERE");
+      return this;
+    }
+
+    public TableWhereCQLClauseFixture assertWherePositionalValues(
+        List<CqlIdentifier> expectedColumnIdentifiers) {
+      List<Object> expectedPositionalValues =
+          expectedColumnIdentifiers.stream().map(expressionBuilder::CqlValue).toList();
+      LOGGER.warn("Expected positional values: {}\n", expectedPositionalValues);
+      LOGGER.warn("Actual positional values:: {}\n", positionalValues);
+
+      // The order of positional values matters here.
+      // E.G.
+      // Expected values [25, "text-value"], Actual values [25, "text-value"]
+      assertThat(expectedPositionalValues).containsExactlyElementsOf(positionalValues);
+
+      return this;
+    }
+
+    public TableWhereCQLClauseFixture assertNoPositionalValues() {
+      LOGGER.warn("Expected NO positional values.\n");
+      LOGGER.warn("Actual positional values:: {}\n", positionalValues);
+      assertThat(positionalValues).isEmpty();
+      return this;
+    }
+
+    private void callApply() {
+      LOGGER.warn("Apply WhereCQLClause: {}\n {}", message, this);
+      // Select from all columns
+      var selectFrom =
+          ExtendedSelect.selectFrom(tableMetadata.getKeyspace(), tableMetadata.getName());
+      var select = selectFrom.all();
+      onGoingWhereClause = tableWhereCQLClause.apply(select, positionalValues);
+      LOGGER.warn("Apply WhereCQLClause result(OngoingWhereClause): {}", onGoingWhereClause);
+    }
+
+    @Override
+    public DataRecorder recordTo(DataRecorder dataRecorder) {
+      return dataRecorder
+          .append("expression", expressionBuilder.rootImplicitAnd)
+          .append("table", tableMetadata.describe(true));
+    }
+  }
+}
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/TestData.java b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/TestData.java
index e09677c2fa..a53b622fa4 100644
--- a/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/TestData.java
+++ b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/TestData.java
@@ -68,6 +68,10 @@ public WhereAnalyzerTestData whereAnalyzer() {
     return getOrCache(WhereAnalyzerTestData.class);
   }
 
+  public TableWhereCQLClauseTestData tableWhereCQLClause() {
+    return getOrCache(TableWhereCQLClauseTestData.class);
+  }
+
   public SelectCQLClauseTestData selectCQLClause() {
     return getOrCache(SelectCQLClauseTestData.class);
   }
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/WhereAnalyzerTestData.java b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/WhereAnalyzerTestData.java
index 69579f036e..8f9189b4ae 100644
--- a/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/WhereAnalyzerTestData.java
+++ b/src/test/java/io/stargate/sgv2/jsonapi/fixtures/testdata/WhereAnalyzerTestData.java
@@ -38,7 +38,7 @@ public WhereAnalyzerFixture table2PK3Clustering1Index(
     return new WhereAnalyzerFixture(
         message,
         tableMetaData,
-        testData.logicalExpression().andExpression(tableMetaData),
+        testData.logicalExpression().implicitAndExpression(tableMetaData),
         statementType);
   }
 
@@ -48,7 +48,7 @@ public WhereAnalyzerFixture tableKeyAndTwoDuration(
     return new WhereAnalyzerFixture(
         message,
         tableMetaData,
-        testData.logicalExpression().andExpression(tableMetaData),
+        testData.logicalExpression().implicitAndExpression(tableMetaData),
         statementType);
   }
 
@@ -58,7 +58,7 @@ public WhereAnalyzerFixture tableAllColumnDatatypesIndexed(
     return new WhereAnalyzerFixture(
         message,
         tableMetaData,
-        testData.logicalExpression().andExpression(tableMetaData),
+        testData.logicalExpression().implicitAndExpression(tableMetaData),
         statementType);
   }
 
@@ -68,7 +68,7 @@ public WhereAnalyzerFixture tableAllColumnDatatypesNotIndexed(
     return new WhereAnalyzerFixture(
         message,
         tableMetaData,
-        testData.logicalExpression().andExpression(tableMetaData),
+        testData.logicalExpression().implicitAndExpression(tableMetaData),
         statementType);
   }
 
@@ -138,7 +138,7 @@ public void callAnalyze() {
       analysisResult =
           analyzer.analyse(
               TableWhereCQLClause.forSelect(
-                      tableSchemaObject, WithWarnings.of(expression.expression))
+                      tableSchemaObject, WithWarnings.of(expression.rootImplicitAnd))
                   .target());
       LOGGER.warn("Analysis result: {}", analysisResult);
     }
@@ -312,7 +312,7 @@ public WhereAnalyzerFixture assertNoWarnings() {
     @Override
     public Recordable.DataRecorder recordTo(Recordable.DataRecorder dataRecorder) {
       return dataRecorder
-          .append("expression", expression.expression)
+          .append("expression", expression.rootImplicitAnd)
           .append("table", tableMetadata.describe(true));
     }
   }
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/FindCollectionOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/FindCollectionOperationTest.java
index c43cada998..8b8ebaaedb 100644
--- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/FindCollectionOperationTest.java
+++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/collections/FindCollectionOperationTest.java
@@ -1328,7 +1328,7 @@ public void findOrWithAllFilter() throws Exception {
       explicitOr.addFilter(
           new TextCollectionFilter("username", MapCollectionFilter.Operator.EQ, "user1"));
       explicitOr.addFilter(new AllCollectionFilter("tags", List.of("tag1", "tag2"), false));
-      implicitAnd.addSubExpression(explicitOr);
+      implicitAnd.addSubExpressionReturnCurrent(explicitOr);
 
       FindCollectionOperation operation =
           FindCollectionOperation.unsortedSingle(
@@ -1396,7 +1396,7 @@ public void findOrWithAllFilterWithNegation() throws Exception {
       explicitOr.addFilter(
           new TextCollectionFilter("username", MapCollectionFilter.Operator.EQ, "user1"));
       explicitOr.addFilter(new AllCollectionFilter("tags", List.of("tag1", "tag2"), true));
-      implicitAnd.addSubExpression(explicitOr);
+      implicitAnd.addSubExpressionReturnCurrent(explicitOr);
 
       FindCollectionOperation operation =
           FindCollectionOperation.unsortedSingle(
diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/tables/SelectWhereAnalyzerTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/tables/SelectWhereAnalyzerTest.java
index e35435bbfa..f999d5b61a 100644
--- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/tables/SelectWhereAnalyzerTest.java
+++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/tables/SelectWhereAnalyzerTest.java
@@ -342,7 +342,7 @@ public void oneUnknownColumn() {
                 "oneUnknownColumn()", WhereCQLClauseAnalyzer.StatementType.SELECT);
     fixture
         .expression()
-        .expression
+        .rootImplicitAnd
         .addFilter(
             LogicalExpressionTestData.ExpressionBuilder.filter(
                 names().COL_UNKNOWN_1, DataTypes.TEXT, NativeTypeTableFilter.Operator.EQ, "value"));
@@ -363,7 +363,7 @@ public void unknownAndFullPk() {
                 "unknownAndFullPk()", WhereCQLClauseAnalyzer.StatementType.SELECT);
     fixture
         .expression()
-        .expression
+        .rootImplicitAnd
         .addFilter(
             LogicalExpressionTestData.ExpressionBuilder.filter(
                 names().COL_UNKNOWN_1, DataTypes.TEXT, NativeTypeTableFilter.Operator.EQ, "value"));


Reviews:
user: login: amorton
body: some small improvements , most noticably in TableWhereCQLClause.applyLogicalRelation() where I think it can be better explained why things are happening. Hard to check the code is doing the correct thing. 

interem review, I will review the tests later today
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T21:09:49Z
user: login: amorton
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T21:12:30Z
user: login: amorton
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T21:13:16Z
user: login: amorton
body: from dsicussiojs, approved - i will check the tests in a few hours
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T21:27:57Z
user: login: Yuqi-Du
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:11:49Z
user: login: Yuqi-Du
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:13:22Z
user: login: Yuqi-Du
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:26:26Z
user: login: Yuqi-Du
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:28:08Z
user: login: Yuqi-Du
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:30:32Z
user: login: Yuqi-Du
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:31:33Z
user: login: vkarpov15
body: LGTM
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T18:38:25Z

Review comments:
diff_hunk: @@ -122,4 +123,36 @@ public Recordable.DataRecorder recordTo(Recordable.DataRecorder dataRecorder) {
         .append("subExpressions", subExpressions)
         .append("filters", filters);
   }
+
+  // ==================================================================================================================
+  // Methods below are created to help construct DBLogicalExpression in unit tests
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/DBLogicalExpression.java
user: login: amorton
body: If these are only needed for testing: 
1) add @VisibleForTesting
2) reduce visibility, they can be package protected (remove the public)
3) Don't do any of that, instead move them into the test classes 
created_at: 2025-05-15T18:51:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,19 +67,15 @@ public boolean filterIsSlice() {
    * java driver.
    *
    * @param tableSchemaObject The table the filter is being applied to.
-   * @param ongoingWhereClause The class from the Java Driver that implements the {@link
-   *     OngoingWhereClause} that is used to build the WHERE in a CQL clause. This is the type of
-   *     the statement the where is being added to such {@link Select} or {@link
-   *     com.datastax.oss.driver.api.querybuilder.update.Update}
    * @param positionalValues Mutable array of values that are used when the {@link
    *     com.datastax.oss.driver.api.querybuilder.QueryBuilder#bindMarker()} method is used, the
    *     values are added to the select statement using {@link Select#build(Object...)}
-   * @return The {@link Select} to use to continue building the query. NOTE: the query builder is a
-   *     fluent builder that returns immutable that are used in a chain, see the
+   * @return The {@link Relation} to use to continue building the query. NOTE: the query builder is
+   *     a fluent builder that returns immutable that are used in a chain, see the
    *     https://docs.datastax.com/en/developer/java-driver/4.3/manual/query_builder/index.html
    */
-  public abstract <StmtT extends OngoingWhereClause<StmtT>> StmtT apply(
-      TableSchemaObject tableSchemaObject, StmtT ongoingWhereClause, List<Object> positionalValues);
+  public abstract Relation apply(
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/TableFilter.java
user: login: amorton
body: I think this is a better approach that passing in the where clause, it makes it easier for the table filters to have hierarchy 
created_at: 2025-05-15T18:53:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: amorton
body: nit: took me a little to think about why this would be allowed, it would be a table scan with no filter. can you just tweak the comment to say that is the case. 
created_at: 2025-05-15T18:57:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
+   * Relation} or AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder
+   * expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR
+    var relationWhere = DefaultSubConditionRelation.subCondition();
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: amorton
body: unclear here - is this making an and or an or condition ? The docs says "Creates new sub-condition in the WHERE clause, surrounded by parenthesis" so why do we start with a sub condition ? 

do we need to know what level of recursive step this is ? 
created_at: 2025-05-15T18:59:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: amorton
body: tableFilters is now unused 
created_at: 2025-05-15T18:59:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: amorton
body: not everything is a helper method :) 
created_at: 2025-05-15T19:00:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
+   * Relation} or AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder
+   * expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR
+    var relationWhere = DefaultSubConditionRelation.subCondition();
+
+    // recursively build relations from sub_levels.
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: amorton
body: nit: can you explain why we apply the filters then the sub expressions . In general the comments here can be improved to explain why things are happening.

this may be easier to read and explain if the Stream is not used 
created_at: 2025-05-15T19:02:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
+   * Relation} or AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder
+   * expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR
+    var relationWhere = DefaultSubConditionRelation.subCondition();
+
+    // recursively build relations from sub_levels.
+    List<Relation> subLevelRelations =
+        Stream.concat(
+                currentLogicalExpression.filters().stream()
+                    .map(filter -> ((TableFilter) filter).apply(tableSchemaObject, objects)),
+                currentLogicalExpression.subExpressions().stream()
+                    .map(subExpression -> applyLogicalRelation(subExpression, objects)))
+            .toList();
+
+    // construct and() relation
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: amorton
body: pls explain more, and prefer for each rather when available (less error prone)
created_at: 2025-05-15T21:06:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
+   * Relation} or AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder
+   * expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR
+    var relationWhere = DefaultSubConditionRelation.subCondition();
+
+    // recursively build relations from sub_levels.
+    List<Relation> subLevelRelations =
+        Stream.concat(
+                currentLogicalExpression.filters().stream()
+                    .map(filter -> ((TableFilter) filter).apply(tableSchemaObject, objects)),
+                currentLogicalExpression.subExpressions().stream()
+                    .map(subExpression -> applyLogicalRelation(subExpression, objects)))
+            .toList();
+
+    // construct and() relation
+    if (currentLogicalExpression.operator() == DBLogicalExpression.DBLogicalOperator.AND
+        && !subLevelRelations.isEmpty()) {
+      relationWhere = relationWhere.where(subLevelRelations.getFirst());
+      for (int i = 1; i < subLevelRelations.size(); i++) {
+        relationWhere = relationWhere.and().where(subLevelRelations.get(i));
+      }
+    }
+
+    // construct or() relation
+    if (currentLogicalExpression.operator() == DBLogicalExpression.DBLogicalOperator.OR
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: amorton
body: logicalls, should these two if statements be one ? 
created_at: 2025-05-15T21:07:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,166 @@
+package io.stargate.sgv2.jsonapi.api.v1.tables;
+
+import static io.stargate.sgv2.jsonapi.api.v1.util.DataApiCommandSenders.assertNamespaceCommand;
+import static io.stargate.sgv2.jsonapi.api.v1.util.DataApiCommandSenders.assertTableCommand;
+
+import io.quarkus.test.common.WithTestResource;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.stargate.sgv2.jsonapi.exception.WarningException;
+import io.stargate.sgv2.jsonapi.testresource.DseTestResource;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.ClassOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestClassOrder;
+
+@QuarkusIntegrationTest
+@WithTestResource(value = DseTestResource.class, restrictToAnnotatedClass = false)
+@TestClassOrder(ClassOrderer.OrderAnnotation.class)
+public class LogicalFilterIntegrationTest extends AbstractTableIntegrationTestBase {
path: src/test/java/io/stargate/sgv2/jsonapi/api/v1/tables/LogicalFilterIntegrationTest.java
user: login: amorton
body: it would be nice if we did not have to do this as an integration test
created_at: 2025-05-15T21:12:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,144 @@
+package io.stargate.sgv2.jsonapi.service.operation.tables;
+
+import io.stargate.sgv2.jsonapi.fixtures.testdata.TestData;
+import io.stargate.sgv2.jsonapi.fixtures.testdata.TestDataNames;
+import io.stargate.sgv2.jsonapi.service.operation.query.DBLogicalExpression;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link TableWhereCQLClause} to build correct logicalRelation statement */
+public class TableLogicalRelationTest {
path: src/test/java/io/stargate/sgv2/jsonapi/fixtures/tables/TableLogicalRelationTest.java
user: login: amorton
body: tests need comments to, just some basic why things are happening 
created_at: 2025-05-15T21:13:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -122,4 +123,36 @@ public Recordable.DataRecorder recordTo(Recordable.DataRecorder dataRecorder) {
         .append("subExpressions", subExpressions)
         .append("filters", filters);
   }
+
+  // ==================================================================================================================
+  // Methods below are created to help construct DBLogicalExpression in unit tests
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/query/DBLogicalExpression.java
user: login: Yuqi-Du
body: fixed
created_at: 2025-05-16T15:11:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: Yuqi-Du
body: added
```
    // If there is no filter in the entire logicalExpression tree
    // Just return the ongoingWhereClause without apply anything.
    // This could happen when user provides no filter or empty filter in the request.
```
created_at: 2025-05-16T15:13:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
+   * Relation} or AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder
+   * expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR
+    var relationWhere = DefaultSubConditionRelation.subCondition();
+
+    // recursively build relations from sub_levels.
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: Yuqi-Du
body: remove stream processing, and add docs.
created_at: 2025-05-16T15:26:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
+   * Relation} or AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder
+   * expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR
+    var relationWhere = DefaultSubConditionRelation.subCondition();
+
+    // recursively build relations from sub_levels.
+    List<Relation> subLevelRelations =
+        Stream.concat(
+                currentLogicalExpression.filters().stream()
+                    .map(filter -> ((TableFilter) filter).apply(tableSchemaObject, objects)),
+                currentLogicalExpression.subExpressions().stream()
+                    .map(subExpression -> applyLogicalRelation(subExpression, objects)))
+            .toList();
+
+    // construct and() relation
+    if (currentLogicalExpression.operator() == DBLogicalExpression.DBLogicalOperator.AND
+        && !subLevelRelations.isEmpty()) {
+      relationWhere = relationWhere.where(subLevelRelations.getFirst());
+      for (int i = 1; i < subLevelRelations.size(); i++) {
+        relationWhere = relationWhere.and().where(subLevelRelations.get(i));
+      }
+    }
+
+    // construct or() relation
+    if (currentLogicalExpression.operator() == DBLogicalExpression.DBLogicalOperator.OR
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: Yuqi-Du
body: it is clear with two if statements here, I will do the forEach to make it more neat.
created_at: 2025-05-16T15:28:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
+   * Relation} or AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder
+   * expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR
+    var relationWhere = DefaultSubConditionRelation.subCondition();
+
+    // recursively build relations from sub_levels.
+    List<Relation> subLevelRelations =
+        Stream.concat(
+                currentLogicalExpression.filters().stream()
+                    .map(filter -> ((TableFilter) filter).apply(tableSchemaObject, objects)),
+                currentLogicalExpression.subExpressions().stream()
+                    .map(subExpression -> applyLogicalRelation(subExpression, objects)))
+            .toList();
+
+    // construct and() relation
+    if (currentLogicalExpression.operator() == DBLogicalExpression.DBLogicalOperator.AND
+        && !subLevelRelations.isEmpty()) {
+      relationWhere = relationWhere.where(subLevelRelations.getFirst());
+      for (int i = 1; i < subLevelRelations.size(); i++) {
+        relationWhere = relationWhere.and().where(subLevelRelations.get(i));
+      }
+    }
+
+    // construct or() relation
+    if (currentLogicalExpression.operator() == DBLogicalExpression.DBLogicalOperator.OR
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: Yuqi-Du
body: actually, converted to steam makes it complicated, will leave as is.
created_at: 2025-05-16T15:30:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -69,34 +68,81 @@ public static WithWarnings<TableWhereCQLClause<Update>> forUpdate(
   /**
    * Build an instance to add the where clause to a {@link Delete}.
    *
-   * <p>
-   *
-   * @param table
-   * @param dbLogicalExpression
-   * @return
+   * @param table the target table schema.
+   * @param dbLogicalExpression the DB LogicalExpression that contains all the filters and
+   *     logicalOperator
    */
   public static TableWhereCQLClause<Delete> forDelete(
       TableSchemaObject table, DBLogicalExpression dbLogicalExpression) {
     return new TableWhereCQLClause<>(table, dbLogicalExpression);
   }
 
-  @Override
-  public DBLogicalExpression getLogicalExpression() {
-    return dbLogicalExpression;
-  }
-
+  /**
+   * Apply the {@link TableWhereCQLClause} to the {@link OngoingWhereClause}. It will recursively
+   * apply all the filters and logical relations to build the {@link Relation} and feed to the
+   * {@link OngoingWhereClause}.
+   *
+   * @param tOngoingWhereClause the {@link OngoingWhereClause} to apply the filters and logical
+   *     relations to.
+   * @param objects the positional values to append to the {@link Relation}.
+   * @return the {@link OngoingWhereClause} with the filters and logical relations applied.
+   */
   @Override
   public T apply(T tOngoingWhereClause, List<Object> objects) {
-    // TODO BUG: this probably breaks order for nested expressions, for now enough to get this
-    // tested
     var tableFilters =
         dbLogicalExpression.filters().stream().map(dbFilter -> (TableFilter) dbFilter).toList();
 
-    // Add the where clause operations
-    for (TableFilter tableFilter : tableFilters) {
-      tOngoingWhereClause = tableFilter.apply(tableSchemaObject, tOngoingWhereClause, objects);
+    // If no filters at all, just return the ongoingWhereClause.
+    if (dbLogicalExpression.isEmpty()) {
+      return tOngoingWhereClause;
     }
-    return tOngoingWhereClause;
+
+    return tOngoingWhereClause.where(applyLogicalRelation(dbLogicalExpression, objects));
+  }
+
+  /**
+   * The helper method to recursively resolve the DBLogicalExpression into regular relation {@link
+   * Relation} or AND/OR relation {@link DefaultSubConditionRelation} that Driver QueryBuilder
+   * expects.
+   *
+   * @param currentLogicalExpression currentLogicalExpression.
+   * @param objects positionalValues to append in order.
+   * @return {@link Relation} conjunct relation from DBLogicalExpression.
+   */
+  private Relation applyLogicalRelation(
+      DBLogicalExpression currentLogicalExpression, List<Object> objects) {
+
+    // create the default relation to represent the current level of AND/OR
+    var relationWhere = DefaultSubConditionRelation.subCondition();
+
+    // recursively build relations from sub_levels.
+    List<Relation> subLevelRelations =
+        Stream.concat(
+                currentLogicalExpression.filters().stream()
+                    .map(filter -> ((TableFilter) filter).apply(tableSchemaObject, objects)),
+                currentLogicalExpression.subExpressions().stream()
+                    .map(subExpression -> applyLogicalRelation(subExpression, objects)))
+            .toList();
+
+    // construct and() relation
path: src/main/java/io/stargate/sgv2/jsonapi/service/operation/tables/TableWhereCQLClause.java
user: login: Yuqi-Du
body: foreach will make code complicated here,
since we need to fetch the first one as the start point, for loop is better here in my second thought.
Will leave as is.
created_at: 2025-05-16T15:31:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: Yuqi-Du
created_at: 2025-05-16T16:07:35Z
body: 

Note,
With this PR, you should see all the table CQL will have implicit AND translated to root level parenthesis.
E.G  implicit and: {"name": "John"} -> WHERE (name=?)
E.G implicit and with explicit or: {"$or": [{"name": "John"}, {"age": 30}]} -> WHERE ((name=? OR age=?))
 
Ideally, we don't need the parenthesis in the root level, but this is how the driver override works current, see DefaultSubConditionRelation.java, We can not remove the root level parenthesis currently to build the logical relation.

In other words, to build or concatenate logical relation, you must start with DefaultSubConditionRelation.subCondition(), and that brings the parenthesis.
author_association: CONTRIBUTOR
user: login: tatu-at-datastax
created_at: 2025-05-19T16:49:24Z
body: @Yuqi-Du I know https://issues.apache.org/jira/browse/CASSJAVA-47 is blocked/waiting, but I decided to vote for it at any rate. You may want to do the same, to show there is interest.


author_association: CONTRIBUTOR