fix(filters): relative dates in an analysis filter no longer answer HTTP 500
maven / build (push) Successful in 4m20s
build / maven (push) Successful in 4m20s

A filter criterion whose value is a filter expression (NowWithOffset, ThisOrLastPeriod, Now)
on a date or date-time field failed in DrillFilters.restoreTypes with 'Unsupported class
...NowWithOffset for converting to Instant': the value was converted to the field's type
before the query. Expressions are now passed through for core to evaluate, so a pivot
'created in the last day' works directly, after the JSON round trip of the UI API and of a
saved definition, and its drill filters carry the expression.

PivotContractTest.pivotWithRelativeDateFilter fails without the change on the memory, H2
and PostgreSQL engines.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
james.maesandClaude Fable 5.2 committed 2026-11-11 07:05:30 -06:00
1 parent 0e9c4ec2b8
commit a707215317
3 files changed
+36

No files matched your search

+4
View File
@@ -2,6 +2,10 @@
Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
## Unreleased - Relative dates in analysis filters
- Fixed HTTP 500 ("Unsupported class ...NowWithOffset for converting to Instant") from every analysis whose filter holds a relative date (a filter expression such as "now minus one day" or "this month") on a date or date-time field: the pivot, profiles, comparisons and the stand-in backend converted filter values to the field's type before querying and did not expect an expression. Expressions are now left for core to evaluate when the query runs, so "printed in the last day" works from the Analyses screens, over the UI API (`POST /qbit/data-analysis/tables/{table}/pivot`) and in a saved or scheduled analysis, where it means the last day on every run. Drill-through filters carry the expression.
## [Unreleased]
### Changed
@@ -302,9 +302,19 @@ public final class DrillFilters
/*******************************************************************************
** String decimal operands preserve exact scale and precision across JSON round trips.
** Filter expressions (relative dates) are left for core to evaluate.
*******************************************************************************/
private static Serializable restoreValue(com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType type, Serializable value)
{
if(value instanceof com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression<?>)
{
///////////////////////////////////////////////////////////////////////////
// a relative date ("now minus one day", "this month") is not a value to //
// convert: core evaluates it when the query runs, so a saved or //
// scheduled analysis means "the last day" on every run //
///////////////////////////////////////////////////////////////////////////
return (value);
}
if(type == com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType.DECIMAL)
{
return (value == null ? null : value.toString());
@@ -118,6 +118,28 @@ abstract class PivotContractTest extends EngineFixture
}
}
/*******************************************************************************
** A relative date in the filter ("created in the last N days") is evaluated
** by core when the pivot runs, also after the JSON round trip of an HTTP
** request or a saved definition, and the drills carry it.
*******************************************************************************/
@Test
void pivotWithRelativeDateFilter() throws Exception
{
PivotRequest everything = pivot(List.of("status"), List.of()).withFilter(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter()
.withCriteria("created", QCriteriaOperator.GREATER_THAN_OR_EQUALS, com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset.minus(36500, java.time.temporal.ChronoUnit.DAYS)));
for(PivotRequest request : List.of(everything, JsonUtils.toObject(JsonUtils.toJson(everything), PivotRequest.class)))
{
PivotResult result = service.pivot(request);
equalNumber(result.getGrandTotals().getFirst(), "12");
countDrills(result);
}
PivotRequest lastDay = pivot(List.of("status"), List.of()).withFilter(new com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter()
.withCriteria("created", QCriteriaOperator.GREATER_THAN_OR_EQUALS, com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset.minus(1, java.time.temporal.ChronoUnit.DAYS)));
assertThat(service.pivot(JsonUtils.toObject(JsonUtils.toJson(lastDay), PivotRequest.class)).getCells()).isEmpty();
}
/*******************************************************************************
** Verify or support the pivot and field-access contract.
*******************************************************************************/