Merge branch 'feature/datetime-query-expressions' into integration/sprint-29

This commit is contained in:
2023-07-17 16:26:48 -05:00
10 changed files with 631 additions and 67 deletions

View File

@ -26,8 +26,9 @@ import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; 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.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.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -36,6 +37,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
* A single criteria Component of a Query * A single criteria Component of a Query
* *
*******************************************************************************/ *******************************************************************************/
@JsonDeserialize(using = QFilterCriteriaDeserializer.class)
public class QFilterCriteria implements Serializable, Cloneable public class QFilterCriteria implements Serializable, Cloneable
{ {
private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class); private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class);
@ -44,8 +46,10 @@ public class QFilterCriteria implements Serializable, Cloneable
private QCriteriaOperator operator; private QCriteriaOperator operator;
private List<Serializable> values; private List<Serializable> values;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - probably implement this as a type of expression - though would require a little special handling i think when evaluating... //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private String otherFieldName; private String otherFieldName;
private AbstractFilterExpression<?> expression;
@ -98,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<>();
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -332,35 +319,4 @@ public class QFilterCriteria implements Serializable, Cloneable
return (rs.toString()); 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);
}
} }

View File

@ -28,10 +28,31 @@ import java.io.Serializable;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public abstract class AbstractFilterExpression<T extends Serializable> public abstract class AbstractFilterExpression<T extends Serializable> implements Serializable
{ {
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public abstract T evaluate(); public abstract T evaluate();
/*******************************************************************************
** To help with serialization, define a "type" in all subclasses
*******************************************************************************/
public String getType()
{
return (getClass().getSimpleName());
}
/*******************************************************************************
** noop - but here so serialization won't be upset about there being a type
** in a json object.
*******************************************************************************/
public void setType(String type)
{
}
} }

View File

@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -31,9 +35,9 @@ import java.util.concurrent.TimeUnit;
*******************************************************************************/ *******************************************************************************/
public class NowWithOffset extends AbstractFilterExpression<Instant> public class NowWithOffset extends AbstractFilterExpression<Instant>
{ {
private final Operator operator; private Operator operator;
private final int amount; private int amount;
private final TimeUnit timeUnit; private ChronoUnit timeUnit;
@ -46,7 +50,17 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Constructor ** Constructor
** **
*******************************************************************************/ *******************************************************************************/
private NowWithOffset(Operator operator, int amount, TimeUnit timeUnit) public NowWithOffset()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private NowWithOffset(Operator operator, int amount, ChronoUnit timeUnit)
{ {
this.operator = operator; this.operator = operator;
this.amount = amount; this.amount = amount;
@ -59,7 +73,19 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Factory ** Factory
** **
*******************************************************************************/ *******************************************************************************/
@Deprecated
public static NowWithOffset minus(int amount, TimeUnit timeUnit) public static NowWithOffset minus(int amount, TimeUnit timeUnit)
{
return (minus(amount, timeUnit.toChronoUnit()));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset minus(int amount, ChronoUnit timeUnit)
{ {
return (new NowWithOffset(Operator.MINUS, amount, timeUnit)); return (new NowWithOffset(Operator.MINUS, amount, timeUnit));
} }
@ -70,7 +96,19 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Factory ** Factory
** **
*******************************************************************************/ *******************************************************************************/
@Deprecated
public static NowWithOffset plus(int amount, TimeUnit timeUnit) public static NowWithOffset plus(int amount, TimeUnit timeUnit)
{
return (plus(amount, timeUnit.toChronoUnit()));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset plus(int amount, ChronoUnit timeUnit)
{ {
return (new NowWithOffset(Operator.PLUS, amount, timeUnit)); return (new NowWithOffset(Operator.PLUS, amount, timeUnit));
} }
@ -83,14 +121,24 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
@Override @Override
public Instant evaluate() public Instant evaluate()
{ {
/////////////////////////////////////////////////////////////////////////////
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //
// but LocalDateTime does. So, make a LDT in UTC, do the plus/minus, then //
// convert back to Instant @ UTC //
/////////////////////////////////////////////////////////////////////////////
LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
LocalDateTime then;
if(operator.equals(Operator.PLUS)) if(operator.equals(Operator.PLUS))
{ {
return (Instant.now().plus(amount, timeUnit.toChronoUnit())); then = now.plus(amount, timeUnit);
} }
else else
{ {
return (Instant.now().minus(amount, timeUnit.toChronoUnit())); then = now.minus(amount, timeUnit);
} }
return (then.toInstant(ZoneOffset.UTC));
} }
@ -121,7 +169,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Getter for timeUnit ** Getter for timeUnit
** **
*******************************************************************************/ *******************************************************************************/
public TimeUnit getTimeUnit() public ChronoUnit getTimeUnit()
{ {
return timeUnit; return timeUnit;
} }

View File

@ -0,0 +1,178 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.model.actions.tables.query.expressions;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
{
private Operator operator;
private ChronoUnit timeUnit;
public enum Operator
{THIS, LAST}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ThisOrLastPeriod()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private ThisOrLastPeriod(Operator operator, ChronoUnit timeUnit)
{
this.operator = operator;
this.timeUnit = timeUnit;
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static ThisOrLastPeriod this_(ChronoUnit timeUnit)
{
return (new ThisOrLastPeriod(Operator.THIS, timeUnit));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static ThisOrLastPeriod last(int amount, ChronoUnit timeUnit)
{
return (new ThisOrLastPeriod(Operator.LAST, timeUnit));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant evaluate()
{
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
switch(timeUnit)
{
case HOURS ->
{
if(operator.equals(Operator.THIS))
{
return Instant.now().truncatedTo(ChronoUnit.HOURS);
}
else
{
return Instant.now().minus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
}
}
case DAYS ->
{
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
return operator.equals(Operator.THIS) ? startOfToday : startOfToday.minus(1, ChronoUnit.DAYS);
}
case WEEKS ->
{
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
LocalDateTime startOfThisWeekLDT = LocalDateTime.ofInstant(startOfToday, zoneId);
while(startOfThisWeekLDT.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
{
////////////////////////////////////////
// go backwards until sunday is found //
////////////////////////////////////////
startOfThisWeekLDT = startOfThisWeekLDT.minus(1, ChronoUnit.DAYS);
}
Instant startOfThisWeek = startOfThisWeekLDT.toInstant(zoneId.getRules().getOffset(startOfThisWeekLDT));
return operator.equals(Operator.THIS) ? startOfThisWeek : startOfThisWeek.minus(7, ChronoUnit.DAYS);
}
case MONTHS ->
{
Instant startOfThisMonth = ValueUtils.getStartOfMonthInZoneId(zoneId.getId());
LocalDateTime startOfThisMonthLDT = LocalDateTime.ofInstant(startOfThisMonth, ZoneId.of(zoneId.getId()));
LocalDateTime startOfLastMonthLDT = startOfThisMonthLDT.minus(1, ChronoUnit.MONTHS);
Instant startOfLastMonth = startOfLastMonthLDT.toInstant(ZoneId.of(zoneId.getId()).getRules().getOffset(Instant.now()));
return operator.equals(Operator.THIS) ? startOfThisMonth : startOfLastMonth;
}
case YEARS ->
{
Instant startOfThisYear = ValueUtils.getStartOfYearInZoneId(zoneId.getId());
LocalDateTime startOfThisYearLDT = LocalDateTime.ofInstant(startOfThisYear, zoneId);
LocalDateTime startOfLastYearLDT = startOfThisYearLDT.minus(1, ChronoUnit.YEARS);
Instant startOfLastYear = startOfLastYearLDT.toInstant(zoneId.getRules().getOffset(Instant.now()));
return operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear;
}
default -> throw (new QRuntimeException("Unsupported timeUnit: " + timeUnit));
}
}
/*******************************************************************************
** Getter for operator
**
*******************************************************************************/
public Operator getOperator()
{
return operator;
}
/*******************************************************************************
** Getter for timeUnit
**
*******************************************************************************/
public ChronoUnit getTimeUnit()
{
return timeUnit;
}
}

View File

@ -0,0 +1,129 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.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;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
/*******************************************************************************
** Custom jackson deserializer, to deal w/ abstract expression field
*******************************************************************************/
public class QFilterCriteriaDeserializer extends StdDeserializer<QFilterCriteria>
{
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteriaDeserializer()
{
this(null);
}
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteriaDeserializer(Class<?> vc)
{
super(vc);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QFilterCriteria deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException
{
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
ObjectMapper objectMapper = new ObjectMapper();
/////////////////////////////////
// get values out of json node //
/////////////////////////////////
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);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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())
{
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 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));
}
}
}
///////////////////////////////////
// put fields into return object //
///////////////////////////////////
QFilterCriteria criteria = new QFilterCriteria();
criteria.setFieldName(fieldName);
criteria.setOperator(operator);
criteria.setValues(values);
criteria.setOtherFieldName(otherFieldName);
return (criteria);
}
}

View File

@ -151,8 +151,27 @@ public class JsonUtils
** **
*******************************************************************************/ *******************************************************************************/
public static <T> T toObject(String json, Class<T> targetClass) throws IOException 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(); ObjectMapper objectMapper = newObjectMapper();
if(objectMapperCustomizer != null)
{
objectMapperCustomizer.accept(objectMapper);
}
return objectMapper.reader().readValue(json, targetClass); 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 "{") ** De-serialize a json string into a JSONObject (string must start with "{")
** **

View File

@ -0,0 +1,68 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.model.actions.tables.query.expressions;
import java.time.temporal.ChronoUnit;
import com.kingsrook.qqq.backend.core.BaseTest;
import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit test for NowWithOffset
*******************************************************************************/
class NowWithOffsetTest extends BaseTest
{
private static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
long now = System.currentTimeMillis();
long oneWeekAgoMillis = NowWithOffset.minus(1, ChronoUnit.WEEKS).evaluate().toEpochMilli();
assertThat(oneWeekAgoMillis).isCloseTo(now - (7 * DAY_IN_MILLIS), Offset.offset(10_000L));
long oneWeekFromNowMillis = NowWithOffset.plus(2, ChronoUnit.WEEKS).evaluate().toEpochMilli();
assertThat(oneWeekFromNowMillis).isCloseTo(now + (14 * DAY_IN_MILLIS), Offset.offset(10_000L));
long oneMonthAgoMillis = NowWithOffset.minus(1, ChronoUnit.MONTHS).evaluate().toEpochMilli();
assertThat(oneMonthAgoMillis).isCloseTo(now - (30 * DAY_IN_MILLIS), Offset.offset(10_000L + 2 * DAY_IN_MILLIS));
long oneMonthFromNowMillis = NowWithOffset.plus(2, ChronoUnit.MONTHS).evaluate().toEpochMilli();
assertThat(oneMonthFromNowMillis).isCloseTo(now + (60 * DAY_IN_MILLIS), Offset.offset(10_000L + 3 * DAY_IN_MILLIS));
long oneYearAgoMillis = NowWithOffset.minus(1, ChronoUnit.YEARS).evaluate().toEpochMilli();
assertThat(oneYearAgoMillis).isCloseTo(now - (365 * DAY_IN_MILLIS), Offset.offset(10_000L + 2 * DAY_IN_MILLIS));
long oneYearFromNowMillis = NowWithOffset.plus(2, ChronoUnit.YEARS).evaluate().toEpochMilli();
assertThat(oneYearFromNowMillis).isCloseTo(now + (730 * DAY_IN_MILLIS), Offset.offset(10_000L + 3 * DAY_IN_MILLIS));
}
}

View File

@ -0,0 +1,116 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.model.actions.tables.query.serialization;
import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.List;
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.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;
/*******************************************************************************
** Unit test for QFilterCriteriaDeserializer
*******************************************************************************/
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]}
""", QFilterCriteria.class);
assertEquals("id", criteria.getFieldName());
assertEquals(EQUALS, criteria.getOperator());
assertEquals(List.of(1), criteria.getValues());
}
{
QFilterCriteria criteria = JsonUtils.toObject("""
{"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());
AbstractFilterExpression<?> expression = (AbstractFilterExpression<?>) criteria.getValues().get(0);
assertThat(expression).isInstanceOf(NowWithOffset.class);
NowWithOffset nowWithOffset = (NowWithOffset) expression;
assertEquals(5, nowWithOffset.getAmount());
assertEquals(NowWithOffset.Operator.PLUS, nowWithOffset.getOperator());
assertEquals(ChronoUnit.MINUTES, nowWithOffset.getTimeUnit());
}
{
QFilterCriteria criteria = JsonUtils.toObject("""
{"fieldName": "orderDate", "operator": "EQUALS", "values": [{"type": "Now"}] }
""", QFilterCriteria.class);
assertEquals("orderDate", criteria.getFieldName());
assertEquals(EQUALS, criteria.getOperator());
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");
}
}
}

View File

@ -33,6 +33,7 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.ListIterator;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; 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.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.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.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.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
@ -713,14 +715,23 @@ public abstract class AbstractRDBMSAction implements QActionInterface
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
values = Collections.emptyList(); values = Collections.emptyList();
} }
else if(expectedNoOfParams.equals(1) && criterion.getExpression() != null)
{
values = List.of(criterion.getExpression().evaluate());
}
else if(!expectedNoOfParams.equals(values.size())) else if(!expectedNoOfParams.equals(values.size()))
{ {
throw new IllegalArgumentException("Incorrect number of values given for criteria [" + field.getName() + "]"); 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 + ")"); clauses.add("(" + clause + ")");

View File

@ -29,7 +29,6 @@ import java.util.Collections;
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.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate; import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
@ -559,7 +558,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
QueryInput queryInput = initQueryRequest(); QueryInput queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter() queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest"))) .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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows");
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); 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 queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter() queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest"))) .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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows");
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); 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 queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter() queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria().withFieldName("lastName").withOperator(QCriteriaOperator.EQUALS).withValues(List.of("ExpressionTest"))) .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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows");
Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row");