From 262038bc87ed0b9e3855457ed6f43ec65410579b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 10 Oct 2022 08:40:51 -0500 Subject: [PATCH] Adding booleanOperator and subFilters to QQueryFilter --- .../actions/tables/query/QQueryFilter.java | 118 ++++++++++ .../memory/MemoryRecordStore.java | 202 +++++++++--------- .../memory/MemoryBackendModuleTest.java | 120 +++++++++-- .../rdbms/actions/AbstractRDBMSAction.java | 81 +++++-- .../rdbms/actions/RDBMSCountAction.java | 5 +- .../rdbms/actions/RDBMSDeleteAction.java | 3 +- .../rdbms/actions/RDBMSQueryAction.java | 4 +- .../rdbms/actions/RDBMSQueryActionTest.java | 74 +++++++ 8 files changed, 453 insertions(+), 154 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java index 638f9cda..ac6a99ce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* @@ -36,6 +37,20 @@ public class QQueryFilter implements Serializable, Cloneable private List criteria = new ArrayList<>(); private List orderBys = new ArrayList<>(); + private BooleanOperator booleanOperator = BooleanOperator.AND; + private List subFilters = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public enum BooleanOperator + { + AND, + OR + } + /******************************************************************************* @@ -66,6 +81,15 @@ public class QQueryFilter implements Serializable, Cloneable } } + if(subFilters != null) + { + clone.subFilters = new ArrayList<>(); + for(QQueryFilter subFilter : subFilters) + { + clone.subFilters.add(subFilter.clone()); + } + } + return clone; } catch(CloneNotSupportedException e) @@ -76,6 +100,32 @@ public class QQueryFilter implements Serializable, Cloneable + /******************************************************************************* + ** + *******************************************************************************/ + public boolean hasAnyCriteria() + { + if(CollectionUtils.nullSafeHasContents(criteria)) + { + return (true); + } + + if(CollectionUtils.nullSafeHasContents(subFilters)) + { + for(QQueryFilter subFilter : subFilters) + { + if(subFilter.hasAnyCriteria()) + { + return (true); + } + } + } + + return (false); + } + + + /******************************************************************************* ** Getter for criteria ** @@ -168,4 +218,72 @@ public class QQueryFilter implements Serializable, Cloneable return (this); } + + + /******************************************************************************* + ** Getter for booleanOperator + ** + *******************************************************************************/ + public BooleanOperator getBooleanOperator() + { + return booleanOperator; + } + + + + /******************************************************************************* + ** Setter for booleanOperator + ** + *******************************************************************************/ + public void setBooleanOperator(BooleanOperator booleanOperator) + { + this.booleanOperator = booleanOperator; + } + + + + /******************************************************************************* + ** Fluent setter for booleanOperator + ** + *******************************************************************************/ + public QQueryFilter withBooleanOperator(BooleanOperator booleanOperator) + { + this.booleanOperator = booleanOperator; + return (this); + } + + + + /******************************************************************************* + ** Getter for subFilters + ** + *******************************************************************************/ + public List getSubFilters() + { + return subFilters; + } + + + + /******************************************************************************* + ** Setter for subFilters + ** + *******************************************************************************/ + public void setSubFilters(List subFilters) + { + this.subFilters = subFilters; + } + + + + /******************************************************************************* + ** Fluent setter for subFilters + ** + *******************************************************************************/ + public QQueryFilter withSubFilters(List subFilters) + { + this.subFilters = subFilters; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index a7447453..894a842e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; @@ -141,126 +142,121 @@ public class MemoryRecordStore /******************************************************************************* ** *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") private boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord) { - boolean recordMatches = true; - if(filter != null && filter.getCriteria() != null) + if(filter == null || !filter.hasAnyCriteria()) { - for(QFilterCriteria criterion : filter.getCriteria()) - { - String fieldName = criterion.getFieldName(); - Serializable value = qRecord.getValue(fieldName); + return (true); + } - switch(criterion.getOperator()) + ///////////////////////////////////////////////////////////////////////////////////// + // for an AND query, default to a TRUE answer, and we'll &= each criteria's value. // + // for an OR query, default to FALSE, and |= each criteria's value. // + ///////////////////////////////////////////////////////////////////////////////////// + AtomicBoolean recordMatches = new AtomicBoolean(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.AND) ? true : false); + + /////////////////////////////////////// + // if there are criteria, apply them // + /////////////////////////////////////// + for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria())) + { + String fieldName = criterion.getFieldName(); + Serializable value = qRecord.getValue(fieldName); + + boolean criterionMatches = switch(criterion.getOperator()) { - case EQUALS: - { - recordMatches = testEquals(criterion, value); - break; - } - case NOT_EQUALS: - { - recordMatches = !testEquals(criterion, value); - break; - } - case IN: - { - recordMatches = testIn(criterion, value); - break; - } - case NOT_IN: - { - recordMatches = !testIn(criterion, value); - break; - } - case IS_BLANK: - { - recordMatches = testBlank(criterion, value); - break; - } - case IS_NOT_BLANK: - { - recordMatches = !testBlank(criterion, value); - break; - } - case CONTAINS: - { - recordMatches = testContains(criterion, fieldName, value); - break; - } - case NOT_CONTAINS: - { - recordMatches = !testContains(criterion, fieldName, value); - break; - } - case STARTS_WITH: - { - recordMatches = testStartsWith(criterion, fieldName, value); - break; - } - case NOT_STARTS_WITH: - { - recordMatches = !testStartsWith(criterion, fieldName, value); - break; - } - case ENDS_WITH: - { - recordMatches = testEndsWith(criterion, fieldName, value); - break; - } - case NOT_ENDS_WITH: - { - recordMatches = !testEndsWith(criterion, fieldName, value); - break; - } - case GREATER_THAN: - { - recordMatches = testGreaterThan(criterion, value); - break; - } - case GREATER_THAN_OR_EQUALS: - { - recordMatches = testGreaterThan(criterion, value) || testEquals(criterion, value); - break; - } - case LESS_THAN: - { - recordMatches = !testGreaterThan(criterion, value) && !testEquals(criterion, value); - break; - } - case LESS_THAN_OR_EQUALS: - { - recordMatches = !testGreaterThan(criterion, value); - break; - } - case BETWEEN: + case EQUALS -> testEquals(criterion, value); + case NOT_EQUALS -> !testEquals(criterion, value); + case IN -> testIn(criterion, value); + case NOT_IN -> !testIn(criterion, value); + case IS_BLANK -> testBlank(criterion, value); + case IS_NOT_BLANK -> !testBlank(criterion, value); + case CONTAINS -> testContains(criterion, fieldName, value); + case NOT_CONTAINS -> !testContains(criterion, fieldName, value); + case STARTS_WITH -> testStartsWith(criterion, fieldName, value); + case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value); + case ENDS_WITH -> testEndsWith(criterion, fieldName, value); + case NOT_ENDS_WITH -> !testEndsWith(criterion, fieldName, value); + case GREATER_THAN -> testGreaterThan(criterion, value); + case GREATER_THAN_OR_EQUALS -> testGreaterThan(criterion, value) || testEquals(criterion, value); + case LESS_THAN -> !testGreaterThan(criterion, value) && !testEquals(criterion, value); + case LESS_THAN_OR_EQUALS -> !testGreaterThan(criterion, value); + case BETWEEN -> { QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues()); QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues())); criteria1.getValues().remove(0); - recordMatches = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); - break; + yield (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); } - case NOT_BETWEEN: + case NOT_BETWEEN -> { QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues()); QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues()); criteria1.getValues().remove(0); - recordMatches = !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); - break; + yield !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); } - default: - { - throw new NotImplementedException("Operator [" + criterion.getOperator() + "] is not yet implemented in the Memory backend."); - } - } - if(!recordMatches) - { - break; - } + }; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Boolean shortCircuitValue = applyBooleanOperator(recordMatches, criterionMatches, filter.getBooleanOperator()); + if(shortCircuitValue != null) + { + return (shortCircuitValue); } } - return recordMatches; + + //////////////////////////////////////// + // apply sub-filters if there are any // + //////////////////////////////////////// + for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters())) + { + boolean subFilterMatches = doesRecordMatch(subFilter, qRecord); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + Boolean shortCircuitValue = applyBooleanOperator(recordMatches, subFilterMatches, filter.getBooleanOperator()); + if(shortCircuitValue != null) + { + return (shortCircuitValue); + } + } + + return (recordMatches.getPlain()); + } + + + + /******************************************************************************* + ** Based on an incoming boolean value (accumulator), a new value, and a boolean + ** operator, update the accumulator, and if we can then short-circuit remaining + ** operations, return a true or false. Returning null means to keep going. + *******************************************************************************/ + private Boolean applyBooleanOperator(AtomicBoolean accumulator, boolean newValue, QQueryFilter.BooleanOperator booleanOperator) + { + boolean accumulatorValue = accumulator.getPlain(); + if(booleanOperator.equals(QQueryFilter.BooleanOperator.AND)) + { + accumulatorValue &= newValue; + if(!accumulatorValue) + { + return (false); + } + } + else + { + accumulatorValue |= newValue; + if(accumulatorValue) + { + return (true); + } + } + + accumulator.set(accumulatorValue); + return (null); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java index 567129b9..3f451a6b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -55,6 +55,7 @@ import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -211,48 +212,49 @@ class MemoryBackendModuleTest insertInput.setTableName(table.getName()); insertInput.setRecords(List.of( new QRecord().withValue("id", 1).withValue("name", "Square").withValue("date", LocalDate.of(1980, Month.MAY, 31)), - new QRecord().withValue("id", 2).withValue("name", "Triangle").withValue("date", LocalDate.of(1999, Month.DECEMBER, 31)) + new QRecord().withValue("id", 2).withValue("name", "Triangle").withValue("date", LocalDate.of(1999, Month.DECEMBER, 31)), + new QRecord().withValue("id", 3).withValue("name", "Circle").withValue("date", LocalDate.of(2022, Month.OCTOBER, 10)) )); new InsertAction().execute(insertInput); assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.IN, List.of(1, 2))).size()); - assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.IN, List.of(2, 3))).size()); + assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.IN, List.of(3, 4))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.NOT_IN, List.of(3, 4))).size()); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.NOT_IN, List.of(4, 5))).size()); assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.NOT_IN, List.of(2, 3))).size()); assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.EQUALS, List.of("Square"))).size()); assertEquals("Square", queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.EQUALS, List.of("Square"))).get(0).getValue("name")); assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("notAFieldSoNull", QCriteriaOperator.EQUALS, List.of("Square"))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.NOT_EQUALS, List.of("notFound"))).size()); - assertEquals("Square", queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.NOT_EQUALS, List.of("Triangle"))).get(0).getValue("name")); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.NOT_EQUALS, List.of("notFound"))).size()); + assertEquals("Square", queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.NOT_EQUALS, List.of("Triangle", "Circle"))).get(0).getValue("name")); assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.CONTAINS, List.of("ria"))).size()); assertEquals("Triangle", queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.CONTAINS, List.of("ria"))).get(0).getValue("name")); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.NOT_CONTAINS, List.of("notFound"))).size()); - assertEquals("Square", queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.NOT_CONTAINS, List.of("ria"))).get(0).getValue("name")); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.NOT_CONTAINS, List.of("notFound"))).size()); + assertEquals("Square", queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.NOT_CONTAINS, List.of("le"))).get(0).getValue("name")); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.CONTAINS, List.of("ria")))); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.CONTAINS, List.of(1)))); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.CONTAINS, List.of()))); - assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN, List.of(LocalDate.of(2022, Month.SEPTEMBER, 1)))).size()); - assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN, List.of(LocalDate.of(1990, Month.JANUARY, 1)))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN, List.of(LocalDate.of(1970, Month.JANUARY, 1)))).size()); - assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, List.of(2))).size()); - assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, List.of(1))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, List.of(0))).size()); + assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN, List.of(LocalDate.of(2035, Month.JANUARY, 1)))).size()); + assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN, List.of(LocalDate.of(1990, Month.JANUARY, 1)))).size()); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN, List.of(LocalDate.of(1970, Month.JANUARY, 1)))).size()); + assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, List.of(3))).size()); + assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, List.of(1))).size()); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, List.of(0))).size()); - assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(LocalDate.of(2022, Month.SEPTEMBER, 1)))).size()); - assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(LocalDate.of(1990, Month.JANUARY, 1)))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(LocalDate.of(1970, Month.JANUARY, 1)))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(LocalDate.of(1980, Month.MAY, 31)))).size()); - assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(3))).size()); - assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(2))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(1))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(0))).size()); + assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(LocalDate.of(2035, Month.JANUARY, 1)))).size()); + assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(LocalDate.of(1990, Month.JANUARY, 1)))).size()); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(LocalDate.of(1970, Month.JANUARY, 1)))).size()); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(LocalDate.of(1980, Month.MAY, 31)))).size()); + assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(4))).size()); + assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(3))).size()); + assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(2))).size()); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, List.of(1))).size()); assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.LESS_THAN, List.of(LocalDate.of(2022, Month.SEPTEMBER, 1)))).size()); assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.LESS_THAN, List.of(LocalDate.of(1990, Month.JANUARY, 1)))).size()); @@ -265,7 +267,7 @@ class MemoryBackendModuleTest assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(LocalDate.of(1990, Month.JANUARY, 1)))).size()); assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(LocalDate.of(1980, Month.MAY, 31)))).size()); assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("date", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(LocalDate.of(1970, Month.JANUARY, 1)))).size()); - assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(3))).size()); + assertEquals(3, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(3))).size()); assertEquals(2, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(2))).size()); assertEquals(1, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(1))).size()); assertEquals(0, queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of(0))).size()); @@ -275,16 +277,88 @@ class MemoryBackendModuleTest assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN, List.of()))); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("id", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of()))); assertThrows(QException.class, () -> queryShapes(qInstance, table, session, new QFilterCriteria("name", QCriteriaOperator.LESS_THAN_OR_EQUALS, List.of("Bob")))); + + { + //////////////////////////// + // test a simple OR query // + //////////////////////////// + QQueryFilter filter = new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, List.of("Square"))) + .withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, List.of("Triangle"))); + assertEquals(2, queryShapes(qInstance, table, session, filter).size()); + assertThat(queryShapes(qInstance, table, session, filter)).anyMatch(r -> r.getValueString("name").equals("Square")); + assertThat(queryShapes(qInstance, table, session, filter)).anyMatch(r -> r.getValueString("name").equals("Triangle")); + } + + /////////////////////////////////////////////////// + // null or empty query - should find all records // + /////////////////////////////////////////////////// + assertEquals(3, queryShapes(qInstance, table, session, (QQueryFilter) null).size()); + assertEquals(3, queryShapes(qInstance, table, session, new QQueryFilter()).size()); + + { + ///////////////////////////////// + // test a complex nested query // + ///////////////////////////////// + QQueryFilter filter = new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withSubFilters(List.of( + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, List.of("Square"))) + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, List.of(1))), + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, List.of("Circle"))) + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, List.of(3))) + )); + assertEquals(2, queryShapes(qInstance, table, session, filter).size()); + assertThat(queryShapes(qInstance, table, session, filter)).anyMatch(r -> r.getValueString("name").equals("Square") && r.getValueInteger("id").equals(1)); + assertThat(queryShapes(qInstance, table, session, filter)).anyMatch(r -> r.getValueString("name").equals("Circle") && r.getValueInteger("id").equals(3)); + } + + { + ///////////////////////////////// + // test a complex nested query // + ///////////////////////////////// + QQueryFilter filter = new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withSubFilters(List.of( + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, List.of(1))) + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, List.of(3))), + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, List.of("Square"))) + .withCriteria(new QFilterCriteria("name", QCriteriaOperator.EQUALS, List.of("Circle"))) + )); + assertEquals(2, queryShapes(qInstance, table, session, filter).size()); + assertThat(queryShapes(qInstance, table, session, filter)).anyMatch(r -> r.getValueString("name").equals("Square") && r.getValueInteger("id").equals(1)); + assertThat(queryShapes(qInstance, table, session, filter)).anyMatch(r -> r.getValueString("name").equals("Circle") && r.getValueInteger("id").equals(3)); + } + } + /******************************************************************************* + ** + *******************************************************************************/ private List queryShapes(QInstance qInstance, QTableMetaData table, QSession session, QFilterCriteria criteria) throws QException + { + return queryShapes(qInstance, table, session, new QQueryFilter().withCriteria(criteria)); + } + + + + private List queryShapes(QInstance qInstance, QTableMetaData table, QSession session, QQueryFilter filter) throws QException { QueryInput queryInput = new QueryInput(qInstance); queryInput.setSession(session); queryInput.setTableName(table.getName()); - queryInput.setFilter(new QQueryFilter().withCriteria(criteria)); + queryInput.setFilter(filter); QueryOutput queryOutput = new QueryAction().execute(queryInput); return queryOutput.getRecords(); } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 9e66f9a8..346f0805 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -34,10 +34,12 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.QActionInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; @@ -161,7 +163,42 @@ public abstract class AbstractRDBMSAction implements QActionInterface /******************************************************************************* ** *******************************************************************************/ - protected String makeWhereClause(QTableMetaData table, List criteria, List params) throws IllegalArgumentException + protected String makeWhereClause(QTableMetaData table, QQueryFilter filter, List params) throws IllegalArgumentException + { + String clause = makeWhereClause(table, filter.getCriteria(), filter.getBooleanOperator(), params); + if(!CollectionUtils.nullSafeHasContents(filter.getSubFilters())) + { + /////////////////////////////////////////////////////////////// + // if there are no sub-clauses, then just return this clause // + /////////////////////////////////////////////////////////////// + return (clause); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // else, build a list of clauses - recursively expanding the sub-filters into clauses, then return them joined with our operator // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + List clauses = new ArrayList<>(); + if(StringUtils.hasContent(clause)) + { + clauses.add("(" + clause + ")"); + } + for(QQueryFilter subFilter : filter.getSubFilters()) + { + String subClause = makeWhereClause(table, subFilter, params); + if(StringUtils.hasContent(subClause)) + { + clauses.add("(" + subClause + ")"); + } + } + return (String.join(" " + filter.getBooleanOperator().toString() + " ", clauses)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String makeWhereClause(QTableMetaData table, List criteria, QQueryFilter.BooleanOperator booleanOperator, List params) throws IllegalArgumentException { List clauses = new ArrayList<>(); for(QFilterCriteria criterion : criteria) @@ -175,13 +212,13 @@ public abstract class AbstractRDBMSAction implements QActionInterface { case EQUALS: { - clause += " = ? "; + clause += " = ?"; expectedNoOfParams = 1; break; } case NOT_EQUALS: { - clause += " != ? "; + clause += " != ?"; expectedNoOfParams = 1; break; } @@ -196,7 +233,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface } else { - clause += " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ") "; + clause += " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")"; } break; } @@ -211,105 +248,105 @@ public abstract class AbstractRDBMSAction implements QActionInterface } else { - clause += " NOT IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ") "; + clause += " NOT IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")"; } break; } case STARTS_WITH: { - clause += " LIKE ? "; + clause += " LIKE ?"; editFirstValue(values, (s -> s + "%")); expectedNoOfParams = 1; break; } case ENDS_WITH: { - clause += " LIKE ? "; + clause += " LIKE ?"; editFirstValue(values, (s -> "%" + s)); expectedNoOfParams = 1; break; } case CONTAINS: { - clause += " LIKE ? "; + clause += " LIKE ?"; editFirstValue(values, (s -> "%" + s + "%")); expectedNoOfParams = 1; break; } case NOT_STARTS_WITH: { - clause += " NOT LIKE ? "; + clause += " NOT LIKE ?"; editFirstValue(values, (s -> s + "%")); expectedNoOfParams = 1; break; } case NOT_ENDS_WITH: { - clause += " NOT LIKE ? "; + clause += " NOT LIKE ?"; editFirstValue(values, (s -> "%" + s)); expectedNoOfParams = 1; break; } case NOT_CONTAINS: { - clause += " NOT LIKE ? "; + clause += " NOT LIKE ?"; editFirstValue(values, (s -> "%" + s + "%")); expectedNoOfParams = 1; break; } case LESS_THAN: { - clause += " < ? "; + clause += " < ?"; expectedNoOfParams = 1; break; } case LESS_THAN_OR_EQUALS: { - clause += " <= ? "; + clause += " <= ?"; expectedNoOfParams = 1; break; } case GREATER_THAN: { - clause += " > ? "; + clause += " > ?"; expectedNoOfParams = 1; break; } case GREATER_THAN_OR_EQUALS: { - clause += " >= ? "; + clause += " >= ?"; expectedNoOfParams = 1; break; } case IS_BLANK: { - clause += " IS NULL "; + clause += " IS NULL"; if(isString(field.getType())) { - clause += " OR " + column + " = '' "; + clause += " OR " + column + " = ''"; } expectedNoOfParams = 0; break; } case IS_NOT_BLANK: { - clause += " IS NOT NULL "; + clause += " IS NOT NULL"; if(isString(field.getType())) { - clause += " AND " + column + " != '' "; + clause += " AND " + column + " != ''"; } expectedNoOfParams = 0; break; } case BETWEEN: { - clause += " BETWEEN ? AND ? "; + clause += " BETWEEN ? AND ?"; expectedNoOfParams = 2; break; } case NOT_BETWEEN: { - clause += " NOT BETWEEN ? AND ? "; + clause += " NOT BETWEEN ? AND ?"; expectedNoOfParams = 2; break; } @@ -330,7 +367,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface params.addAll(values); } - return (String.join(" AND ", clauses)); + return (String.join(" " + booleanOperator.toString() + " ", clauses)); } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java index 885dc6f6..3a568f95 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java @@ -33,7 +33,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -62,9 +61,9 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf QQueryFilter filter = countInput.getFilter(); List params = new ArrayList<>(); - if(filter != null && CollectionUtils.nullSafeHasContents(filter.getCriteria())) + if(filter != null && filter.hasAnyCriteria()) { - sql += " WHERE " + makeWhereClause(table, filter.getCriteria(), params); + sql += " WHERE " + makeWhereClause(table, filter, params); } // todo sql customization - can edit sql and/or param list diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java index 4e5a8e7f..6753fe19 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java @@ -48,6 +48,7 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte private static final Logger LOG = LogManager.getLogger(RDBMSDeleteAction.class); + /******************************************************************************* ** *******************************************************************************/ @@ -258,7 +259,7 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte QTableMetaData table = deleteInput.getTable(); String tableName = getTableName(table); - String whereClause = makeWhereClause(table, filter.getCriteria(), params); + String whereClause = makeWhereClause(table, filter, params); // todo sql customization - can edit sql and/or param list? String sql = "DELETE FROM " diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index a901eab5..ad5e42d2 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -76,9 +76,9 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf QQueryFilter filter = queryInput.getFilter(); List params = new ArrayList<>(); - if(filter != null && CollectionUtils.nullSafeHasContents(filter.getCriteria())) + if(filter != null && filter.hasAnyCriteria()) { - sql += " WHERE " + makeWhereClause(table, filter.getCriteria(), params); + sql += " WHERE " + makeWhereClause(table, filter, params); } if(filter != null && CollectionUtils.nullSafeHasContents(filter.getOrderBys())) diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 97568eaa..a221bcc5 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -519,7 +519,81 @@ public class RDBMSQueryActionTest extends RDBMSActionTest queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.NOT_IN, List.of()))); queryOutput = new QueryAction().execute(queryInput); Assertions.assertEquals(5, queryOutput.getRecords().size(), "NOT_IN empty list should find everything."); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOr() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Darin"))) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Tim"))) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + Assertions.assertEquals(2, queryOutput.getRecords().size(), "OR should find 2 rows"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNestedFilterAndOrOr() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withSubFilters(List.of( + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("James"))) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Maes"))), + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Darin"))) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Kelkhoff"))) + )) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + Assertions.assertEquals(2, queryOutput.getRecords().size(), "Complex query should find 2 rows"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("James") && r.getValueString("lastName").equals("Maes")); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Darin") && r.getValueString("lastName").equals("Kelkhoff")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNestedFilterOrAndAnd() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withSubFilters(List.of( + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("James"))) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Tim"))), + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Kelkhoff"))) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Chamberlain"))) + )) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + Assertions.assertEquals(1, queryOutput.getRecords().size(), "Complex query should find 1 row"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueString("firstName").equals("Tim") && r.getValueString("lastName").equals("Chamberlain")); } } \ No newline at end of file