From b03de8ec0f9d78b1db6af4982e24fe47c512737b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 16 Feb 2024 10:17:26 -0600 Subject: [PATCH 1/3] CE-798 - Add de-duplication of (some) redundant criteria in dashboard links --- .../dashboard/AbstractHTMLWidgetRenderer.java | 7 + .../core/utils/QQueryFilterDeduper.java | 363 ++++++++++++++++++ .../AbstractHTMLWidgetRendererTest.java | 58 +++ .../core/utils/QQueryFilterDeduperTest.java | 355 +++++++++++++++++ 4 files changed, 783 insertions(+) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduper.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRendererTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduperTest.java 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 From 6e3ef2254af2c173d855bee94e6a1c9de331835e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 16 Feb 2024 10:20:39 -0600 Subject: [PATCH 2/3] CE-798 - Add equals and hashcode, to support de-deup --- .../actions/tables/query/QFilterCriteria.java | 34 +++++++++++++++++++ .../actions/tables/query/QQueryFilter.java | 33 ++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java index 811f2491..bf14f05f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Objects; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer; @@ -346,4 +347,37 @@ public class QFilterCriteria implements Serializable, Cloneable return (rs.toString()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + + if(o == null || getClass() != o.getClass()) + { + return false; + } + + QFilterCriteria that = (QFilterCriteria) o; + return Objects.equals(fieldName, that.fieldName) && operator == that.operator && Objects.equals(values, that.values) && Objects.equals(otherFieldName, that.otherFieldName); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int hashCode() + { + return Objects.hash(fieldName, operator, values, otherFieldName); + } + } 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 6ce122bb..506d067a 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 @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -467,4 +468,36 @@ public class QQueryFilter implements Serializable, Cloneable return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + + if(o == null || getClass() != o.getClass()) + { + return false; + } + + QQueryFilter that = (QQueryFilter) o; + return Objects.equals(criteria, that.criteria) && Objects.equals(orderBys, that.orderBys) && booleanOperator == that.booleanOperator && Objects.equals(subFilters, that.subFilters) && Objects.equals(skip, that.skip) && Objects.equals(limit, that.limit); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int hashCode() + { + return Objects.hash(criteria, orderBys, booleanOperator, subFilters, skip, limit); + } } From d54010e89d7c1d55c1a259f2f378e8ead2268b92 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Feb 2024 10:27:24 -0600 Subject: [PATCH 3/3] Add order-by to the query used for running automations; updated logs. --- .../PollingAutomationPerTableRunner.java | 53 +++++++++++++-- .../PollingAutomationPerTableRunnerTest.java | 67 +++++++++++++++++++ 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java index 88602fc4..b6ba394f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunner.java @@ -28,6 +28,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; @@ -60,6 +61,8 @@ import com.kingsrook.qqq.backend.core.model.automation.TableTrigger; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; @@ -257,7 +260,7 @@ public class PollingAutomationPerTableRunner implements Runnable } catch(Exception e) { - LOG.warn("Error running automations", e); + LOG.warn("Error running automations", e, logPair("tableName", tableActions.tableName()), logPair("status", tableActions.status())); } finally { @@ -301,7 +304,9 @@ public class PollingAutomationPerTableRunner implements Runnable AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType(); if(AutomationStatusTrackingType.FIELD_IN_TABLE.equals(statusTrackingType)) { - queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId())))); + QQueryFilter filter = new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))); + addOrderByToQueryFilter(table, automationStatus, filter); + queryInput.setFilter(filter); } else { @@ -330,6 +335,38 @@ public class PollingAutomationPerTableRunner implements Runnable + /******************************************************************************* + ** + *******************************************************************************/ + static void addOrderByToQueryFilter(QTableMetaData table, AutomationStatus automationStatus, QQueryFilter filter) + { + //////////////////////////////////////////////////////////////////////////////////// + // look for a field in the table with either create-date or modify-date behavior, // + // based on if doing insert or update automations // + //////////////////////////////////////////////////////////////////////////////////// + DynamicDefaultValueBehavior dynamicDefaultValueBehavior = automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS) ? DynamicDefaultValueBehavior.CREATE_DATE : DynamicDefaultValueBehavior.MODIFY_DATE; + Optional field = table.getFields().values().stream() + .filter(f -> dynamicDefaultValueBehavior.equals(f.getBehaviorOrDefault(QContext.getQInstance(), DynamicDefaultValueBehavior.class))) + .findFirst(); + + if(field.isPresent()) + { + ////////////////////////////////////////////////////////////////////// + // if a create/modify date field was found, order by it (ascending) // + ////////////////////////////////////////////////////////////////////// + filter.addOrderBy(new QFilterOrderBy(field.get().getName())); + } + else + { + //////////////////////////////////// + // else, order by the primary key // + //////////////////////////////////// + filter.addOrderBy(new QFilterOrderBy(table.getPrimaryKeyField())); + } + } + + + /******************************************************************************* ** get the actions to run against a table in an automation status. both from ** metaData and tableTriggers/data. @@ -458,13 +495,15 @@ public class PollingAutomationPerTableRunner implements Runnable //////////////////////////////////////// // update status on all these records // //////////////////////////////////////// - if(anyActionsFailed) + AutomationStatus statusToUpdateTo = anyActionsFailed ? pendingToFailedStatusMap.get(automationStatus) : AutomationStatus.OK; + try { - RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToFailedStatusMap.get(automationStatus)); + RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, statusToUpdateTo); } - else + catch(Exception e) { - RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, AutomationStatus.OK); + LOG.warn("Error updating automationStatus after running automations", logPair("tableName", table), logPair("count", records.size()), logPair("status", statusToUpdateTo)); + throw (e); } } @@ -494,7 +533,7 @@ public class PollingAutomationPerTableRunner implements Runnable } catch(Exception e) { - LOG.warn("Caught exception processing records on " + table + " for action " + action, e); + LOG.warn("Caught exception processing automations", e, logPair("tableName", table), logPair("action", action.getName())); return (true); } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerTest.java index 6ce3868c..f1dc131a 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/automation/polling/PollingAutomationPerTableRunnerTest.java @@ -46,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior; 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.processes.QProcessMetaData; @@ -593,4 +594,70 @@ class PollingAutomationPerTableRunnerTest extends BaseTest new PollingAutomationPerTableRunner.ShardedTableActions(null, null, null, null, null).noopToFakeTestCoverage(); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAddOrderByToQueryFilter() + { + ////////////////////////////////////////////////////////////////////////// + // make a table we'll test with. just put a primary-key id on it first // + ////////////////////////////////////////////////////////////////////////// + QTableMetaData table = new QTableMetaData() + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER)); + { + QQueryFilter filter = new QQueryFilter(); + PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter); + assertEquals("id", filter.getOrderBys().get(0).getFieldName()); + } + + { + QQueryFilter filter = new QQueryFilter(); + PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter); + assertEquals("id", filter.getOrderBys().get(0).getFieldName()); + } + + //////////////////////////////////////////////////////////////////////////////// + // add createDate & modifyDate fields, but not with dynamic-default-behaviors // + // so should still sort by id // + //////////////////////////////////////////////////////////////////////////////// + QFieldMetaData createDate = new QFieldMetaData("createDate", QFieldType.DATE_TIME); + QFieldMetaData modifyDate = new QFieldMetaData("modifyDate", QFieldType.DATE_TIME); + table.addField(createDate); + table.addField(modifyDate); + { + QQueryFilter filter = new QQueryFilter(); + PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter); + assertEquals("id", filter.getOrderBys().get(0).getFieldName()); + } + + { + QQueryFilter filter = new QQueryFilter(); + PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter); + assertEquals("id", filter.getOrderBys().get(0).getFieldName()); + } + + ///////////////////////////////////////////////////////////////////////////////////// + // add dynamic default value behaviors, confirm create/modify date fields are used // + ///////////////////////////////////////////////////////////////////////////////////// + createDate.withBehavior(DynamicDefaultValueBehavior.CREATE_DATE); + modifyDate.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE); + + { + QQueryFilter filter = new QQueryFilter(); + PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_INSERT_AUTOMATIONS, filter); + assertEquals("createDate", filter.getOrderBys().get(0).getFieldName()); + } + + { + QQueryFilter filter = new QQueryFilter(); + PollingAutomationPerTableRunner.addOrderByToQueryFilter(table, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, filter); + assertEquals("modifyDate", filter.getOrderBys().get(0).getFieldName()); + } + + } + } \ No newline at end of file