mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Stop doing criteria expressions as their own thing, and instead put them in the values list
This commit is contained in:
@ -28,7 +28,6 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
@ -47,8 +46,10 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
private QCriteriaOperator operator;
|
||||
private List<Serializable> values;
|
||||
|
||||
private String otherFieldName;
|
||||
private AbstractFilterExpression<?> expression;
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - probably implement this as a type of expression - though would require a little special handling i think when evaluating... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private String otherFieldName;
|
||||
|
||||
|
||||
|
||||
@ -101,23 +102,6 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFilterCriteria(String fieldName, QCriteriaOperator operator, AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
this.operator = operator;
|
||||
this.expression = expression;
|
||||
|
||||
///////////////////////////////////////
|
||||
// this guy doesn't like to be null? //
|
||||
///////////////////////////////////////
|
||||
this.values = new ArrayList<>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -335,35 +319,4 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for expression
|
||||
*******************************************************************************/
|
||||
public AbstractFilterExpression<?> getExpression()
|
||||
{
|
||||
return (this.expression);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for expression
|
||||
*******************************************************************************/
|
||||
public void setExpression(AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for expression
|
||||
*******************************************************************************/
|
||||
public QFilterCriteria withExpression(AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.expression = expression;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ import java.io.Serializable;
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractFilterExpression<T extends Serializable>
|
||||
public abstract class AbstractFilterExpression<T extends Serializable> implements Serializable
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
|
@ -23,7 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
@ -33,6 +36,9 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
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.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -73,31 +79,39 @@ public class QFilterCriteriaDeserializer extends StdDeserializer<QFilterCriteria
|
||||
/////////////////////////////////
|
||||
// get values out of json node //
|
||||
/////////////////////////////////
|
||||
List values = objectMapper.treeToValue(node.get("values"), List.class);
|
||||
String fieldName = objectMapper.treeToValue(node.get("fieldName"), String.class);
|
||||
QCriteriaOperator operator = objectMapper.treeToValue(node.get("operator"), QCriteriaOperator.class);
|
||||
String otherFieldName = objectMapper.treeToValue(node.get("otherFieldName"), String.class);
|
||||
List<Serializable> values = objectMapper.treeToValue(node.get("values"), List.class);
|
||||
String fieldName = objectMapper.treeToValue(node.get("fieldName"), String.class);
|
||||
QCriteriaOperator operator = objectMapper.treeToValue(node.get("operator"), QCriteriaOperator.class);
|
||||
String otherFieldName = objectMapper.treeToValue(node.get("otherFieldName"), String.class);
|
||||
|
||||
AbstractFilterExpression<?> expression = null;
|
||||
if(node.has("expression"))
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// look at all the values - if any of them are actually meant to be an Expression (instance of subclass of AbstractFilterExpression) //
|
||||
// they'll have deserialized as a Map, with a "type" key. If that's the case, then re/de serialize them into the proper expression type //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ListIterator<Serializable> valuesIterator = CollectionUtils.nonNullList(values).listIterator();
|
||||
while(valuesIterator.hasNext())
|
||||
{
|
||||
JsonNode expressionNode = node.get("expression");
|
||||
String expressionType = objectMapper.treeToValue(expressionNode.get("type"), String.class);
|
||||
Object value = valuesIterator.next();
|
||||
if(value instanceof Map<?, ?> map && map.containsKey("type"))
|
||||
{
|
||||
String expressionType = ValueUtils.getValueAsString(map.get("type"));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// right now, we'll assume that all expression subclasses are in the same package as AbstractFilterExpression //
|
||||
// so, we can just do a Class.forName on that name, and treeToValue like that. //
|
||||
// if we ever had to, we could instead switch(expressionType), and do like so... //
|
||||
// case "NowWithOffset" -> objectMapper.treeToValue(expressionNode, NowWithOffset.class); //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
String className = AbstractFilterExpression.class.getName().replace(AbstractFilterExpression.class.getSimpleName(), expressionType);
|
||||
expression = (AbstractFilterExpression<?>) objectMapper.treeToValue(expressionNode, Class.forName(className));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new IOException("Error deserializing expression of type [" + expressionType + "] inside QFilterCriteria", e));
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// right now, we'll assume that all expression subclasses are in the same package as AbstractFilterExpression //
|
||||
// so, we can just do a Class.forName on that name, and use JsonUtils.toObject requesting that class. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
String assumedExpressionJSON = JsonUtils.toJson(map);
|
||||
|
||||
String className = AbstractFilterExpression.class.getName().replace(AbstractFilterExpression.class.getSimpleName(), expressionType);
|
||||
Serializable replacementValue = (Serializable) JsonUtils.toObject(assumedExpressionJSON, Class.forName(className));
|
||||
valuesIterator.set(replacementValue);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new IOException("Error deserializing criteria value which appeared to be an expression of type [" + expressionType + "] inside QFilterCriteria", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,16 +123,7 @@ public class QFilterCriteriaDeserializer extends StdDeserializer<QFilterCriteria
|
||||
criteria.setOperator(operator);
|
||||
criteria.setValues(values);
|
||||
criteria.setOtherFieldName(otherFieldName);
|
||||
criteria.setExpression(expression);
|
||||
|
||||
return (criteria);
|
||||
|
||||
/*
|
||||
int id = (Integer) (node.get("id")).numberValue();
|
||||
String itemName = node.get("itemName").asText();
|
||||
int userId = (Integer) (node.get("createdBy")).numberValue();
|
||||
|
||||
return null;
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
@ -151,8 +151,27 @@ public class JsonUtils
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T> T toObject(String json, Class<T> targetClass) throws IOException
|
||||
{
|
||||
return (toObject(json, targetClass, null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** De-serialize a json string into an object of the specified class - with
|
||||
** customizations on the Jackson ObjectMapper.
|
||||
**.
|
||||
**
|
||||
** Internally using jackson - so jackson annotations apply!
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T> T toObject(String json, Class<T> targetClass, Consumer<ObjectMapper> objectMapperCustomizer) throws IOException
|
||||
{
|
||||
ObjectMapper objectMapper = newObjectMapper();
|
||||
if(objectMapperCustomizer != null)
|
||||
{
|
||||
objectMapperCustomizer.accept(objectMapper);
|
||||
}
|
||||
return objectMapper.reader().readValue(json, targetClass);
|
||||
}
|
||||
|
||||
@ -172,6 +191,25 @@ public class JsonUtils
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** De-serialize a json string into an object of the specified class - with
|
||||
** customizations on the Jackson ObjectMapper.
|
||||
**
|
||||
** Internally using jackson - so jackson annotations apply!
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T> T toObject(String json, TypeReference<T> typeReference, Consumer<ObjectMapper> objectMapperCustomizer) throws IOException
|
||||
{
|
||||
ObjectMapper objectMapper = newObjectMapper();
|
||||
if(objectMapperCustomizer != null)
|
||||
{
|
||||
objectMapperCustomizer.accept(objectMapper);
|
||||
}
|
||||
return objectMapper.readValue(json, typeReference);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** De-serialize a json string into a JSONObject (string must start with "{")
|
||||
**
|
||||
|
@ -30,13 +30,15 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.Now;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.ThisOrLastPeriod;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.BETWEEN;
|
||||
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 org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -52,6 +54,11 @@ class QFilterCriteriaDeserializerTest extends BaseTest
|
||||
@Test
|
||||
void testDeserialize() throws IOException
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// just put a reference to this class here, so it's a tad easier to find this class via navigation in IDE... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
new QFilterCriteriaDeserializer();
|
||||
|
||||
{
|
||||
QFilterCriteria criteria = JsonUtils.toObject("""
|
||||
{"fieldName": "id", "operator": "EQUALS", "values": [1]}
|
||||
@ -63,14 +70,13 @@ class QFilterCriteriaDeserializerTest extends BaseTest
|
||||
|
||||
{
|
||||
QFilterCriteria criteria = JsonUtils.toObject("""
|
||||
{"fieldName": "createDate", "operator": "GREATER_THAN", "expression":
|
||||
{"type": "NowWithOffset", "operator": "PLUS", "amount": 5, "timeUnit": "MINUTES"}
|
||||
{"fieldName": "createDate", "operator": "GREATER_THAN", "values":
|
||||
[{"type": "NowWithOffset", "operator": "PLUS", "amount": 5, "timeUnit": "MINUTES"}]
|
||||
}
|
||||
""", QFilterCriteria.class);
|
||||
assertEquals("createDate", criteria.getFieldName());
|
||||
assertEquals(GREATER_THAN, criteria.getOperator());
|
||||
assertNull(criteria.getValues());
|
||||
AbstractFilterExpression<?> expression = criteria.getExpression();
|
||||
AbstractFilterExpression<?> expression = (AbstractFilterExpression<?>) criteria.getValues().get(0);
|
||||
assertThat(expression).isInstanceOf(NowWithOffset.class);
|
||||
NowWithOffset nowWithOffset = (NowWithOffset) expression;
|
||||
assertEquals(5, nowWithOffset.getAmount());
|
||||
@ -80,14 +86,31 @@ class QFilterCriteriaDeserializerTest extends BaseTest
|
||||
|
||||
{
|
||||
QFilterCriteria criteria = JsonUtils.toObject("""
|
||||
{"fieldName": "orderDate", "operator": "EQUALS", "expression": {"type": "Now"} }
|
||||
{"fieldName": "orderDate", "operator": "EQUALS", "values": [{"type": "Now"}] }
|
||||
""", QFilterCriteria.class);
|
||||
assertEquals("orderDate", criteria.getFieldName());
|
||||
assertEquals(EQUALS, criteria.getOperator());
|
||||
assertNull(criteria.getValues());
|
||||
AbstractFilterExpression<?> expression = criteria.getExpression();
|
||||
AbstractFilterExpression<?> expression = (AbstractFilterExpression<?>) criteria.getValues().get(0);
|
||||
assertThat(expression).isInstanceOf(Now.class);
|
||||
}
|
||||
|
||||
{
|
||||
QFilterCriteria criteria = JsonUtils.toObject("""
|
||||
{"fieldName": "orderDate", "operator": "BETWEEN", "values": [{"type": "Now"}, {"type": "ThisOrLastPeriod"}] }
|
||||
""", QFilterCriteria.class);
|
||||
assertEquals("orderDate", criteria.getFieldName());
|
||||
assertEquals(BETWEEN, criteria.getOperator());
|
||||
AbstractFilterExpression<?> expression0 = (AbstractFilterExpression<?>) criteria.getValues().get(0);
|
||||
assertThat(expression0).isInstanceOf(Now.class);
|
||||
AbstractFilterExpression<?> expression1 = (AbstractFilterExpression<?>) criteria.getValues().get(1);
|
||||
assertThat(expression1).isInstanceOf(ThisOrLastPeriod.class);
|
||||
}
|
||||
|
||||
{
|
||||
assertThatThrownBy(() -> JsonUtils.toObject("""
|
||||
{"fieldName": "orderDate", "operator": "BETWEEN", "values": [{"type": "NotAnExpressionType"}] }
|
||||
""", QFilterCriteria.class)).hasMessageContaining("Error deserializing criteria value which appeared to be an expression");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -33,6 +33,7 @@ import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@ -57,6 +58,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
|
||||
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.DisplayFormat;
|
||||
@ -713,14 +715,23 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
values = Collections.emptyList();
|
||||
}
|
||||
else if(expectedNoOfParams.equals(1) && criterion.getExpression() != null)
|
||||
{
|
||||
values = List.of(criterion.getExpression().evaluate());
|
||||
}
|
||||
else if(!expectedNoOfParams.equals(values.size()))
|
||||
{
|
||||
throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]");
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// replace any expression-type values with their evaluation //
|
||||
//////////////////////////////////////////////////////////////
|
||||
ListIterator<Serializable> valueListIterator = values.listIterator();
|
||||
while(valueListIterator.hasNext())
|
||||
{
|
||||
Serializable value = valueListIterator.next();
|
||||
if(value instanceof AbstractFilterExpression<?> expression)
|
||||
{
|
||||
valueListIterator.set(expression.evaluate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clauses.add("(" + clause + ")");
|
||||
|
@ -29,7 +29,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
@ -559,7 +558,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
|
||||
QueryInput queryInput = initQueryRequest();
|
||||
queryInput.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest")))
|
||||
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withExpression(new Now())));
|
||||
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withValues(List.of(new Now()))));
|
||||
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
|
||||
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows");
|
||||
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row");
|
||||
@ -569,7 +568,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
|
||||
QueryInput queryInput = initQueryRequest();
|
||||
queryInput.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest")))
|
||||
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withExpression(NowWithOffset.plus(2, TimeUnit.DAYS))));
|
||||
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withValues(List.of(NowWithOffset.plus(2, ChronoUnit.DAYS)))));
|
||||
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
|
||||
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows");
|
||||
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row");
|
||||
@ -579,7 +578,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
|
||||
QueryInput queryInput = initQueryRequest();
|
||||
queryInput.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest")))
|
||||
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.GREATER_THAN).withExpression(NowWithOffset.minus(5, TimeUnit.DAYS))));
|
||||
.withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.GREATER_THAN).withValues(List.of(NowWithOffset.minus(5, ChronoUnit.DAYS)))));
|
||||
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
|
||||
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows");
|
||||
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row");
|
||||
|
Reference in New Issue
Block a user