diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/AbstractFilterExpression.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/AbstractFilterExpression.java index fe9cc897..fe3f4ddd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/AbstractFilterExpression.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/AbstractFilterExpression.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions; import java.io.Serializable; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; /******************************************************************************* @@ -35,7 +36,7 @@ public abstract class AbstractFilterExpression implement /******************************************************************************* ** *******************************************************************************/ - public abstract T evaluate() throws QException; + public abstract T evaluate(QFieldMetaData field) throws QException; @@ -47,7 +48,7 @@ public abstract class AbstractFilterExpression implement *******************************************************************************/ public T evaluateInputValues(Map inputValues) throws QException { - return evaluate(); + return evaluate(null); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/FilterVariableExpression.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/FilterVariableExpression.java index 5c3be8f7..71fc9d02 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/FilterVariableExpression.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/FilterVariableExpression.java @@ -26,6 +26,7 @@ import java.io.Serializable; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -45,7 +46,7 @@ public class FilterVariableExpression extends AbstractFilterExpression +public class Now extends AbstractFilterExpression { /******************************************************************************* ** *******************************************************************************/ @Override - public Instant evaluate() throws QException + public Serializable evaluate(QFieldMetaData field) throws QException { - return (Instant.now()); + QFieldType type = field == null ? QFieldType.DATE_TIME : field.getType(); + + if(type.equals(QFieldType.DATE_TIME)) + { + return (Instant.now()); + } + else if(type.equals(QFieldType.DATE)) + { + ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId(); + return (Instant.now().atZone(zoneId).toLocalDate()); + } + else + { + throw (new QException("Unsupported field type [" + type + "]")); + } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/NowWithOffset.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/NowWithOffset.java index c941ea9e..694dee91 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/NowWithOffset.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/NowWithOffset.java @@ -22,19 +22,24 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions; +import java.io.Serializable; import java.time.Instant; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.backend.core.exceptions.QException; +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.utils.ValueUtils; /******************************************************************************* ** *******************************************************************************/ -public class NowWithOffset extends AbstractFilterExpression +public class NowWithOffset extends AbstractFilterExpression { private Operator operator; private int amount; @@ -123,7 +128,30 @@ public class NowWithOffset extends AbstractFilterExpression ** *******************************************************************************/ @Override - public Instant evaluate() throws QException + public Serializable evaluate(QFieldMetaData field) throws QException + { + QFieldType type = field == null ? QFieldType.DATE_TIME : field.getType(); + + if(type.equals(QFieldType.DATE_TIME)) + { + return (evaluateForDateTime()); + } + else if(type.equals(QFieldType.DATE)) + { + return (evaluateForDate()); + } + else + { + throw (new QException("Unsupported field type [" + type + "]")); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private Instant evaluateForDateTime() { ///////////////////////////////////////////////////////////////////////////// // Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... // @@ -147,6 +175,26 @@ public class NowWithOffset extends AbstractFilterExpression + /*************************************************************************** + ** + ***************************************************************************/ + private LocalDate evaluateForDate() + { + ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId(); + LocalDate now = Instant.now().atZone(zoneId).toLocalDate(); + + if(operator.equals(Operator.PLUS)) + { + return (now.plus(amount, timeUnit)); + } + else + { + return (now.minus(amount, timeUnit)); + } + } + + + /******************************************************************************* ** Getter for operator ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/ThisOrLastPeriod.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/ThisOrLastPeriod.java index 0f0e10ce..9f154a01 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/ThisOrLastPeriod.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/expressions/ThisOrLastPeriod.java @@ -22,27 +22,32 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions; +import java.io.Serializable; import java.time.DayOfWeek; import java.time.Instant; +import java.time.LocalDate; 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.QException; import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; +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.utils.ValueUtils; /******************************************************************************* ** *******************************************************************************/ -public class ThisOrLastPeriod extends AbstractFilterExpression +public class ThisOrLastPeriod extends AbstractFilterExpression { private Operator operator; private ChronoUnit timeUnit; + /*************************************************************************** ** ***************************************************************************/ @@ -88,7 +93,7 @@ public class ThisOrLastPeriod extends AbstractFilterExpression ** Factory ** *******************************************************************************/ - public static ThisOrLastPeriod last(int amount, ChronoUnit timeUnit) + public static ThisOrLastPeriod last(ChronoUnit timeUnit) { return (new ThisOrLastPeriod(Operator.LAST, timeUnit)); } @@ -99,7 +104,31 @@ public class ThisOrLastPeriod extends AbstractFilterExpression ** *******************************************************************************/ @Override - public Instant evaluate() throws QException + public Serializable evaluate(QFieldMetaData field) throws QException + { + QFieldType type = field == null ? QFieldType.DATE_TIME : field.getType(); + + if(type.equals(QFieldType.DATE_TIME)) + { + return (evaluateForDateTime()); + } + else if(type.equals(QFieldType.DATE)) + { + // return (evaluateForDateTime()); + return (evaluateForDate()); + } + else + { + throw (new QException("Unsupported field type [" + type + "]")); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private Instant evaluateForDateTime() { ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId(); @@ -154,7 +183,57 @@ public class ThisOrLastPeriod extends AbstractFilterExpression return operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear; } - default -> throw (new QRuntimeException("Unsupported timeUnit: " + timeUnit)); + default -> throw (new QRuntimeException("Unsupported unit: " + timeUnit)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public LocalDate evaluateForDate() + { + ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId(); + LocalDate today = Instant.now().atZone(zoneId).toLocalDate(); + + switch(timeUnit) + { + case DAYS -> + { + return operator.equals(Operator.THIS) ? today : today.minusDays(1); + } + case WEEKS -> + { + LocalDate startOfThisWeek = today; + while(startOfThisWeek.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue()) + { + //////////////////////////////////////// + // go backwards until sunday is found // + //////////////////////////////////////// + startOfThisWeek = startOfThisWeek.minusDays(1); + } + return operator.equals(Operator.THIS) ? startOfThisWeek : startOfThisWeek.minusDays(7); + } + case MONTHS -> + { + Instant startOfThisMonth = ValueUtils.getStartOfMonthInZoneId(zoneId.getId()); + LocalDateTime startOfThisMonthLDT = LocalDateTime.ofInstant(startOfThisMonth, ZoneId.of(zoneId.getId())); + LocalDateTime startOfLastMonthLDT = startOfThisMonthLDT.minusMonths(1); + Instant startOfLastMonth = startOfLastMonthLDT.toInstant(ZoneId.of(zoneId.getId()).getRules().getOffset(Instant.now())); + + return (operator.equals(Operator.THIS) ? startOfThisMonth : startOfLastMonth).atZone(zoneId).toLocalDate(); + } + case YEARS -> + { + Instant startOfThisYear = ValueUtils.getStartOfYearInZoneId(zoneId.getId()); + LocalDateTime startOfThisYearLDT = LocalDateTime.ofInstant(startOfThisYear, zoneId); + LocalDateTime startOfLastYearLDT = startOfThisYearLDT.minusYears(1); + Instant startOfLastYear = startOfLastYearLDT.toInstant(zoneId.getRules().getOffset(Instant.now())); + + return (operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear).atZone(zoneId).toLocalDate(); + } + default -> throw (new QRuntimeException("Unsupported unit: " + timeUnit)); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index a547aaba..84c5928f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -58,6 +58,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; 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.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; @@ -170,6 +171,8 @@ public class MemoryRecordStore Collection tableData = getTableData(input.getTable()).values(); List records = new ArrayList<>(); + QQueryFilter filter = clonedOrNewFilter(input.getFilter()); + JoinsContext joinsContext = new JoinsContext(QContext.getQInstance(), input.getTableName(), input.getQueryJoins(), filter); if(CollectionUtils.nullSafeHasContents(input.getQueryJoins())) { tableData = buildJoinCrossProduct(input); @@ -185,7 +188,7 @@ public class MemoryRecordStore qRecord.setTableName(input.getTableName()); } - boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(input.getFilter(), qRecord); + boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(input.getFilter(), joinsContext, qRecord); if(recordMatches) { @@ -224,8 +227,7 @@ public class MemoryRecordStore *******************************************************************************/ private Collection buildJoinCrossProduct(QueryInput input) throws QException { - QInstance qInstance = QContext.getQInstance(); - JoinsContext joinsContext = new JoinsContext(qInstance, input.getTableName(), input.getQueryJoins(), input.getFilter()); + QInstance qInstance = QContext.getQInstance(); List crossProduct = new ArrayList<>(); QTableMetaData leftTable = input.getTable(); @@ -901,4 +903,21 @@ public class MemoryRecordStore return ValueUtils.getValueAsFieldType(fieldType, aggregateValue); } + + + + /******************************************************************************* + ** Either clone the input filter (so we can change it safely), or return a new blank filter. + *******************************************************************************/ + protected QQueryFilter clonedOrNewFilter(QQueryFilter filter) + { + if(filter == null) + { + return (new QQueryFilter()); + } + else + { + return (filter.clone()); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java index 6862e01f..281ee9cc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java @@ -34,14 +34,18 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; 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.expressions.AbstractFilterExpression; import com.kingsrook.qqq.backend.core.model.data.QRecord; +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.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.commons.lang.NotImplementedException; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -58,8 +62,22 @@ public class BackendQueryFilterUtils /******************************************************************************* ** Test if record matches filter. - *******************************************************************************/ + ******************************************************************************/ public static boolean doesRecordMatch(QQueryFilter filter, QRecord qRecord) + { + return doesRecordMatch(filter, null, qRecord); + } + + + + /******************************************************************************* + ** Test if record matches filter - where we are executing a QueryAction, and + ** we have a JoinsContext. Note, if you don't have one of those, you can call + ** the overload of this method that doesn't take one, and everything downstream + ** /should/ be tolerant of that being absent... You just might not have the + ** benefit of things like knowing field-meta-data associated with criteria... + *******************************************************************************/ + public static boolean doesRecordMatch(QQueryFilter filter, JoinsContext joinsContext, QRecord qRecord) { if(filter == null || !filter.hasAnyCriteria()) { @@ -97,7 +115,36 @@ public class BackendQueryFilterUtils } } - boolean criterionMatches = doesCriteriaMatch(criterion, fieldName, value); + /////////////////////////////////////////////////////////////////////////////////////////////// + // Test if this criteria(on) matches the record. // + // As criteria have become more sophisticated over time, we would like to be able to know // + // what field they are for. In general, we'll try to get that from the query's JoinsContext. // + // But, in some scenarios, that isn't available - so - be safe and defer to simpler methods // + // that might not have the full field, when necessary. // + /////////////////////////////////////////////////////////////////////////////////////////////// + Boolean criterionMatches = null; + if(joinsContext != null) + { + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = null; + try + { + fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(criterion.getFieldName()); + } + catch(Exception e) + { + LOG.debug("Exception getting field from joinsContext", e, logPair("fieldName", criterion.getFieldName())); + } + + if(fieldAndTableNameOrAlias != null) + { + criterionMatches = doesCriteriaMatch(criterion, fieldAndTableNameOrAlias.field(), value); + } + } + + if(criterionMatches == null) + { + criterionMatches = doesCriteriaMatch(criterion, criterion.getFieldName(), value); + } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // add this new value to the existing recordMatches value - and if we can short circuit the remaining checks, do so. // @@ -131,11 +178,24 @@ public class BackendQueryFilterUtils + /*************************************************************************** + ** + ***************************************************************************/ + public static boolean doesCriteriaMatch(QFilterCriteria criterion, String fieldName, Serializable value) + { + QFieldMetaData field = new QFieldMetaData(fieldName, ValueUtils.inferQFieldTypeFromValue(value, QFieldType.STRING)); + return doesCriteriaMatch(criterion, field, value); + } + + + /******************************************************************************* ** *******************************************************************************/ - public static boolean doesCriteriaMatch(QFilterCriteria criterion, String fieldName, Serializable value) + private static boolean doesCriteriaMatch(QFilterCriteria criterion, QFieldMetaData field, Serializable value) { + String fieldName = field == null ? "__unknownField" : field.getName(); + ListIterator valueListIterator = criterion.getValues().listIterator(); while(valueListIterator.hasNext()) { @@ -144,7 +204,7 @@ public class BackendQueryFilterUtils { try { - valueListIterator.set(expression.evaluate()); + valueListIterator.set(expression.evaluate(field)); } catch(QException qe) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 78a26d2a..f2ee6802 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.TimeZone; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QValueException; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum; import com.kingsrook.qqq.backend.core.model.session.QSession; @@ -51,6 +52,8 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; *******************************************************************************/ public class ValueUtils { + private static final QLogger LOG = QLogger.getLogger(ValueUtils.class); + private static final DateTimeFormatter dateTimeFormatter_yyyyMMddWithDashes = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter dateTimeFormatter_MdyyyyWithSlashes = DateTimeFormatter.ofPattern("M/d/yyyy"); private static final DateTimeFormatter dateTimeFormatter_yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd"); @@ -931,4 +934,48 @@ public class ValueUtils return (ZoneId.of(QContext.getQInstance().getDefaultTimeZoneId())); } + + + /*************************************************************************** + ** + ***************************************************************************/ + public static QFieldType inferQFieldTypeFromValue(Serializable value, QFieldType defaultIfCannotInfer) + { + if(value instanceof String) + { + return QFieldType.STRING; + } + else if(value instanceof Integer) + { + return QFieldType.INTEGER; + } + else if(value instanceof Long) + { + return QFieldType.LONG; + } + else if(value instanceof BigDecimal) + { + return QFieldType.DECIMAL; + } + else if(value instanceof Boolean) + { + return QFieldType.BOOLEAN; + } + else if(value instanceof Instant) + { + return QFieldType.DATE_TIME; + } + else if(value instanceof LocalDate) + { + return QFieldType.DATE; + } + else if(value instanceof LocalTime) + { + return QFieldType.TIME; + } + + LOG.debug("Could not infer QFieldType from value [" + (value == null ? "null" : value.getClass().getSimpleName()) + "]"); + + return defaultIfCannotInfer; + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java index a726eca3..d61a56c1 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java @@ -32,12 +32,14 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; import java.time.ZoneId; +import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QValueException; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.session.QSession; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -333,4 +335,28 @@ class ValueUtilsTest extends BaseTest assertEquals(ZoneId.of("UTC-05:00"), ValueUtils.getSessionOrInstanceZoneId()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInferQFieldTypeFromValue() + { + assertNull(ValueUtils.inferQFieldTypeFromValue(null, null)); + assertNull(ValueUtils.inferQFieldTypeFromValue(new ArrayList<>(), null)); + assertEquals(QFieldType.HTML, ValueUtils.inferQFieldTypeFromValue(new ArrayList<>(), QFieldType.HTML)); + + assertEquals(QFieldType.STRING, ValueUtils.inferQFieldTypeFromValue("value", null)); + assertEquals(QFieldType.INTEGER, ValueUtils.inferQFieldTypeFromValue(1, null)); + assertEquals(QFieldType.INTEGER, ValueUtils.inferQFieldTypeFromValue(Integer.valueOf("1"), null)); + assertEquals(QFieldType.LONG, ValueUtils.inferQFieldTypeFromValue(10_000_000_000L, null)); + assertEquals(QFieldType.DECIMAL, ValueUtils.inferQFieldTypeFromValue(BigDecimal.ZERO, null)); + assertEquals(QFieldType.BOOLEAN, ValueUtils.inferQFieldTypeFromValue(true, null)); + assertEquals(QFieldType.BOOLEAN, ValueUtils.inferQFieldTypeFromValue(Boolean.FALSE, null)); + assertEquals(QFieldType.DATE_TIME, ValueUtils.inferQFieldTypeFromValue(Instant.now(), null)); + assertEquals(QFieldType.DATE, ValueUtils.inferQFieldTypeFromValue(LocalDate.now(), null)); + assertEquals(QFieldType.TIME, ValueUtils.inferQFieldTypeFromValue(LocalTime.now(), null)); + } + } \ No newline at end of file diff --git a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java index 8fd22d08..33529ade 100644 --- a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java +++ b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java @@ -569,7 +569,7 @@ public class AbstractMongoDBAction { try { - valueListIterator.set(expression.evaluate()); + valueListIterator.set(expression.evaluate(field)); } catch(QException qe) { diff --git a/qqq-backend-module-rdbms/pom.xml b/qqq-backend-module-rdbms/pom.xml index 3e9be513..a3dad12e 100644 --- a/qqq-backend-module-rdbms/pom.xml +++ b/qqq-backend-module-rdbms/pom.xml @@ -81,6 +81,11 @@ junit-jupiter-engine test + + org.junit.jupiter + junit-jupiter-params + test + org.assertj assertj-core diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 501a45ba..dd8fdaa2 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -676,7 +676,7 @@ public abstract class AbstractRDBMSAction { try { - valueListIterator.set(expression.evaluate()); + valueListIterator.set(expression.evaluate(field)); } catch(QException qe) { diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 2b084bf0..7697e976 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -24,11 +24,14 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.io.Serializable; import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -38,20 +41,26 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; 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.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; 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.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; 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.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -84,6 +93,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest void afterEach() { AbstractRDBMSAction.setLogSQL(false); + QContext.getQSession().removeValue(QSession.VALUE_KEY_USER_TIMEZONE); } @@ -612,6 +622,101 @@ public class RDBMSQueryActionTest extends RDBMSActionTest + /******************************************************************************* + ** Adding additional test conditions, specifically for DATE-precision + *******************************************************************************/ + @ParameterizedTest() + @ValueSource(strings = { "UTC", "US/Eastern", "UTC+12" }) + void testMoreFilterExpressions(String userTimezone) throws QException + { + QContext.getQSession().setValue(QSession.VALUE_KEY_USER_TIMEZONE, userTimezone); + + LocalDate today = Instant.now().atZone(ZoneId.of(userTimezone)).toLocalDate(); + LocalDate yesterday = today.minusDays(1); + LocalDate tomorrow = today.plusDays(1); + + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON).withRecords(List.of( + new QRecord().withValue("email", "-").withValue("firstName", "yesterday").withValue("lastName", "ExpressionTest").withValue("birthDate", yesterday), + new QRecord().withValue("email", "-").withValue("firstName", "today").withValue("lastName", "ExpressionTest").withValue("birthDate", today), + new QRecord().withValue("email", "-").withValue("firstName", "tomorrow").withValue("lastName", "ExpressionTest").withValue("birthDate", tomorrow)) + )); + + UnsafeFunction, List, QException> testFunction = (filterConsumer) -> + { + QQueryFilter filter = new QQueryFilter().withCriteria("lastName", QCriteriaOperator.EQUALS, "ExpressionTest"); + filter.withOrderBy(new QFilterOrderBy("birthDate")); + filterConsumer.accept(filter); + + return QueryAction.execute(TestUtils.TABLE_NAME_PERSON, filter); + }; + + assertOneRecordWithFirstName("today", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, new Now())))); + assertOneRecordWithFirstName("tomorrow", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, new Now())))); + assertOneRecordWithFirstName("yesterday", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, new Now())))); + assertTwoRecordsWithFirstNames("yesterday", "today", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN_OR_EQUALS, new Now())))); + assertTwoRecordsWithFirstNames("today", "tomorrow", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN_OR_EQUALS, new Now())))); + + assertNoOfRecords(0, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.minus(1, ChronoUnit.DAYS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN_OR_EQUALS, NowWithOffset.plus(1, ChronoUnit.DAYS))))); + assertOneRecordWithFirstName("yesterday", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, NowWithOffset.minus(1, ChronoUnit.DAYS))))); + assertOneRecordWithFirstName("tomorrow", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, NowWithOffset.plus(1, ChronoUnit.DAYS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.plus(1, ChronoUnit.WEEKS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.plus(1, ChronoUnit.MONTHS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.plus(1, ChronoUnit.YEARS))))); + + assertThatThrownBy(() -> testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, NowWithOffset.plus(1, ChronoUnit.HOURS))))) + .hasRootCauseMessage("Unsupported unit: Hours"); + + assertOneRecordWithFirstName("today", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, ThisOrLastPeriod.this_(ChronoUnit.DAYS))))); + assertOneRecordWithFirstName("yesterday", testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.EQUALS, ThisOrLastPeriod.last(ChronoUnit.DAYS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, ThisOrLastPeriod.last(ChronoUnit.WEEKS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, ThisOrLastPeriod.last(ChronoUnit.MONTHS))))); + assertNoOfRecords(3, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, ThisOrLastPeriod.last(ChronoUnit.YEARS))))); + assertNoOfRecords(0, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.last(ChronoUnit.WEEKS))))); + assertNoOfRecords(0, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.last(ChronoUnit.MONTHS))))); + assertNoOfRecords(0, testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.last(ChronoUnit.YEARS))))); + + assertThatThrownBy(() -> testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.this_(ChronoUnit.HOURS))))) + .hasRootCauseMessage("Unsupported unit: Hours"); + assertThatThrownBy(() -> testFunction.apply(filter -> filter.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.LESS_THAN, ThisOrLastPeriod.last(ChronoUnit.MINUTES))))) + .hasRootCauseMessage("Unsupported unit: Minutes"); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void assertNoOfRecords(Integer expectedSize, List actualRecords) + { + assertEquals(expectedSize, actualRecords.size()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void assertOneRecordWithFirstName(String expectedFirstName, List actualRecords) + { + assertEquals(1, actualRecords.size()); + assertEquals(expectedFirstName, actualRecords.get(0).getValueString("firstName")); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void assertTwoRecordsWithFirstNames(String expectedFirstName0, String expectedFirstName1, List actualRecords) + { + assertEquals(2, actualRecords.size()); + assertEquals(expectedFirstName0, actualRecords.get(0).getValueString("firstName")); + assertEquals(expectedFirstName1, actualRecords.get(1).getValueString("firstName")); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -1017,8 +1122,8 @@ public class RDBMSQueryActionTest extends RDBMSActionTest @Test void testFieldNamesToInclude() throws QException { - QQueryFilter filter = new QQueryFilter().withCriteria("id", QCriteriaOperator.EQUALS, 1); - QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_PERSON).withFilter(filter); + QQueryFilter filter = new QQueryFilter().withCriteria("id", QCriteriaOperator.EQUALS, 1); + QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_PERSON).withFilter(filter); QRecord record = new QueryAction().execute(queryInput.withFieldNamesToInclude(null)).getRecords().get(0); assertTrue(record.getValues().containsKey("id"));