diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java index 3705968c..26481e78 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -176,11 +177,13 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer { return (totalString); } + filter = QQueryFilterDeduper.dedupeFilter(filter); return ("" + totalString + ""); } + /******************************************************************************* ** *******************************************************************************/ @@ -192,6 +195,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer return; } + filter = QQueryFilterDeduper.dedupeFilter(filter); urls.add(tablePath + "?filter=" + JsonUtils.toJson(filter)); } @@ -208,6 +212,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer return (null); } + filter = QQueryFilterDeduper.dedupeFilter(filter); return (tablePath + "?filter=" + JsonUtils.toJson(filter)); } @@ -224,6 +229,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer return (null); } + filter = QQueryFilterDeduper.dedupeFilter(filter); return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset())); } @@ -326,6 +332,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer } String tablePath = QContext.getQInstance().getTablePath(tableName); + filter = QQueryFilterDeduper.dedupeFilter(filter); return (tablePath + "/" + processName + "?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8)); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduper.java new file mode 100644 index 00000000..b6d6e0d0 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduper.java @@ -0,0 +1,363 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.utils; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN; + + +/******************************************************************************* + ** Class to help deduplicate redundant criteria in filters. + ** + ** Original use-case is for making more clean url links out of filters. + ** + ** Does not (at this time) look into sub-filters at all, or support any "OR" + ** filters other than the most basic (a=1 OR a=1). + ** + ** Also, other than for completely redundant criteria (e.g., a>1 and a>1) only + * works on a limited subset of criteria operators (EQUALS, NOT_EQUALS, IN, and NOT_IN) + *******************************************************************************/ +public class QQueryFilterDeduper +{ + private static final QLogger LOG = QLogger.getLogger(QQueryFilterDeduper.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QQueryFilter dedupeFilter(QQueryFilter filter) + { + if(filter == null) + { + return (null); + } + + try + { + ///////////////////////////////////////////////////////////////// + // track (just for logging) if we failed or if we did any good // + ///////////////////////////////////////////////////////////////// + List log = new ArrayList<>(); + boolean fail = false; + boolean didAnyGood = false; + + ////////////////////////////////////////////////////////////////////////////////////////// + // always create a clone to be returned. this is especially useful because, // + // the clone's lists will be ArrayLists, which are mutable - since some of the deduping // + // involves manipulating value lists. // + ////////////////////////////////////////////////////////////////////////////////////////// + QQueryFilter rs = filter.clone(); + + //////////////////////////////////////////////////////////////////////////////////// + // general strategy is: // + // iterate over criteria, possibly removing the one the iterator is pointing at, // + // if we are able to somehow merge it into other criteria we've already seen. // + // the others-we've-seen will be tracked in the criteriaByFieldName listing hash. // + //////////////////////////////////////////////////////////////////////////////////// + ListingHash criteriaByFieldName = new ListingHash<>(); + Iterator iterator = rs.getCriteria().iterator(); + while(iterator.hasNext()) + { + QFilterCriteria criteria = iterator.next(); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // first thing to check is, have we seen any other criteria for this field - if so - try to do some de-duping. // + // note that, any time we do a remove, we'll need to do a continue - to avoid adding the now-removed criteria // + // to the listing hash // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(criteriaByFieldName.containsKey(criteria.getFieldName())) + { + List others = criteriaByFieldName.get(criteria.getFieldName()); + QFilterCriteria other = others.get(0); + + if(others.size() == 1 && other.equals(criteria)) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if we've only see 1 other criteria for this field so far, and this one is an exact match, then remove this one. // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + log.add(String.format("Remove duplicate criteria [%s]", criteria)); + iterator.remove(); + didAnyGood = true; + continue; + } + else + { + ///////////////////////////////////////////////////////////////////////////////////////// + // else - if there's still just 1 other, and it's an AND query - then apply some basic // + // logic-merging operations, based on the pair of criteria operators // + ///////////////////////////////////////////////////////////////////////////////////////// + if(others.size() == 1 && QQueryFilter.BooleanOperator.AND.equals(filter.getBooleanOperator())) + { + if((NOT_EQUALS.equals(other.getOperator()) || NOT_IN.equals(other.getOperator())) && EQUALS.equals(criteria.getOperator())) + { + /////////////////////////////////////////////////////////////////////////// + // if we previously saw a not-equals or not-in, and now we see an equals // + // and the value from the EQUALS isn't in the not-in list // + // then replace the not-equals with the equals // + // then just discard this equals // + /////////////////////////////////////////////////////////////////////////// + if(other.getValues().contains(criteria.getValues().get(0))) + { + log.add("Contradicting NOT_EQUALS/NOT_IN and EQUALS"); + fail = true; + } + else + { + other.setOperator(criteria.getOperator()); + other.setValues(criteria.getValues()); + iterator.remove(); + didAnyGood = true; + log.add("Replace a not-equals or not-in superseded by an equals"); + continue; + } + } + else if(EQUALS.equals(other.getOperator()) && (NOT_EQUALS.equals(criteria.getOperator()) || NOT_IN.equals(criteria.getOperator()))) + { + ///////////////////////////////////////////////////////////////////////////// + // if we previously saw an equals, and now we see a not-equals or a not-in // + // and the value from the EQUALS isn't in the not-in list // + // then just discard this not-equals // + ///////////////////////////////////////////////////////////////////////////// + if(criteria.getValues().contains(other.getValues().get(0))) + { + log.add("Contradicting NOT_EQUALS/NOT_IN and EQUALS"); + fail = true; + } + else + { + iterator.remove(); + didAnyGood = true; + log.add("Remove a redundant not-equals"); + continue; + } + } + else if(NOT_EQUALS.equals(other.getOperator()) && IN.equals(criteria.getOperator())) + { + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // if we previously saw a not-equals, and now we see an IN // + // then replace the not-equals with the IN (making sure the not-equals value isn't in the in-list) // + // then just discard this equals // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + Serializable notEqualsValue = other.getValues().get(0); + List inValues = new ArrayList<>(criteria.getValues()); + inValues.remove(notEqualsValue); + if(inValues.isEmpty()) + { + /////////////////////////////////////////////////////////////////////////////////// + // if the only in-value was the not-equal value, then... i don't know, don't try // + /////////////////////////////////////////////////////////////////////////////////// + log.add("Contradicting IN and NOT_EQUAL"); + fail = true; + } + else + { + ////////////////////////////////////////////////////////////////// + // else, we can proceed by replacing the not-equals with the in // + ////////////////////////////////////////////////////////////////// + other.setOperator(criteria.getOperator()); + other.setValues(criteria.getValues()); + iterator.remove(); + didAnyGood = true; + log.add("Replace superseded not-equals (removing its value from in-list)"); + continue; + } + } + else if(IN.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator())) + { + ////////////////////////////////////////////////////////////////// + // if we previously saw an in, and now we see a not-equals // + // discard the not-equals (removing its value from the in-list) // + // then just discard this not-equals // + ////////////////////////////////////////////////////////////////// + Serializable notEqualsValue = criteria.getValues().get(0); + List inValues = new ArrayList<>(other.getValues()); + inValues.remove(notEqualsValue); + if(inValues.isEmpty()) + { + /////////////////////////////////////////////////////////////////////////////////// + // if the only in-value was the not-equal value, then... i don't know, don't try // + /////////////////////////////////////////////////////////////////////////////////// + log.add("Contradicting IN and NOT_EQUAL"); + fail = true; + } + else + { + ////////////////////////////////////////////////////////////////// + // else, we can proceed by replacing the not-equals with the in // + ////////////////////////////////////////////////////////////////// + iterator.remove(); + didAnyGood = true; + log.add("Remove redundant not-equals (removing its value from in-list)"); + continue; + } + } + else if(NOT_EQUALS.equals(other.getOperator()) && NOT_IN.equals(criteria.getOperator())) + { + ///////////////////////////////////////////////////////////////////////////////////////// + // if we previously saw a not-equals, and now we see a not-in // + // we can change the not-equals to the not-in, and make sure it's value is in the list // + // then just discard this not-in // + ///////////////////////////////////////////////////////////////////////////////////////// + Serializable originalNotEqualsValue = other.getValues().get(0); + other.setOperator(criteria.getOperator()); + other.setValues(criteria.getValues()); + if(!other.getValues().contains(originalNotEqualsValue)) + { + other.getValues().add(originalNotEqualsValue); + } + iterator.remove(); + didAnyGood = true; + log.add("Replace superseded not-equals with not-in"); + continue; + } + else if(NOT_IN.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator())) + { + //////////////////////////////////////////////////////////////////////////////////////// + // if we previously saw a not-in, and now we see a not-equals // + // we can discard this not-equals, and just make sure its value is in the not-in list // + //////////////////////////////////////////////////////////////////////////////////////// + Serializable originalNotEqualsValue = criteria.getValues().get(0); + if(!other.getValues().contains(originalNotEqualsValue)) + { + other.getValues().add(originalNotEqualsValue); + } + iterator.remove(); + didAnyGood = true; + log.add("Remove not-equals, absorbing into not-in"); + continue; + } + else if(NOT_IN.equals(other.getOperator()) && NOT_IN.equals(criteria.getOperator())) + { + //////////////////////////////////////////////////////////////// + // for multiple not-ins, just merge their values (as a union) // + //////////////////////////////////////////////////////////////// + for(Serializable value : criteria.getValues()) + { + if(!other.getValues().contains(value)) + { + other.getValues().add(value); + } + } + iterator.remove(); + didAnyGood = true; + log.add("Merging not-ins"); + continue; + } + else if(IN.equals(other.getOperator()) && IN.equals(criteria.getOperator())) + { + //////////////////////////////////////////////////////////////////////// + // for multiple not-ins, just merge their values (as an intersection) // + //////////////////////////////////////////////////////////////////////// + Set otherValues = new HashSet<>(other.getValues()); + Set criteriaValues = new HashSet<>(criteria.getValues()); + otherValues.retainAll(criteriaValues); + if(otherValues.isEmpty()) + { + log.add("Contradicting IN lists (no values)"); + fail = true; + } + else + { + other.setValues(new ArrayList<>(otherValues)); + iterator.remove(); + didAnyGood = true; + log.add("Merging not-ins"); + continue; + } + } + else if(NOT_EQUALS.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator())) + { + ///////////////////////////////////////////////////////////////////////////////////// + // if we have 2 not-equals, we can merge them in a not-in // + // we can assume their values are different, else they'd have been equals up above // + ///////////////////////////////////////////////////////////////////////////////////// + other.setOperator(NOT_IN); + other.setValues(new ArrayList<>(List.of(other.getValues().get(0), criteria.getValues().get(0)))); + iterator.remove(); + didAnyGood = true; + log.add("Merge two not-equals as not-in"); + continue; + } + else + { + log.add("Fail because unhandled operator pair"); + fail = true; + } + } + else + { + log.add("Fail because > 1 other or operator: OR"); + fail = true; + } + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if we reach here (e.g., no continue), then assuming we didn't remove the criteria, add it to the listing hash. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + criteriaByFieldName.add(criteria.getFieldName(), criteria); + } + + /////////////////////////// + // log based on booleans // + /////////////////////////// + if(fail && didAnyGood) + { + LOG.info("Partially unsuccessful dedupe of filter", logPair("original", filter), logPair("deduped", rs), logPair("log", log)); + } + else if(fail) + { + LOG.info("Unsuccessful dedupe of filter", logPair("filter", filter), logPair("log", log)); + } + else if(didAnyGood) + { + LOG.debug("Successful dedupe of filter", logPair("original", filter), logPair("deduped", rs), logPair("log", log)); + } + else + { + LOG.debug("No duplicates in filter, so nothing to dedupe", logPair("original", filter)); + } + + return rs; + } + catch(Exception e) + { + LOG.warn("Error de-duping filter", e, logPair("filter", filter)); + return (filter.clone()); + } + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRendererTest.java new file mode 100644 index 00000000..22d23494 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRendererTest.java @@ -0,0 +1,58 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.dashboard; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +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.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + + +/******************************************************************************* + ** Unit test for AbstractHTMLWidgetRenderer + *******************************************************************************/ +class AbstractHTMLWidgetRendererTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + String link = AbstractHTMLWidgetRenderer.getCountLink(null, TestUtils.TABLE_NAME_PERSON, new QQueryFilter() + .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)) + .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)), 2 + ); + + //////////////////////////////////////////////////// + // assert that filter de-duplication is occurring // + //////////////////////////////////////////////////// + assertThat(link).doesNotMatch(".*EQUALS.*EQUALS.*"); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduperTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduperTest.java new file mode 100644 index 00000000..2f596de6 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduperTest.java @@ -0,0 +1,355 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.utils; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import org.junit.jupiter.api.Test; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.GREATER_THAN; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN; +import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter.BooleanOperator.OR; +import static com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper.dedupeFilter; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for QQueryFilterDeduper + *******************************************************************************/ +class QQueryFilterDeduperTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testDegenerateCases() + { + assertNull(dedupeFilter(null)); + + QQueryFilter empty = new QQueryFilter(); + assertEquals(empty, dedupeFilter(empty)); + assertNotSame(empty, dedupeFilter(empty)); // method always clones, so, just assert that. + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSimpleFiltersWithNoChanges() + { + QQueryFilter oneCriteria = new QQueryFilter() + .withCriteria(new QFilterCriteria("a", EQUALS, 1)); + assertEquals(oneCriteria, dedupeFilter(oneCriteria)); + assertNotSame(oneCriteria, dedupeFilter(oneCriteria)); + + QQueryFilter twoCriteriaDifferentFields = new QQueryFilter() + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("b", GREATER_THAN, 2)); + assertEquals(twoCriteriaDifferentFields, dedupeFilter(twoCriteriaDifferentFields)); + assertNotSame(twoCriteriaDifferentFields, dedupeFilter(twoCriteriaDifferentFields)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOrs() + { + /////////////////////////////////////////////////////// + // we've only written the simplest cases with ORs... // + /////////////////////////////////////////////////////// + assertEquals(new QQueryFilter().withBooleanOperator(OR).withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withBooleanOperator(OR) + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + )); + + ////////////////////////////////////////////////////////////////////// + // just not built at this time - obviously, could become an IN list // + ////////////////////////////////////////////////////////////////////// + QQueryFilter notSupportedOrTwoEquals = new QQueryFilter() + .withBooleanOperator(OR) + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", EQUALS, 2)); + assertEquals(notSupportedOrTwoEquals, dedupeFilter(notSupportedOrTwoEquals)); + + /////////////////////////////////////////////////////////////////////////////////// + // I think the logic would be, that the EQUALS 1 would be removed (is redundant) // + /////////////////////////////////////////////////////////////////////////////////// + QQueryFilter notSupportedOrEqualsNotEquals = new QQueryFilter() + .withBooleanOperator(OR) + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2)); + assertEquals(notSupportedOrEqualsNotEquals, dedupeFilter(notSupportedOrEqualsNotEquals)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMoreOperators() + { + ////////////////////////////////////////////////////////////////////// + // only simplest case (of criteria being .equals()) is supported... // + ////////////////////////////////////////////////////////////////////// + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", GREATER_THAN, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("a", GREATER_THAN, 1)) + .withCriteria(new QFilterCriteria("a", GREATER_THAN, 1)) + )); + + /////////////////////////////////////////////////////////////////////////////////// + // in theory, we could do more, but we just haven't yet (e.g, this could be > 5) // + /////////////////////////////////////////////////////////////////////////////////// + QQueryFilter tooComplex = new QQueryFilter() + .withCriteria(new QFilterCriteria("f", GREATER_THAN, 1)) + .withCriteria(new QFilterCriteria("f", GREATER_THAN, 5)); + assertEquals(tooComplex, dedupeFilter(tooComplex)); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAllEquals() + { + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + )); + + assertEquals(new QQueryFilter() + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("b", EQUALS, 2)) + .withCriteria(new QFilterCriteria("c", EQUALS, 3)), + dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("b", EQUALS, 2)) + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("b", EQUALS, 2)) + .withCriteria(new QFilterCriteria("b", EQUALS, 2)) + .withCriteria(new QFilterCriteria("a", EQUALS, 1)) + .withCriteria(new QFilterCriteria("c", EQUALS, 3)) + .withCriteria(new QFilterCriteria("c", EQUALS, 3)) + .withCriteria(new QFilterCriteria("c", EQUALS, 3)) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testEqualsAndNotEqualsAndNotIn() + { + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2)) + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3)) + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4)) + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3)) + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4)) + )); + + //////////////////////////////////////////////////////////// + // this is a contradiction, so we choose not to dedupe it // + //////////////////////////////////////////////////////////// + QQueryFilter contradiction1 = new QQueryFilter() + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1)); + assertEquals(contradiction1, dedupeFilter(contradiction1)); + + QQueryFilter contradiction2 = new QQueryFilter() + .withCriteria(new QFilterCriteria("f", EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_IN, 0, 1)); + assertEquals(contradiction2, dedupeFilter(contradiction2)); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this case can collapse the two not-equals, but then fails to merge the equals with them, because they are a contradiction! // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3)) + .withCriteria(new QFilterCriteria("f", EQUALS, 2)), + dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3)) + .withCriteria(new QFilterCriteria("f", EQUALS, 2)) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNotEqualsAndNotIn() + { + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2, 3)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3)) + )); + + ////////////////////////////////////////////////////////////////////////////////////////// + // ideally, maybe, this would have the values ordered 1,2,3, but, is equivalent enough // + ////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3, 1)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2, 3)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2)) + .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3)) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInAndNotEquals() + { + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2, 3)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", IN, 2, 3)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2, 3)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", IN, 2, 3)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1)) + )); + + QQueryFilter contradiction1 = new QQueryFilter() + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1)) + .withCriteria(new QFilterCriteria("f", IN, 1)); + assertEquals(contradiction1, dedupeFilter(contradiction1)); + + QQueryFilter contradiction2 = new QQueryFilter() + .withCriteria(new QFilterCriteria("f", IN, 1)) + .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1)); + assertEquals(contradiction2, dedupeFilter(contradiction2)); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMultipleInLists() + { + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", IN, 1, 2)) + .withCriteria(new QFilterCriteria("f", IN, 2, 3)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 3, 4)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", IN, 1, 2, 3, 4)) + .withCriteria(new QFilterCriteria("f", IN, 3, 4, 5, 6)) + )); + + assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 3)), dedupeFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("f", IN, 1, 2, 3, 4)) + .withCriteria(new QFilterCriteria("f", IN, 3, 4, 5, 6)) + .withCriteria(new QFilterCriteria("f", IN, 1, 3, 5, 7)) + )); + + /////////////////////////////////////////////////////////////////// + // contradicting in-lists - we give up and refuse to simplify it // + /////////////////////////////////////////////////////////////////// + QQueryFilter contradiction = new QQueryFilter() + .withCriteria(new QFilterCriteria("f", IN, 1, 2)) + .withCriteria(new QFilterCriteria("f", IN, 3, 4)); + assertEquals(contradiction, dedupeFilter(contradiction)); + } + +} \ No newline at end of file