Merged dev into feature/CE-847-bug-triggers-running

This commit is contained in:
2024-02-20 09:29:29 -06:00
8 changed files with 963 additions and 7 deletions

View File

@ -28,6 +28,7 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; 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.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.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
@ -256,7 +259,7 @@ public class PollingAutomationPerTableRunner implements Runnable
} }
catch(Exception e) catch(Exception e)
{ {
LOG.warn("Error running automations", e); LOG.warn("Error running automations", e, logPair("tableName", tableActions.tableName()), logPair("status", tableActions.status()));
} }
finally finally
{ {
@ -300,7 +303,9 @@ public class PollingAutomationPerTableRunner implements Runnable
AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType(); AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType();
if(AutomationStatusTrackingType.FIELD_IN_TABLE.equals(statusTrackingType)) 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 else
{ {
@ -329,6 +334,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<QFieldMetaData> 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 ** get the actions to run against a table in an automation status. both from
** metaData and tableTriggers/data. ** metaData and tableTriggers/data.
@ -457,13 +494,15 @@ public class PollingAutomationPerTableRunner implements Runnable
//////////////////////////////////////// ////////////////////////////////////////
// update status on all these records // // update status on all these records //
//////////////////////////////////////// ////////////////////////////////////////
if(anyActionsFailed) AutomationStatus statusToUpdateTo = anyActionsFailed ? pendingToFailedStatusMap.get(automationStatus) : AutomationStatus.OK;
try
{ {
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, pendingToFailedStatusMap.get(automationStatus), null); RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, statusToUpdateTo, null);
} }
else catch(Exception e)
{ {
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, AutomationStatus.OK, null); LOG.warn("Error updating automationStatus after running automations", logPair("tableName", table), logPair("count", records.size()), logPair("status", statusToUpdateTo));
throw (e);
} }
} }
@ -493,7 +532,7 @@ public class PollingAutomationPerTableRunner implements Runnable
} }
catch(Exception e) 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); return (true);
} }
} }

View File

@ -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.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; 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.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -176,11 +177,13 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
{ {
return (totalString); return (totalString);
} }
filter = QQueryFilterDeduper.dedupeFilter(filter);
return ("<a href='" + tablePath + "?filter=" + JsonUtils.toJson(filter) + "'>" + totalString + "</a>"); return ("<a href='" + tablePath + "?filter=" + JsonUtils.toJson(filter) + "'>" + totalString + "</a>");
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -192,6 +195,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
return; return;
} }
filter = QQueryFilterDeduper.dedupeFilter(filter);
urls.add(tablePath + "?filter=" + JsonUtils.toJson(filter)); urls.add(tablePath + "?filter=" + JsonUtils.toJson(filter));
} }
@ -208,6 +212,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
return (null); return (null);
} }
filter = QQueryFilterDeduper.dedupeFilter(filter);
return (tablePath + "?filter=" + JsonUtils.toJson(filter)); return (tablePath + "?filter=" + JsonUtils.toJson(filter));
} }
@ -224,6 +229,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
return (null); return (null);
} }
filter = QQueryFilterDeduper.dedupeFilter(filter);
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset())); 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); String tablePath = QContext.getQInstance().getTablePath(tableName);
filter = QQueryFilterDeduper.dedupeFilter(filter);
return (tablePath + "/" + processName + "?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8)); return (tablePath + "/" + processName + "?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
} }

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer; 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()); 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);
}
} }

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -479,4 +480,36 @@ public class QQueryFilter implements Serializable, Cloneable
return (this); 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);
}
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> 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<String, QFilterCriteria> criteriaByFieldName = new ListingHash<>();
Iterator<QFilterCriteria> 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<QFilterCriteria> 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<Serializable> 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<Serializable> 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<Serializable> otherValues = new HashSet<>(other.getValues());
Set<Serializable> 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());
}
}
}

View File

@ -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.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; 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.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.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
@ -578,4 +579,70 @@ class PollingAutomationPerTableRunnerTest extends BaseTest
new PollingAutomationPerTableRunner.ShardedTableActions(null, null, null, null, null).noopToFakeTestCoverage(); 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());
}
}
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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.*");
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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));
}
}