This is an automated email from the ASF dual-hosted git repository.
yqm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new f55de8f9f61 Consolidate the conversion between Granularity and
VirtualColumn, and improve the mapping of granularity usage in projections.
(#18403)
f55de8f9f61 is described below
commit f55de8f9f6125a6ee89eea56c5c18ce1e651ce30
Author: Cece Mei <[email protected]>
AuthorDate: Fri Sep 12 12:09:16 2025 -0700
Consolidate the conversion between Granularity and VirtualColumn, and
improve the mapping of granularity usage in projections. (#18403)
* some
* style
* more-gran
* better-gran
* forbidden
* revert-expr
* review-comment
* more-gran
* gran-with-cache
* fix-build
* fix-test
* format
* format
* concurrent
* gran
* finer-gran
* format
* comment
---
.../druid/segment/DatasketchesProjectionTest.java | 2 +-
.../data/input/impl/AggregateProjectionSpec.java | 33 ++-
.../util/common/granularity/Granularities.java | 52 ++--
.../util/common/granularity/PeriodGranularity.java | 172 +++++++++++
.../query/expression/TimestampFloorExprMacro.java | 10 +-
.../druid/segment/projections/Projections.java | 37 ++-
.../input/impl/AggregateProjectionSpecTest.java | 87 +++++-
.../druid/granularity/QueryGranularityTest.java | 52 +++-
.../java/util/common/PeriodGranularityTest.java | 145 ++++++++++
.../druid/segment/CursorFactoryProjectionTest.java | 319 ++++++++++++++-------
.../druid/segment/projections/ProjectionsTest.java | 104 +++++++
11 files changed, 857 insertions(+), 156 deletions(-)
diff --git
a/extensions-core/datasketches/src/test/java/org/apache/druid/segment/DatasketchesProjectionTest.java
b/extensions-core/datasketches/src/test/java/org/apache/druid/segment/DatasketchesProjectionTest.java
index 6073b062303..51075d095f5 100644
---
a/extensions-core/datasketches/src/test/java/org/apache/druid/segment/DatasketchesProjectionTest.java
+++
b/extensions-core/datasketches/src/test/java/org/apache/druid/segment/DatasketchesProjectionTest.java
@@ -227,7 +227,7 @@ public class DatasketchesProjectionTest extends
InitializedNullHandlingTest
IncrementalIndexSchema.builder()
.withDimensionsSpec(dimensionsSpec)
.withRollup(false)
-
.withMinTimestamp(CursorFactoryProjectionTest.TIMESTAMP.getMillis())
+
.withMinTimestamp(CursorFactoryProjectionTest.UTC_MIDNIGHT.getMillis())
.withProjections(autoSchema ?
AUTO_PROJECTIONS : PROJECTIONS)
.build()
)
diff --git
a/processing/src/main/java/org/apache/druid/data/input/impl/AggregateProjectionSpec.java
b/processing/src/main/java/org/apache/druid/data/input/impl/AggregateProjectionSpec.java
index d9e09cc6fba..42fc58eb15f 100644
---
a/processing/src/main/java/org/apache/druid/data/input/impl/AggregateProjectionSpec.java
+++
b/processing/src/main/java/org/apache/druid/data/input/impl/AggregateProjectionSpec.java
@@ -29,6 +29,7 @@ import com.google.common.collect.Lists;
import org.apache.druid.error.InvalidInput;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.java.util.common.granularity.Granularity;
+import org.apache.druid.java.util.common.granularity.PeriodGranularity;
import org.apache.druid.query.OrderBy;
import org.apache.druid.query.aggregation.AggregatorFactory;
import org.apache.druid.query.filter.DimFilter;
@@ -37,6 +38,7 @@ import org.apache.druid.segment.VirtualColumn;
import org.apache.druid.segment.VirtualColumns;
import org.apache.druid.segment.column.ColumnHolder;
import org.apache.druid.utils.CollectionUtils;
+import org.joda.time.DateTimeZone;
import javax.annotation.Nullable;
import java.util.Arrays;
@@ -47,7 +49,7 @@ import java.util.stream.Collectors;
/**
* API type to specify an aggregating projection on {@link
org.apache.druid.segment.incremental.IncrementalIndexSchema}
- *
+ * <p>
* Decorated with {@link JsonTypeInfo} annotations as a future-proofing
mechanism in the event we add other types of
* projections and need to extract out a base interface from this class.
*/
@@ -208,7 +210,10 @@ public class AggregateProjectionSpec
'}';
}
- private static ProjectionOrdering computeOrdering(VirtualColumns
virtualColumns, List<DimensionSchema> groupingColumns)
+ private static ProjectionOrdering computeOrdering(
+ VirtualColumns virtualColumns,
+ List<DimensionSchema> groupingColumns
+ )
{
if (groupingColumns.isEmpty()) {
// no ordering since there is only 1 row for this projection
@@ -218,24 +223,30 @@ public class AggregateProjectionSpec
String timeColumnName = null;
Granularity granularity = null;
- // try to find the __time column equivalent, which might be a time_floor
expression to model granularity
- // bucketing. The time column is decided as the finest granularity on
__time detected. If the projection does
- // not have a time-like column, the granularity will be handled as ALL for
the projection and all projection
- // rows will use a synthetic timestamp of the minimum timestamp of the
incremental index
+
+ // determine the granularity and time column name for the projection,
based on the finest time-like grouping column.
for (final DimensionSchema dimension : groupingColumns) {
ordering.add(OrderBy.ascending(dimension.getName()));
if (ColumnHolder.TIME_COLUMN_NAME.equals(dimension.getName())) {
timeColumnName = dimension.getName();
- granularity = Granularities.NONE;
+ // already found exact __time grouping, skip assigning, granularity =
Granularities.NONE;
+ break;
} else {
final VirtualColumn vc =
virtualColumns.getVirtualColumn(dimension.getName());
final Granularity maybeGranularity =
Granularities.fromVirtualColumn(vc);
- if (granularity == null && maybeGranularity != null) {
- granularity = maybeGranularity;
+ if (maybeGranularity == null ||
maybeGranularity.equals(Granularities.ALL)) {
+ // no __time in inputs or not supported, skip
+ } else if (Granularities.NONE.equals(maybeGranularity)) {
timeColumnName = dimension.getName();
- } else if (granularity != null && maybeGranularity != null &&
maybeGranularity.isFinerThan(granularity)) {
- granularity = maybeGranularity;
+ // already found exact __time grouping, skip assigning, granularity
= Granularities.NONE;
+ break;
+ } else if (maybeGranularity.getClass().equals(PeriodGranularity.class)
+ && maybeGranularity.getTimeZone().equals(DateTimeZone.UTC)
+ && ((PeriodGranularity) maybeGranularity).getOrigin() == null
+ && (granularity == null ||
maybeGranularity.isFinerThan(granularity))) {
+ // found a finer period granularity than the existing granularity,
or it's the first one
timeColumnName = dimension.getName();
+ granularity = maybeGranularity;
}
}
}
diff --git
a/processing/src/main/java/org/apache/druid/java/util/common/granularity/Granularities.java
b/processing/src/main/java/org/apache/druid/java/util/common/granularity/Granularities.java
index b3fa3e8ede7..2283aea23e1 100644
---
a/processing/src/main/java/org/apache/druid/java/util/common/granularity/Granularities.java
+++
b/processing/src/main/java/org/apache/druid/java/util/common/granularity/Granularities.java
@@ -37,7 +37,6 @@ import org.apache.druid.segment.VirtualColumns;
import org.apache.druid.segment.column.ColumnHolder;
import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.segment.virtual.ExpressionVirtualColumn;
-import org.joda.time.chrono.ISOChronology;
import javax.annotation.Nullable;
import java.util.Arrays;
@@ -145,10 +144,11 @@ public class Granularities
/**
* Translates a {@link Granularity} to a {@link ExpressionVirtualColumn} on
{@link ColumnHolder#TIME_COLUMN_NAME} of
- * the equivalent grouping column. If granularity is {@link #ALL}, this
method returns null since we are not grouping
- * on time. If granularity is a {@link PeriodGranularity} with UTC timezone
and no origin, this method returns a
- * virtual column with {@link TimestampFloorExprMacro.TimestampFloorExpr} of
the specified period. If granularity is
- * {@link #NONE}, or any other kind of granularity (duration, period with
non-utc timezone or origin) this method
+ * the equivalent grouping column.
+ * <ul>
+ * <li>If granularity is {@link #ALL}, this method returns null since we are
not grouping on time.
+ * <li>If granularity is a {@link PeriodGranularity}, we'd map it to {@link
TimestampFloorExprMacro.TimestampFloorExpr}.
+ * <li>If granularity is {@link #NONE}, or any other kind of granularity
(duration, period with non-utc timezone or origin) this method
* returns a virtual column with {@link
org.apache.druid.math.expr.IdentifierExpr} specifying
* {@link ColumnHolder#TIME_COLUMN_NAME} directly.
*/
@@ -158,16 +158,14 @@ public class Granularities
if (ALL.equals(granularity)) {
return null;
}
+
final String expression;
- if (NONE.equals(granularity) || granularity instanceof
DurationGranularity) {
- expression = ColumnHolder.TIME_COLUMN_NAME;
- } else {
+ if (granularity instanceof PeriodGranularity) {
PeriodGranularity period = (PeriodGranularity) granularity;
- if
(!ISOChronology.getInstanceUTC().getZone().equals(period.getTimeZone()) ||
period.getOrigin() != null) {
- expression = ColumnHolder.TIME_COLUMN_NAME;
- } else {
- expression =
TimestampFloorExprMacro.forQueryGranularity(period.getPeriod());
- }
+ expression = TimestampFloorExprMacro.forQueryGranularity(period);
+ } else {
+ // DurationGranularity or any other granularity that is not a
PeriodGranularity
+ expression = ColumnHolder.TIME_COLUMN_NAME;
}
return new ExpressionVirtualColumn(
@@ -194,14 +192,26 @@ public class Granularities
public static Granularity fromVirtualColumn(VirtualColumn virtualColumn)
{
if (virtualColumn instanceof ExpressionVirtualColumn) {
- final ExpressionVirtualColumn expressionVirtualColumn =
(ExpressionVirtualColumn) virtualColumn;
- final Expr expr = expressionVirtualColumn.getParsedExpression().get();
- if (expr instanceof TimestampFloorExprMacro.TimestampFloorExpr) {
- final TimestampFloorExprMacro.TimestampFloorExpr gran =
(TimestampFloorExprMacro.TimestampFloorExpr) expr;
- if (gran.getArg().getBindingIfIdentifier() != null) {
- return gran.getGranularity();
- }
- }
+ return fromExpr(((ExpressionVirtualColumn)
virtualColumn).getParsedExpression().get());
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Granularity fromExpr(Expr expr)
+ {
+ String identifier = expr.getBindingIfIdentifier();
+ if (identifier != null) {
+ // If the grouping is based on __time directly, return None.
+ // Otherwise, grouping based on non-time columns, return ALL.
+ return identifier.equals(ColumnHolder.TIME_COLUMN_NAME)
+ ? Granularities.NONE
+ : Granularities.ALL;
+ }
+
+ if (expr instanceof TimestampFloorExprMacro.TimestampFloorExpr) {
+ final TimestampFloorExprMacro.TimestampFloorExpr gran =
(TimestampFloorExprMacro.TimestampFloorExpr) expr;
+ return gran.getGranularity();
}
return null;
}
diff --git
a/processing/src/main/java/org/apache/druid/java/util/common/granularity/PeriodGranularity.java
b/processing/src/main/java/org/apache/druid/java/util/common/granularity/PeriodGranularity.java
index ba9ba02feb9..909e80f72c9 100644
---
a/processing/src/main/java/org/apache/druid/java/util/common/granularity/PeriodGranularity.java
+++
b/processing/src/main/java/org/apache/druid/java/util/common/granularity/PeriodGranularity.java
@@ -40,6 +40,15 @@ import org.joda.time.format.DateTimeFormatter;
import javax.annotation.Nullable;
import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.zone.ZoneOffsetTransition;
+import java.time.zone.ZoneRules;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* PeriodGranularity buckets data based on any custom time period
@@ -216,6 +225,169 @@ public class PeriodGranularity extends Granularity
implements JsonSerializable
'}';
}
+ /**
+ * Returns true if this granularity can be mapped to the target granularity.
A granularity can be mapped when each
+ * interval of the source fits entirely within a single interval of the
target under the given time zone.
+ *
+ * <p>Examples:
+ * <ul>
+ * <li>{@code Period("PT1H")} in UTC can be mapped to {@code
Period("P1D")} in UTC,
+ * since every hourly interval is fully contained within some day
interval.</li>
+ * <li>{@code Period("PT1H")} in {@code America/Los_Angeles} can be mapped
to
+ * {@code Period("PT1H")} in UTC, since each hour in local time still
fits inside
+ * a corresponding hour in UTC (even though offsets can differ due to
daylight saving).</li>
+ * <li>{@code Period("P1D")} in {@code America/Los_Angeles} cannot be
mapped to
+ * {@code Period("P1D")} in UTC, since local day boundaries may cross
UTC days and
+ * are not fully contained within a single UTC day.</li>
+ * <li>{@code Period("PT1H")} in {@code Asia/Kolkata} cannot be mapped to
+ * {@code Period("PT1H")} in UTC, since the 30-minute offset causes
local hour
+ * intervals to straddle two UTC hour intervals.</li>
+ * </ul>
+ *
+ * @param target the target granularity to check against
+ * @return {@code true} if this granularity is fully contained within the
target granularity; {@code false} otherwise
+ */
+ public boolean canBeMappedTo(PeriodGranularity target)
+ {
+ if (hasOrigin || target.hasOrigin) {
+ return false;
+ }
+
+ if (getTimeZone().equals(target.getTimeZone())) {
+ int periodMonths = period.getYears() * 12 + period.getMonths();
+ int targetMonths = target.period.getYears() * 12 +
target.period.getMonths();
+ if (targetMonths == 0 && periodMonths != 0) {
+ // cannot map if target has no month, but period has month, e.x. P1M
cannot be mapped to P1D or P1W
+ return false;
+ }
+
+ Optional<Long> periodStandardSeconds =
getStandardSeconds(period.withYears(0).withMonths(0));
+ if (periodStandardSeconds.isEmpty()) {
+ // millisecond precision period is not supported
+ return false;
+ }
+ Optional<Long> targetStandardSeconds =
getStandardSeconds(target.period.withYears(0).withMonths(0));
+ if (targetStandardSeconds.isEmpty()) {
+ // millisecond precision period is not supported
+ return false;
+ }
+ if (targetMonths == 0 && periodMonths == 0) {
+ // both periods have zero months, we only need to check standard
seconds
+ // e.x. PT1H can be mapped to PT3H, PT15M can be mapped to PT1H
+ return targetStandardSeconds.get() % periodStandardSeconds.get() == 0;
+ }
+ // if we reach here, targetMonths != 0
+ if (periodMonths == 0) {
+ // can map if 1.target not have week/day/hour/minute/second, and
2.period can be mapped to day
+ // e.x PT3H can be mapped to P1M
+ return targetStandardSeconds.get() == 0 && (3600 * 24) %
periodStandardSeconds.get() == 0;
+ } else {
+ // can map if 1.target&period not have week/day/hour/minute/second,
and 2.period month can be mapped to target month
+ // e.x. P1M can be mapped to P3M, P1M can be mapped to P1Y
+ return targetMonths % periodMonths == 0
+ && targetStandardSeconds.get() == 0
+ && periodStandardSeconds.get() == 0;
+ }
+ }
+
+ // different time zones, we'd map to UTC first, then check if the target
can cover the UTC-mapped period
+ Optional<Long> standardSeconds = getStandardSeconds(period);
+ if (standardSeconds.isEmpty()) {
+ // must be in whole seconds, i.e. no years, months, or milliseconds.
+ return false;
+ }
+ Optional<Long> utcMappablePeriodSeconds = getUtcMappablePeriodSeconds();
+ if (utcMappablePeriodSeconds.isEmpty()) {
+ return false;
+ }
+ if (!standardSeconds.get().equals(utcMappablePeriodSeconds.get())) {
+ // the period cannot be mapped to UTC with the same period, e.x. PT1H in
Asia/Kolkata cannot be mapped to PT1H in UTC
+ return false;
+ }
+ if (target.period.getYears() == 0 && target.period.getMonths() == 0) {
+ Optional<Long> targetUtcMappablePeriodSeconds =
target.getUtcMappablePeriodSeconds();
+ if (targetUtcMappablePeriodSeconds.isEmpty()) {
+ return false;
+ }
+ // both periods have zero months, we only need to check standard seconds
+ // e.x. PT30M in Asia/Kolkata can be mapped to PT1H in
America/Los_Angeles
+ return targetUtcMappablePeriodSeconds.get() % standardSeconds.get() == 0;
+ } else {
+ // can map if 1.target not have week/day/hour/minute/second, and
2.period can be mapped to day
+ // e.x PT1H in America/Los_Angeles can be mapped to P1M in Asia/Shanghai
+ Optional<Long> targetStandardSecondsIgnoringMonth =
getStandardSeconds(target.period.withYears(0).withMonths(0));
+ return targetStandardSecondsIgnoringMonth.isPresent()
+ && targetStandardSecondsIgnoringMonth.get() == 0
+ && (3600 * 24) % standardSeconds.get() == 0;
+ }
+ }
+
+ /**
+ * Returns the maximum possible period seconds that this granularity can be
mapped to UTC.
+ * <p>
+ * Returns empty if the period cannot be mapped to whole seconds, i.e. it
has years or months, or milliseconds.
+ */
+ private Optional<Long> getUtcMappablePeriodSeconds()
+ {
+ Optional<Long> periodSeconds =
PeriodGranularity.getStandardSeconds(period);
+ if (periodSeconds.isEmpty()) {
+ return Optional.empty();
+ }
+
+ if (ISOChronology.getInstanceUTC().getZone().equals(getTimeZone())) {
+ return periodSeconds;
+ }
+ ZoneRules rules = ZoneId.of(getTimeZone().getID()).getRules();
+ Set<Integer> offsets = Stream.concat(
+ Stream.of(rules.getStandardOffset(Instant.now())),
+ rules.getTransitions()
+ .stream()
+ .filter(t -> t.getInstant().isAfter(Instant.EPOCH)) // timezone
transitions before epoch are patchy
+ .map(ZoneOffsetTransition::getOffsetBefore)
+ ).map(ZoneOffset::getTotalSeconds).collect(Collectors.toSet());
+
+ if (offsets.isEmpty()) {
+ // no offsets
+ return periodSeconds;
+ }
+
+ if (offsets.stream().allMatch(o -> o % periodSeconds.get() == 0)) {
+ // all offsets are multiples of the period, e.x. PT8H and PT2H in
Asia/Shanghai
+ return periodSeconds;
+ } else if (periodSeconds.get() % 3600 == 0 && offsets.stream().allMatch(o
-> o % 3600 == 0)) {
+ // fall back to hour if period is a multiple of hour and all offsets are
multiples of hour, e.x. PT1H in America/Los_Angeles
+ return Optional.of(3600L);
+ } else if (periodSeconds.get() % 1800 == 0 && offsets.stream().allMatch(o
-> o % 1800 == 0)) {
+ // fall back to 30 minutes if period is a multiple of 30 minutes and all
offsets are multiples of 30 minutes, e.x. PT30M in Asia/Kolkata
+ return Optional.of(1800L);
+ } else if (periodSeconds.get() % 60 == 0 && offsets.stream().allMatch(o ->
o % 60 == 0)) {
+ // fall back to minute if period is a multiple of minute and all offsets
are multiples of minute
+ return Optional.of(60L);
+ } else {
+ // default to second
+ return Optional.of(1L);
+ }
+ }
+
+ /**
+ * Returns the standard whole seconds for the given period.
+ * <p>
+ * Returns empty if the period cannot be mapped to whole seconds, i.e. one
of the following applies:
+ * <ul>
+ * <li>it has years or months
+ * <li>it has milliseconds
+ */
+ private static Optional<Long> getStandardSeconds(Period period)
+ {
+ if (period.getYears() == 0 && period.getMonths() == 0) {
+ long millis = period.toStandardDuration().getMillis();
+ return millis % 1000 == 0
+ ? Optional.of(millis / 1000)
+ : Optional.empty();
+ }
+ return Optional.empty();
+ }
+
private static boolean isCompoundPeriod(Period period)
{
int[] values = period.getValues();
diff --git
a/processing/src/main/java/org/apache/druid/query/expression/TimestampFloorExprMacro.java
b/processing/src/main/java/org/apache/druid/query/expression/TimestampFloorExprMacro.java
index e71412c6dda..3a17c7cd6df 100644
---
a/processing/src/main/java/org/apache/druid/query/expression/TimestampFloorExprMacro.java
+++
b/processing/src/main/java/org/apache/druid/query/expression/TimestampFloorExprMacro.java
@@ -19,6 +19,7 @@
package org.apache.druid.query.expression;
+import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.granularity.PeriodGranularity;
import org.apache.druid.math.expr.Expr;
import org.apache.druid.math.expr.ExprEval;
@@ -29,7 +30,6 @@ import
org.apache.druid.math.expr.vector.CastToTypeVectorProcessor;
import org.apache.druid.math.expr.vector.ExprVectorProcessor;
import
org.apache.druid.math.expr.vector.LongUnivariateLongFunctionVectorProcessor;
import org.apache.druid.segment.column.ColumnHolder;
-import org.joda.time.Period;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -38,9 +38,13 @@ import java.util.Objects;
public class TimestampFloorExprMacro implements ExprMacroTable.ExprMacro
{
- public static String forQueryGranularity(Period period)
+ public static String forQueryGranularity(PeriodGranularity period)
{
- return FN_NAME + "(" + ColumnHolder.TIME_COLUMN_NAME + ",'" + period +
"')";
+ return FN_NAME + "(" + ColumnHolder.TIME_COLUMN_NAME
+ + "," + StringUtils.format("'%s'", period.getPeriod())
+ + "," + (period.getOrigin() == null ? "null" :
StringUtils.format("'%s'", period.getOrigin()))
+ + "," + StringUtils.format("'%s'", period.getTimeZone())
+ + ")";
}
private static final String FN_NAME = "timestamp_floor";
diff --git
a/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
b/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
index 7370cff6355..c7f62759a6c 100644
---
a/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
+++
b/processing/src/main/java/org/apache/druid/segment/projections/Projections.java
@@ -23,8 +23,10 @@ import
org.apache.druid.data.input.impl.AggregateProjectionSpec;
import org.apache.druid.error.InvalidInput;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.java.util.common.granularity.Granularity;
+import org.apache.druid.java.util.common.granularity.PeriodGranularity;
import org.apache.druid.query.QueryContexts;
import org.apache.druid.query.aggregation.AggregatorFactory;
+import org.apache.druid.query.cache.CacheKeyBuilder;
import org.apache.druid.query.filter.Filter;
import org.apache.druid.segment.AggregateProjectionMetadata;
import org.apache.druid.segment.CursorBuildSpec;
@@ -42,10 +44,13 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
public class Projections
{
+ private static final ConcurrentHashMap<byte[], Boolean> PERIOD_GRAN_CACHE =
new ConcurrentHashMap<>();
+
@Nullable
public static <T> QueryableProjection<T> findMatchingProjection(
CursorBuildSpec cursorBuildSpec,
@@ -65,7 +70,12 @@ public class Projections
if (name != null && !name.equals(spec.getSchema().getName())) {
continue;
}
- final ProjectionMatch match =
matchAggregateProjection(spec.getSchema(), cursorBuildSpec, dataInterval,
physicalChecker);
+ final ProjectionMatch match = matchAggregateProjection(
+ spec.getSchema(),
+ cursorBuildSpec,
+ dataInterval,
+ physicalChecker
+ );
if (match != null) {
if (cursorBuildSpec.getQueryMetrics() != null) {
cursorBuildSpec.getQueryMetrics().projection(spec.getSchema().getName());
@@ -383,17 +393,32 @@ public class Projections
// virtual column and underlying expression itself, but this will do for
now
final Granularity virtualGranularity =
Granularities.fromVirtualColumn(queryVirtualColumn);
if (virtualGranularity != null) {
- if
(virtualGranularity.isFinerThan(projection.getEffectiveGranularity())) {
- return null;
- }
// same granularity, replace virtual column directly by remapping it
to the physical column
if (projection.getEffectiveGranularity().equals(virtualGranularity)) {
return matchBuilder.remapColumn(queryVirtualColumn.getOutputName(),
ColumnHolder.TIME_COLUMN_NAME)
.addReferencedPhysicalColumn(ColumnHolder.TIME_COLUMN_NAME);
+ } else if (Granularities.ALL.equals(virtualGranularity)
+ ||
Granularities.NONE.equals(projection.getEffectiveGranularity())) {
+ // if virtual gran is ALL or projection gran is NONE, it's
guaranteed that projection gran can be mapped to virtual gran
+ return
matchBuilder.addReferencedPhysicalColumn(ColumnHolder.TIME_COLUMN_NAME);
+ } else if (virtualGranularity instanceof PeriodGranularity
+ && projection.getEffectiveGranularity() instanceof
PeriodGranularity) {
+ PeriodGranularity virtualGran = (PeriodGranularity)
virtualGranularity;
+ PeriodGranularity projectionGran = (PeriodGranularity)
projection.getEffectiveGranularity();
+ byte[] combinedKey = new CacheKeyBuilder((byte)
0x0).appendCacheable(projectionGran)
+
.appendCacheable(virtualGran)
+ .build();
+ if (PERIOD_GRAN_CACHE.computeIfAbsent(combinedKey, (unused) ->
projectionGran.canBeMappedTo(virtualGran))) {
+ return
matchBuilder.addReferencedPhysicalColumn(ColumnHolder.TIME_COLUMN_NAME);
+ }
}
- return
matchBuilder.addReferencedPhysicalColumn(ColumnHolder.TIME_COLUMN_NAME);
+ // if it reaches here, can be one of the following cases:
+ // 1. virtual gran is NONE, and projection gran is not
+ // 2. projection gran is ALL, and virtual gran is not
+ // 3. both are period granularities, but projection gran can't be
mapped to virtual gran, e.x. PT2H can't be mapped to PT1H
+ return null;
} else {
- // anything else with __time requires none granularity
+ // we can't decide query granularity for the virtual column with
__time, requires none granularity to be safe
if (Granularities.NONE.equals(projection.getEffectiveGranularity())) {
return
matchBuilder.addReferencedPhysicalColumn(ColumnHolder.TIME_COLUMN_NAME);
}
diff --git
a/processing/src/test/java/org/apache/druid/data/input/impl/AggregateProjectionSpecTest.java
b/processing/src/test/java/org/apache/druid/data/input/impl/AggregateProjectionSpecTest.java
index 7c43e8cf4ad..24f7d2741a4 100644
---
a/processing/src/test/java/org/apache/druid/data/input/impl/AggregateProjectionSpecTest.java
+++
b/processing/src/test/java/org/apache/druid/data/input/impl/AggregateProjectionSpecTest.java
@@ -27,10 +27,12 @@ import
org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.query.aggregation.AggregatorFactory;
import org.apache.druid.query.aggregation.CountAggregatorFactory;
import org.apache.druid.query.aggregation.LongSumAggregatorFactory;
+import org.apache.druid.query.expression.TestExprMacroTable;
import org.apache.druid.query.filter.EqualityFilter;
import org.apache.druid.segment.TestHelper;
import org.apache.druid.segment.VirtualColumns;
import org.apache.druid.segment.column.ColumnType;
+import org.apache.druid.segment.virtual.ExpressionVirtualColumn;
import org.apache.druid.testing.InitializedNullHandlingTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -59,12 +61,15 @@ class AggregateProjectionSpecTest extends
InitializedNullHandlingTest
new FloatDimensionSchema("c"),
new DoubleDimensionSchema("d")
),
- new AggregatorFactory[] {
+ new AggregatorFactory[]{
new CountAggregatorFactory("count"),
new LongSumAggregatorFactory("e", "e")
}
);
- Assertions.assertEquals(spec,
JSON_MAPPER.readValue(JSON_MAPPER.writeValueAsString(spec),
AggregateProjectionSpec.class));
+ Assertions.assertEquals(
+ spec,
+ JSON_MAPPER.readValue(JSON_MAPPER.writeValueAsString(spec),
AggregateProjectionSpec.class)
+ );
}
@Test
@@ -75,7 +80,7 @@ class AggregateProjectionSpecTest extends
InitializedNullHandlingTest
null,
VirtualColumns.EMPTY,
List.of(),
- new AggregatorFactory[] {
+ new AggregatorFactory[]{
new CountAggregatorFactory("count"),
new LongSumAggregatorFactory("e", "e")
}
@@ -83,6 +88,72 @@ class AggregateProjectionSpecTest extends
InitializedNullHandlingTest
Assertions.assertTrue(spec.getOrdering().isEmpty());
}
+ @Test
+ void testComputeOrdering_granularity()
+ {
+ AggregateProjectionSpec spec = new AggregateProjectionSpec(
+ "some_projection",
+ null,
+ VirtualColumns.EMPTY,
+ List.of(new LongDimensionSchema("__time")),
+ new AggregatorFactory[]{}
+ );
+ Assertions.assertEquals("__time",
spec.toMetadataSchema().getTimeColumnName());
+
+ ExpressionVirtualColumn hourly = new ExpressionVirtualColumn(
+ "hourly",
+ "timestamp_floor(__time, 'PT1H', null, null)",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ ExpressionVirtualColumn daily = new ExpressionVirtualColumn(
+ "daily",
+ "timestamp_floor(__time, 'P1D', null, null)",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ ExpressionVirtualColumn ptEvery10Min = new ExpressionVirtualColumn(
+ "ptEvery10Min",
+ "timestamp_floor(__time, 'PT10M', null, 'America/Los_Angeles')",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ ExpressionVirtualColumn every90Min = new ExpressionVirtualColumn(
+ "every90Min",
+ "timestamp_floor(__time, 'PT1H30M', null, null)",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+
+ Assertions.assertEquals("hourly", new AggregateProjectionSpec(
+ "some_projection",
+ null,
+ VirtualColumns.create(daily, hourly, ptEvery10Min),
+ List.of(
+ new LongDimensionSchema("daily"),
+ new LongDimensionSchema("hourly"),
+ new LongDimensionSchema("ptEvery10Min")
+ ),
+ new AggregatorFactory[]{}
+ ).toMetadataSchema().getTimeColumnName());
+
+ Assertions.assertNull(new AggregateProjectionSpec(
+ "some_projection",
+ null,
+ VirtualColumns.create(ptEvery10Min),
+ List.of(new LongDimensionSchema("ptEvery10Min")),
+ new AggregatorFactory[]{}
+ ).toMetadataSchema().getTimeColumnName());
+
+ Assertions.assertEquals("every90Min", new AggregateProjectionSpec(
+ "some_projection",
+ null,
+ VirtualColumns.create(every90Min, ptEvery10Min),
+ List.of(new LongDimensionSchema("every90Min"), new
LongDimensionSchema("ptEvery10Min")),
+ new AggregatorFactory[]{}
+ ).toMetadataSchema().getTimeColumnName());
+ }
+
@Test
void testMissingName()
{
@@ -125,7 +196,10 @@ class AggregateProjectionSpecTest extends
InitializedNullHandlingTest
null
)
);
- Assertions.assertEquals("projection[other_projection] groupingColumns and
aggregators must not both be null or empty", t.getMessage());
+ Assertions.assertEquals(
+ "projection[other_projection] groupingColumns and aggregators must not
both be null or empty",
+ t.getMessage()
+ );
t = Assertions.assertThrows(
DruidException.class,
@@ -137,7 +211,10 @@ class AggregateProjectionSpecTest extends
InitializedNullHandlingTest
null
)
);
- Assertions.assertEquals("projection[other_projection] groupingColumns and
aggregators must not both be null or empty", t.getMessage());
+ Assertions.assertEquals(
+ "projection[other_projection] groupingColumns and aggregators must not
both be null or empty",
+ t.getMessage()
+ );
}
@Test
diff --git
a/processing/src/test/java/org/apache/druid/granularity/QueryGranularityTest.java
b/processing/src/test/java/org/apache/druid/granularity/QueryGranularityTest.java
index a885b2f6df1..5ffc39a2f9f 100644
---
a/processing/src/test/java/org/apache/druid/granularity/QueryGranularityTest.java
+++
b/processing/src/test/java/org/apache/druid/granularity/QueryGranularityTest.java
@@ -57,6 +57,7 @@ import java.util.List;
import java.util.TimeZone;
/**
+ *
*/
public class QueryGranularityTest extends InitializedNullHandlingTest
{
@@ -1040,11 +1041,14 @@ public class QueryGranularityTest extends
InitializedNullHandlingTest
);
ExpressionVirtualColumn column = Granularities.toVirtualColumn(hour,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
- Assert.assertEquals("timestamp_floor(__time,'PT1H')",
column.getExpression());
+ Assert.assertEquals("timestamp_floor(__time,'PT1H',null,'UTC')",
column.getExpression());
column = Granularities.toVirtualColumn(hourWithOrigin,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
- Assert.assertEquals(ColumnHolder.TIME_COLUMN_NAME, column.getExpression());
+ Assert.assertEquals(
+
"timestamp_floor(__time,'PT1H','2012-01-02T13:00:00.000Z','America/Los_Angeles')",
+ column.getExpression()
+ );
column = Granularities.toVirtualColumn(hourWithTz,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
- Assert.assertEquals(ColumnHolder.TIME_COLUMN_NAME, column.getExpression());
+
Assert.assertEquals("timestamp_floor(__time,'PT1H',null,'America/Los_Angeles')",
column.getExpression());
column = Granularities.toVirtualColumn(duration,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
Assert.assertEquals(ColumnHolder.TIME_COLUMN_NAME, column.getExpression());
column = Granularities.toVirtualColumn(Granularities.NONE,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
@@ -1052,11 +1056,11 @@ public class QueryGranularityTest extends
InitializedNullHandlingTest
column = Granularities.toVirtualColumn(Granularities.ALL,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
Assert.assertNull(column);
column = Granularities.toVirtualColumn(Granularities.HOUR,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
- Assert.assertEquals("timestamp_floor(__time,'PT1H')",
column.getExpression());
+ Assert.assertEquals("timestamp_floor(__time,'PT1H',null,'UTC')",
column.getExpression());
column = Granularities.toVirtualColumn(Granularities.MINUTE,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
- Assert.assertEquals("timestamp_floor(__time,'PT1M')",
column.getExpression());
+ Assert.assertEquals("timestamp_floor(__time,'PT1M',null,'UTC')",
column.getExpression());
column = Granularities.toVirtualColumn(Granularities.FIFTEEN_MINUTE,
Granularities.GRANULARITY_VIRTUAL_COLUMN_NAME);
- Assert.assertEquals("timestamp_floor(__time,'PT15M')",
column.getExpression());
+ Assert.assertEquals("timestamp_floor(__time,'PT15M',null,'UTC')",
column.getExpression());
}
@Test
@@ -1070,6 +1074,18 @@ public class QueryGranularityTest extends
InitializedNullHandlingTest
ColumnType.LONG,
TestExprMacroTable.INSTANCE
);
+ ExpressionVirtualColumn hourlyPacificTime = new ExpressionVirtualColumn(
+ "v0",
+ "timestamp_floor(__gran, 'PT1H', null, 'America/Los_Angeles')",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ ExpressionVirtualColumn hourlyIndianTime = new ExpressionVirtualColumn(
+ "v0",
+ "timestamp_floor(__gran, 'PT1H', null, 'Asia/Kolkata')",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
ExpressionVirtualColumn ceilHour = new ExpressionVirtualColumn(
"v0",
"timestamp_ceil(__time, 'PT1M')",
@@ -1078,7 +1094,7 @@ public class QueryGranularityTest extends
InitializedNullHandlingTest
);
ExpressionVirtualColumn floorWithExpression = new ExpressionVirtualColumn(
"v0",
- "timestamp_floor(timestamp_parse(timestamp,null,'UTC'), 'PT1M')",
+ "timestamp_floor(timestamp_parse(__time,null,'UTC'), 'PT1M')",
ColumnType.LONG,
TestExprMacroTable.INSTANCE
);
@@ -1097,8 +1113,16 @@ public class QueryGranularityTest extends
InitializedNullHandlingTest
Assert.assertEquals(Granularities.HOUR,
Granularities.fromVirtualColumn(hourly));
Assert.assertEquals(Granularities.DAY,
Granularities.fromVirtualColumn(day));
Assert.assertEquals(Granularities.HOUR,
Granularities.fromVirtualColumn(hourlyNonstandardTime));
+ Assert.assertEquals(
+ new PeriodGranularity(new Period("PT1H"), null,
DateTimes.inferTzFromString("America/Los_Angeles")),
+ Granularities.fromVirtualColumn(hourlyPacificTime)
+ );
+ Assert.assertEquals(
+ new PeriodGranularity(new Period("PT1H"), null,
DateTimes.inferTzFromString("Asia/Kolkata")),
+ Granularities.fromVirtualColumn(hourlyIndianTime)
+ );
Assert.assertNull(Granularities.fromVirtualColumn(ceilHour));
- Assert.assertNull(Granularities.fromVirtualColumn(floorWithExpression));
+ Assert.assertEquals(Granularities.MINUTE,
Granularities.fromVirtualColumn(floorWithExpression));
final DateTime origin = DateTimes.of("2012-01-02T05:00:00.000-08:00");
final DateTimeZone tz = DateTimes.inferTzFromString("America/Los_Angeles");
final Granularity minuteWithTz = new PeriodGranularity(new Period("PT1M"),
null, tz);
@@ -1107,6 +1131,18 @@ public class QueryGranularityTest extends
InitializedNullHandlingTest
Assert.assertEquals(minuteWithOrigin,
Granularities.fromVirtualColumn(floorWithOriginTimezone));
}
+ @Test
+ public void testFromVirtualColumnExtra()
+ {
+ ExpressionVirtualColumn literalField = new ExpressionVirtualColumn(
+ "v0",
+ "a",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ Assert.assertEquals(Granularities.ALL,
Granularities.fromVirtualColumn(literalField));
+ }
+
private void assertBucketStart(final Granularity granularity, final DateTime
in, final DateTime expectedInProperTz)
{
Assert.assertEquals(
diff --git
a/processing/src/test/java/org/apache/druid/java/util/common/PeriodGranularityTest.java
b/processing/src/test/java/org/apache/druid/java/util/common/PeriodGranularityTest.java
new file mode 100644
index 00000000000..e765dec78ca
--- /dev/null
+++
b/processing/src/test/java/org/apache/druid/java/util/common/PeriodGranularityTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.java.util.common;
+
+import org.apache.druid.java.util.common.granularity.PeriodGranularity;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Period;
+import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+
+public class PeriodGranularityTest
+{
+ PeriodGranularity UTC_PT1H = new PeriodGranularity(new Period("PT1H"), null,
DateTimeZone.UTC);
+ PeriodGranularity UTC_PT30M = new PeriodGranularity(new Period("PT30M"),
null, DateTimeZone.UTC);
+
+ DateTimeZone PACIFIC_TZ = DateTimes.inferTzFromString("America/Los_Angeles");
+ DateTimeZone INDIAN_TZ = DateTimes.inferTzFromString("Asia/Kolkata");
+
+ @Test
+ public void testCanBeMappedTo_sameTimeZone()
+ {
+ Assertions.assertTrue(UTC_PT30M.canBeMappedTo(UTC_PT1H));
+
+ PeriodGranularity pacificPT2H = new PeriodGranularity(new Period("PT2H"),
null, PACIFIC_TZ);
+ Assertions.assertFalse(pacificPT2H.canBeMappedTo(new PeriodGranularity(new
Period("PT20M"), null, PACIFIC_TZ)));
+ Assertions.assertTrue(pacificPT2H.canBeMappedTo(new PeriodGranularity(new
Period("PT6H"), null, PACIFIC_TZ)));
+
+ PeriodGranularity pacificP1D = new PeriodGranularity(new Period("P1D"),
null, PACIFIC_TZ);
+ Assertions.assertFalse(pacificP1D.canBeMappedTo(new PeriodGranularity(new
Period("PT1H"), null, PACIFIC_TZ)));
+ Assertions.assertTrue(pacificP1D.canBeMappedTo(new PeriodGranularity(new
Period("P1M"), null, PACIFIC_TZ)));
+
+ PeriodGranularity pacificP2D = new PeriodGranularity(new Period("P2D"),
null, PACIFIC_TZ);
+ Assertions.assertFalse(pacificP2D.canBeMappedTo(new PeriodGranularity(new
Period("P1W"), null, PACIFIC_TZ)));
+ Assertions.assertTrue(pacificP2D.canBeMappedTo(new PeriodGranularity(new
Period("P2W"), null, PACIFIC_TZ)));
+
+ // some extra tests for different month/week/day combo
+ PeriodGranularity pacificPT1W2D = new PeriodGranularity(new
Period("P1W").withDays(2), null, PACIFIC_TZ);
+ Assertions.assertFalse(pacificPT1W2D.canBeMappedTo(new
PeriodGranularity(new Period("P2M"), null, PACIFIC_TZ)));
+ Assertions.assertFalse(pacificPT1W2D.canBeMappedTo(new
PeriodGranularity(new Period("P2Y"), null, PACIFIC_TZ)));
+ Assertions.assertTrue(pacificPT1W2D.canBeMappedTo(pacificPT1W2D));
+ Assertions.assertTrue(pacificPT1W2D.canBeMappedTo(new PeriodGranularity(
+ new Period("P2W").withDays(4),
+ null,
+ PACIFIC_TZ
+ )));
+
+ Assertions.assertTrue(pacificP1D.canBeMappedTo(pacificPT1W2D));
+ Assertions.assertTrue(pacificPT2H.canBeMappedTo(new PeriodGranularity(
+ new Period("P1D").withHours(2),
+ null,
+ PACIFIC_TZ
+ )));
+ }
+
+ @Test
+ public void testCanBeMappedTo_sameHourlyAlignWithUtc()
+ {
+ Assertions.assertTrue(UTC_PT1H.canBeMappedTo(new PeriodGranularity(new
Period("PT1H"), null, PACIFIC_TZ)));
+ Assertions.assertTrue(UTC_PT1H.canBeMappedTo(new PeriodGranularity(new
Period("PT3H"), null, PACIFIC_TZ)));
+ }
+
+ @Test
+ public void testCanBeMappedTo_same30MinutesAlignWithUtc()
+ {
+ Assertions.assertFalse(UTC_PT1H.canBeMappedTo(new PeriodGranularity(new
Period("PT2H"), null, INDIAN_TZ)));
+ Assertions.assertFalse(new PeriodGranularity(
+ new Period("PT1H"),
+ null,
+ PACIFIC_TZ
+ ).canBeMappedTo(new PeriodGranularity(new Period("PT1H"), null,
INDIAN_TZ)));
+
+ Assertions.assertTrue(new PeriodGranularity(
+ new Period("PT30M"),
+ null,
+ PACIFIC_TZ
+ ).canBeMappedTo(new PeriodGranularity(new Period("PT1H"), null,
INDIAN_TZ)));
+ Assertions.assertTrue(UTC_PT30M.canBeMappedTo(new PeriodGranularity(new
Period("PT30M"), null, PACIFIC_TZ)));
+ }
+
+ @Test
+ public void testCanBeMappedTo_withOrigin()
+ {
+ Assertions.assertFalse(new PeriodGranularity(
+ new Period("PT1H"),
+ DateTimes.nowUtc(),
+ DateTimeZone.UTC
+ ).canBeMappedTo(new PeriodGranularity(new Period("PT2H"), null,
DateTimeZone.UTC)));
+
+ Assertions.assertFalse(UTC_PT1H.canBeMappedTo(new PeriodGranularity(
+ new Period("PT1H"),
+ DateTimes.nowUtc(),
+ DateTimeZone.UTC
+ )));
+ }
+
+ @Test
+ public void testCanBeMappedTo_differentTimeZone()
+ {
+ PeriodGranularity pacificPT1H = new PeriodGranularity(new Period("PT1H"),
null, PACIFIC_TZ);
+ Assertions.assertTrue(pacificPT1H.canBeMappedTo(UTC_PT1H));
+ Assertions.assertTrue(pacificPT1H.canBeMappedTo(new PeriodGranularity(
+ new Period("PT3H"),
+ null,
+ DateTimes.inferTzFromString("America/New_York")
+ )));
+ Assertions.assertTrue(pacificPT1H.canBeMappedTo(new PeriodGranularity(
+ new Period("P1M"),
+ null,
+ DateTimeZone.UTC
+ )));
+
+ Assertions.assertFalse(pacificPT1H.canBeMappedTo(new PeriodGranularity(
+ new Period("PT1H"),
+ null,
+ INDIAN_TZ
+ )));
+ Assertions.assertFalse(pacificPT1H.canBeMappedTo(new PeriodGranularity(
+ new Period("P1D"),
+ null,
+ INDIAN_TZ
+ )));
+ Assertions.assertFalse(new PeriodGranularity(
+ new Period("P1D"),
+ null,
+ PACIFIC_TZ
+ ).canBeMappedTo(new PeriodGranularity(new Period("P1D"), null,
DateTimeZone.UTC)));
+ }
+}
diff --git
a/processing/src/test/java/org/apache/druid/segment/CursorFactoryProjectionTest.java
b/processing/src/test/java/org/apache/druid/segment/CursorFactoryProjectionTest.java
index 21b694c56b8..0eb1c70a872 100644
---
a/processing/src/test/java/org/apache/druid/segment/CursorFactoryProjectionTest.java
+++
b/processing/src/test/java/org/apache/druid/segment/CursorFactoryProjectionTest.java
@@ -41,8 +41,10 @@ import org.apache.druid.java.util.common.FileUtils;
import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.Pair;
import org.apache.druid.java.util.common.granularity.Granularities;
+import org.apache.druid.java.util.common.granularity.PeriodGranularity;
import org.apache.druid.java.util.common.guava.Sequence;
import org.apache.druid.java.util.common.io.Closer;
+import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.query.DefaultQueryMetrics;
import org.apache.druid.query.DruidProcessingConfig;
import org.apache.druid.query.Druids;
@@ -53,10 +55,12 @@ import org.apache.druid.query.aggregation.AggregatorFactory;
import org.apache.druid.query.aggregation.CountAggregatorFactory;
import org.apache.druid.query.aggregation.DoubleSumAggregatorFactory;
import org.apache.druid.query.aggregation.FloatSumAggregatorFactory;
+import org.apache.druid.query.aggregation.LongMaxAggregatorFactory;
import org.apache.druid.query.aggregation.LongSumAggregatorFactory;
import
org.apache.druid.query.aggregation.firstlast.last.LongLastAggregatorFactory;
import org.apache.druid.query.dimension.DefaultDimensionSpec;
import org.apache.druid.query.expression.TestExprMacroTable;
+import org.apache.druid.query.expression.TimestampFloorExprMacro;
import org.apache.druid.query.filter.EqualityFilter;
import org.apache.druid.query.groupby.GroupByQuery;
import org.apache.druid.query.groupby.GroupByQueryConfig;
@@ -67,6 +71,7 @@ import org.apache.druid.query.groupby.GroupingEngine;
import org.apache.druid.query.groupby.ResultRow;
import org.apache.druid.query.groupby.orderby.DefaultLimitSpec;
import org.apache.druid.query.groupby.orderby.OrderByColumnSpec;
+import org.apache.druid.query.groupby.orderby.OrderByColumnSpec.Direction;
import org.apache.druid.query.ordering.StringComparators;
import org.apache.druid.query.timeseries.TimeseriesQuery;
import org.apache.druid.query.timeseries.TimeseriesQueryEngine;
@@ -83,6 +88,7 @@ import
org.apache.druid.segment.virtual.NestedFieldVirtualColumn;
import org.apache.druid.testing.InitializedNullHandlingTest;
import org.joda.time.DateTime;
import org.joda.time.Interval;
+import org.joda.time.Period;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Assume;
@@ -109,7 +115,10 @@ import java.util.stream.Collectors;
public class CursorFactoryProjectionTest extends InitializedNullHandlingTest
{
private static final Closer CLOSER = Closer.create();
- static final DateTime TIMESTAMP =
Granularities.DAY.bucket(DateTimes.nowUtc()).getStart();
+ // Set a fixed time, when IST is 5 hours 30 minutes ahead of UTC, and PDT is
7 hours behind UTC.
+ static final DateTime UTC_MIDNIGHT =
Granularities.DAY.bucket(DateTimes.of("2025-08-13")).getStart();
+ static final DateTime UTC_01H = UTC_MIDNIGHT.plusHours(1);
+ static final DateTime UTC_01H31M = UTC_MIDNIGHT.plusHours(1).plusMinutes(31);
static final RowSignature ROW_SIGNATURE = RowSignature.builder()
.add("a",
ColumnType.STRING)
@@ -125,49 +134,49 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
return Arrays.asList(
new ListBasedInputRow(
ROW_SIGNATURE,
- TIMESTAMP,
+ UTC_MIDNIGHT,
dimensions,
Arrays.asList("a", "aa", 1L, 1.0, null, Map.of("x", "a", "y", 1L,
"z", 1.0))
),
new ListBasedInputRow(
ROW_SIGNATURE,
- TIMESTAMP.plusMinutes(2),
+ UTC_MIDNIGHT.plusMinutes(2),
dimensions,
Arrays.asList("a", "bb", 1L, 1.1, 1.1f, Map.of("x", "a", "y", 1L,
"z", 1.1))
),
new ListBasedInputRow(
ROW_SIGNATURE,
- TIMESTAMP.plusMinutes(4),
+ UTC_MIDNIGHT.plusMinutes(4),
dimensions,
Arrays.asList("a", "cc", 2L, 2.2, 2.2f, Map.of("x", "a", "y", 2L,
"z", 2.2))
),
new ListBasedInputRow(
ROW_SIGNATURE,
- TIMESTAMP.plusMinutes(6),
+ UTC_MIDNIGHT.plusMinutes(6),
dimensions,
Arrays.asList("b", "aa", 3L, 3.3, 3.3f, Map.of("x", "b", "y", 3L,
"z", 3.3))
),
new ListBasedInputRow(
ROW_SIGNATURE,
- TIMESTAMP.plusMinutes(8),
+ UTC_MIDNIGHT.plusMinutes(8),
dimensions,
Arrays.asList("b", "aa", 4L, 4.4, 4.4f, Map.of("x", "b", "y", 4L,
"z", 4.4))
),
new ListBasedInputRow(
ROW_SIGNATURE,
- TIMESTAMP.plusMinutes(10),
+ UTC_MIDNIGHT.plusMinutes(10),
dimensions,
Arrays.asList("b", "bb", 5L, 5.5, 5.5f, Map.of("x", "b", "y", 5L,
"z", 5.5))
),
new ListBasedInputRow(
ROW_SIGNATURE,
- TIMESTAMP.plusHours(1),
+ UTC_01H,
dimensions,
Arrays.asList("a", "aa", 1L, 1.1, 1.1f, Map.of("x", "a", "y", 1L,
"z", 1.1))
),
new ListBasedInputRow(
ROW_SIGNATURE,
- TIMESTAMP.plusHours(1).plusMinutes(1),
+ UTC_01H31M,
dimensions,
Arrays.asList("a", "dd", 2L, 2.2, 2.2f, Map.of("x", "a", "y", 2L,
"z", 2.2))
)
@@ -349,7 +358,10 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
)
)
.groupingColumns(new
StringDimensionSchema("afoo"))
- .aggregators(new
LongSumAggregatorFactory("sum_c", "sum_c"))
+ .aggregators(
+ new LongSumAggregatorFactory("sum_c",
"sum_c"),
+ new LongMaxAggregatorFactory("max_c", "max_c")
+ )
.build()
);
@@ -372,17 +384,13 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
ROLLUP_PROJECTIONS.stream()
.map(
projection ->
- AggregateProjectionSpec.builder(projection)
- .groupingColumns(
-
projection.getGroupingColumns()
- .stream()
- .map(x ->
new AutoTypeColumnSchema(
-
x.getName(),
- null
- ))
-
.collect(Collectors.toList())
- )
- .build()
+ AggregateProjectionSpec
+ .builder(projection)
+
.groupingColumns(projection.getGroupingColumns()
+ .stream()
+ .map(x -> new
AutoTypeColumnSchema(x.getName(), null))
+
.collect(Collectors.toList()))
+ .build()
)
.collect(Collectors.toList());
@@ -413,6 +421,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
)
);
final AggregatorFactory[] rollupAggs = new AggregatorFactory[]{
+ new LongMaxAggregatorFactory("max_c", "c"),
new LongSumAggregatorFactory("sum_c", "c"),
new DoubleSumAggregatorFactory("sum_d", "d"),
new FloatSumAggregatorFactory("sum_e", "e")
@@ -460,7 +469,11 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
}
}
if (incremental) {
- IncrementalIndex index = CLOSER.register(makeBuilder(dims,
autoSchema, writeNullColumns).buildIncrementalIndex());
+ IncrementalIndex index = CLOSER.register(makeBuilder(
+ dims,
+ autoSchema,
+ writeNullColumns
+ ).buildIncrementalIndex());
IncrementalIndex rollupIndex = CLOSER.register(
makeRollupBuilder(rollupDims, rollupAggs,
autoSchema).buildIncrementalIndex()
);
@@ -474,7 +487,11 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
autoSchema
});
} else {
- QueryableIndex index = CLOSER.register(makeBuilder(dims,
autoSchema, writeNullColumns).buildMMappedIndex());
+ QueryableIndex index = CLOSER.register(makeBuilder(
+ dims,
+ autoSchema,
+ writeNullColumns
+ ).buildMMappedIndex());
QueryableIndex rollupIndex = CLOSER.register(
makeRollupBuilder(rollupDims, rollupAggs,
autoSchema).buildMMappedIndex()
);
@@ -613,8 +630,8 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
.setLimitSpec(
new DefaultLimitSpec(
Arrays.asList(
- new OrderByColumnSpec("a",
OrderByColumnSpec.Direction.ASCENDING, StringComparators.LEXICOGRAPHIC),
- new OrderByColumnSpec("v0",
OrderByColumnSpec.Direction.ASCENDING, StringComparators.LEXICOGRAPHIC)
+ new OrderByColumnSpec("a",
Direction.ASCENDING, StringComparators.LEXICOGRAPHIC),
+ new OrderByColumnSpec("v0",
Direction.ASCENDING, StringComparators.LEXICOGRAPHIC)
),
10
)
@@ -721,8 +738,8 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
List.of(
- new Object[]{"a", 7L,
Pair.of(TIMESTAMP.plusHours(1).plusMinutes(1).getMillis(), 2L)},
- new Object[]{"b", 12L,
Pair.of(TIMESTAMP.plusMinutes(10).getMillis(), 5L)}
+ new Object[]{"a", 7L, Pair.of(UTC_01H31M.getMillis(), 2L)},
+ new Object[]{"b", 12L,
Pair.of(UTC_MIDNIGHT.plusMinutes(10).getMillis(), 5L)}
)
);
}
@@ -749,8 +766,8 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
List.of(
- new Object[]{"a", 7L,
Pair.of(TIMESTAMP.plusHours(1).plusMinutes(1).getMillis(), 2L)},
- new Object[]{"b", 12L,
Pair.of(TIMESTAMP.plusMinutes(10).getMillis(), 5L)}
+ new Object[]{"a", 7L, Pair.of(UTC_01H31M.getMillis(), 2L)},
+ new Object[]{"b", 12L,
Pair.of(UTC_MIDNIGHT.plusMinutes(10).getMillis(), 5L)}
)
);
}
@@ -788,7 +805,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
GroupByQuery.builder()
.setDataSource("test")
.setGranularity(Granularities.ALL)
- .setInterval(new Interval(TIMESTAMP,
TIMESTAMP.plusDays(1)))
+ .setInterval(new Interval(UTC_MIDNIGHT,
UTC_MIDNIGHT.plusDays(1)))
.addDimension("a")
.setDimFilter(new EqualityFilter("a", ColumnType.STRING,
"a", null))
.addAggregator(new LongSumAggregatorFactory("c_sum", "c"))
@@ -804,7 +821,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
Collections.singletonList(
- new Object[]{"a", 7L,
Pair.of(TIMESTAMP.plusHours(1).plusMinutes(1).getMillis(), 2L)}
+ new Object[]{"a", 7L, Pair.of(UTC_01H31M.getMillis(), 2L)}
)
);
}
@@ -816,7 +833,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
GroupByQuery.builder()
.setDataSource("test")
.setGranularity(Granularities.ALL)
- .setInterval(new Interval(TIMESTAMP,
TIMESTAMP.plusHours(1)))
+ .setInterval(new Interval(UTC_MIDNIGHT,
UTC_MIDNIGHT.plusHours(1)))
.addDimension("a")
.setDimFilter(new EqualityFilter("a", ColumnType.STRING,
"a", null))
.addAggregator(new LongSumAggregatorFactory("c_sum", "c"))
@@ -832,7 +849,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
Collections.singletonList(
- new Object[]{"a", 4L,
Pair.of(TIMESTAMP.plusMinutes(4).getMillis(), 2L)}
+ new Object[]{"a", 4L,
Pair.of(UTC_MIDNIGHT.plusMinutes(4).getMillis(), 2L)}
)
);
}
@@ -844,7 +861,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
GroupByQuery.builder()
.setDataSource("test")
.setGranularity(Granularities.ALL)
- .setInterval(new Interval(TIMESTAMP,
TIMESTAMP.plusHours(1).minusMinutes(1)))
+ .setInterval(new Interval(UTC_MIDNIGHT,
UTC_MIDNIGHT.plusHours(1).minusMinutes(1)))
.addDimension("a")
.setDimFilter(new EqualityFilter("a", ColumnType.STRING,
"a", null))
.addAggregator(new LongSumAggregatorFactory("c_sum", "c"))
@@ -860,7 +877,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
Collections.singletonList(
- new Object[]{"a", 4L,
Pair.of(TIMESTAMP.plusMinutes(4).getMillis(), 2L)}
+ new Object[]{"a", 4L,
Pair.of(UTC_MIDNIGHT.plusMinutes(4).getMillis(), 2L)}
)
);
}
@@ -989,9 +1006,9 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
makeArrayResultSet(
- new Object[]{TIMESTAMP.getMillis(), "a", 4L},
- new Object[]{TIMESTAMP.getMillis(), "b", 12L},
- new Object[]{TIMESTAMP.plusHours(1).getMillis(), "a", 3L}
+ new Object[]{UTC_MIDNIGHT.getMillis(), "a", 4L},
+ new Object[]{UTC_MIDNIGHT.getMillis(), "b", 12L},
+ new Object[]{UTC_01H.getMillis(), "a", 3L}
)
);
}
@@ -1026,15 +1043,106 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
makeArrayResultSet(
- new Object[]{TIMESTAMP.getMillis(), "aa", 8L},
- new Object[]{TIMESTAMP.getMillis(), "bb", 6L},
- new Object[]{TIMESTAMP.getMillis(), "cc", 2L},
- new Object[]{TIMESTAMP.plusHours(1).getMillis(), "aa", 1L},
- new Object[]{TIMESTAMP.plusHours(1).getMillis(), "dd", 2L}
+ new Object[]{UTC_MIDNIGHT.getMillis(), "aa", 8L},
+ new Object[]{UTC_MIDNIGHT.getMillis(), "bb", 6L},
+ new Object[]{UTC_MIDNIGHT.getMillis(), "cc", 2L},
+ new Object[]{UTC_01H.getMillis(), "aa", 1L},
+ new Object[]{UTC_01H.getMillis(), "dd", 2L}
)
);
}
+ @Test
+ public void testQueryGranularityFitsProjectionGranularityWithTimeZone()
+ {
+ final GroupByQuery.Builder queryBuilder =
+ GroupByQuery.builder()
+ .setDataSource("test")
+ .setInterval(Intervals.ETERNITY)
+ .addAggregator(new LongSumAggregatorFactory("c_sum", "c"));
+ final ExpectedProjectionGroupBy queryMetrics = new
ExpectedProjectionGroupBy("a_hourly_c_sum_with_count_latest");
+
+ if (segmentSortedByTime) {
+ queryBuilder.addDimension("a")
+ .setGranularity(new PeriodGranularity(
+ new Period("PT1H"),
+ null,
+ DateTimes.inferTzFromString("America/Los_Angeles")
+ ));
+ } else {
+ queryBuilder.setGranularity(Granularities.ALL)
+ .setDimensions(
+ DefaultDimensionSpec.of("__gran", ColumnType.LONG),
+ DefaultDimensionSpec.of("a")
+ )
+ .setVirtualColumns(new ExpressionVirtualColumn(
+ "__gran",
+
"timestamp_floor(__time,'PT1H',null,'America/Los_Angeles')",
+ ColumnType.LONG,
+ new ExprMacroTable(List.of(new
TimestampFloorExprMacro()))
+ ));
+ }
+ final GroupByQuery query = queryBuilder.build();
+ final CursorBuildSpec buildSpec =
GroupingEngine.makeCursorBuildSpec(query, queryMetrics);
+
+ assertCursorProjection(buildSpec, queryMetrics, 3);
+
+ testGroupBy(
+ query,
+ queryMetrics,
+ makeArrayResultSet(
+ new Object[]{UTC_MIDNIGHT.getMillis(), "a", 4L},
+ new Object[]{UTC_MIDNIGHT.getMillis(), "b", 12L},
+ new Object[]{UTC_01H.getMillis(), "a", 3L}
+ )
+ );
+ }
+
+ @Test
+ public void testQueryGranularityDoesNotFitProjectionGranularityWithTimeZone()
+ {
+ final GroupByQuery.Builder queryBuilder =
+ GroupByQuery.builder()
+ .setDataSource("test")
+ .setInterval(Intervals.ETERNITY)
+ .addAggregator(new LongSumAggregatorFactory("c_sum", "c"));
+ final ExpectedProjectionGroupBy queryMetrics = new
ExpectedProjectionGroupBy(null);
+
+ if (segmentSortedByTime) {
+ queryBuilder.addDimension("a")
+ .setGranularity(new PeriodGranularity(
+ new Period("PT1H"),
+ null,
+ DateTimes.inferTzFromString("Asia/Kolkata")
+ ));
+ } else {
+ queryBuilder.setGranularity(Granularities.ALL)
+ .setDimensions(
+ DefaultDimensionSpec.of("__gran", ColumnType.LONG),
+ DefaultDimensionSpec.of("a")
+ )
+ .setVirtualColumns(new ExpressionVirtualColumn(
+ "__gran",
+ "timestamp_floor(__time,'PT1H',null,'Asia/Kolkata')",
+ ColumnType.LONG,
+ new ExprMacroTable(List.of(new
TimestampFloorExprMacro()))
+ ));
+ }
+ final GroupByQuery query = queryBuilder.build();
+ final CursorBuildSpec buildSpec =
GroupingEngine.makeCursorBuildSpec(query, queryMetrics);
+
+ assertCursorProjection(buildSpec, queryMetrics, 8);
+ testGroupBy(
+ query,
+ queryMetrics,
+ makeArrayResultSet(
+ new Object[]{UTC_MIDNIGHT.minusMinutes(30).getMillis(), "a", 4L},
+ new Object[]{UTC_MIDNIGHT.minusMinutes(30).getMillis(), "b", 12L},
+ new Object[]{UTC_01H.minusMinutes(30).getMillis(), "a", 1L},
+ new Object[]{UTC_01H31M.minusMinutes(1).getMillis(), "a", 2L}
+ )
+ );
+ }
@Test
public void testQueryGranularityLargerProjectionGranularity()
@@ -1066,8 +1174,8 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
makeArrayResultSet(
- new Object[]{TIMESTAMP.getMillis(), "a", 7L},
- new Object[]{TIMESTAMP.getMillis(), "b", 12L}
+ new Object[]{UTC_MIDNIGHT.getMillis(), "a", 7L},
+ new Object[]{UTC_MIDNIGHT.getMillis(), "b", 12L}
)
);
}
@@ -1179,7 +1287,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
Collections.singletonList(
- new Object[]{TIMESTAMP, 19L}
+ new Object[]{UTC_MIDNIGHT, 19L}
)
);
}
@@ -1207,7 +1315,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
Collections.singletonList(
- new Object[]{TIMESTAMP, 19L}
+ new Object[]{UTC_MIDNIGHT, 19L}
)
);
}
@@ -1228,7 +1336,8 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
final CursorBuildSpec buildSpec =
TimeseriesQueryEngine.makeCursorBuildSpec(query, null);
DruidException e = Assert.assertThrows(
DruidException.class,
- () -> projectionsCursorFactory.makeCursorHolder(buildSpec));
+ () -> projectionsCursorFactory.makeCursorHolder(buildSpec)
+ );
Assert.assertEquals(DruidException.Category.INVALID_INPUT,
e.getCategory());
Assert.assertEquals("Projection[b_c_sum] specified, but does not satisfy
query", e.getMessage());
}
@@ -1253,8 +1362,8 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
List.of(
- new Object[]{TIMESTAMP, 16L},
- new Object[]{TIMESTAMP.plusHours(1), 3L}
+ new Object[]{UTC_MIDNIGHT, 16L},
+ new Object[]{UTC_01H, 3L}
)
);
}
@@ -1280,7 +1389,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
Collections.singletonList(
- new Object[]{TIMESTAMP, 19L}
+ new Object[]{UTC_MIDNIGHT, 19L}
)
);
}
@@ -1306,7 +1415,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
Collections.singletonList(
- new Object[]{TIMESTAMP, 19L}
+ new Object[]{UTC_MIDNIGHT, 19L}
)
);
}
@@ -1334,14 +1443,14 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
List.of(
- new Object[]{TIMESTAMP, 1L},
- new Object[]{TIMESTAMP.plusMinutes(2), 1L},
- new Object[]{TIMESTAMP.plusMinutes(4), 2L},
- new Object[]{TIMESTAMP.plusMinutes(6), 3L},
- new Object[]{TIMESTAMP.plusMinutes(8), 4L},
- new Object[]{TIMESTAMP.plusMinutes(10), 5L},
- new Object[]{TIMESTAMP.plusHours(1), 1L},
- new Object[]{TIMESTAMP.plusHours(1).plusMinutes(1), 2L}
+ new Object[]{UTC_MIDNIGHT, 1L},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(2), 1L},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(4), 2L},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(6), 3L},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(8), 4L},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(10), 5L},
+ new Object[]{UTC_01H, 1L},
+ new Object[]{UTC_01H31M, 2L}
)
);
}
@@ -1379,18 +1488,24 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
@Test
public void testProjectionSingleDimVirtualColumnRollupTable()
{
+ final VirtualColumn vc = new ExpressionVirtualColumn(
+ "v0",
+ "concat(a, 'foo')",
+ ColumnType.STRING,
+ TestExprMacroTable.INSTANCE
+ );
final GroupByQuery query =
GroupByQuery.builder()
.setDataSource("test")
.setGranularity(Granularities.ALL)
.setInterval(Intervals.ETERNITY)
.addDimension("v0")
- .setVirtualColumns(new ExpressionVirtualColumn("v0",
"concat(a, 'foo')", ColumnType.STRING, TestExprMacroTable.INSTANCE))
+ .setVirtualColumns(vc)
.addAggregator(new LongSumAggregatorFactory("c_sum",
"sum_c"))
+ .addAggregator(new LongMaxAggregatorFactory("c_c",
"max_c"))
.build();
- final ExpectedProjectionGroupBy queryMetrics =
- new ExpectedProjectionGroupBy("afoo");
+ final ExpectedProjectionGroupBy queryMetrics = new
ExpectedProjectionGroupBy("afoo");
final CursorBuildSpec buildSpec =
GroupingEngine.makeCursorBuildSpec(query, queryMetrics);
assertCursorProjection(rollupProjectionsCursorFactory, buildSpec,
queryMetrics, 2);
@@ -1401,8 +1516,8 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
makeArrayResultSet(
- new Object[]{"afoo", 7L},
- new Object[]{"bfoo", 12L}
+ new Object[]{"afoo", 7L, 2L},
+ new Object[]{"bfoo", 12L, 5L}
)
);
}
@@ -1629,7 +1744,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
// realltime results are inconsistent between projection and base table
since projection is totally empty, but base
// table is reduced with filter
final boolean isRealtime = projectionsCursorFactory instanceof
IncrementalIndexCursorFactory;
- final List<Object[]> expectedResults = Collections.singletonList(new
Object[]{TIMESTAMP, null});
+ final List<Object[]> expectedResults = Collections.singletonList(new
Object[]{UTC_MIDNIGHT, null});
final List<Object[]> expectedRealtimeResults = List.of();
final Sequence<Result<TimeseriesResultValue>> resultRows =
timeseriesEngine.process(
@@ -1649,15 +1764,12 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
);
- Assertions.assertEquals(TIMESTAMP,
projectionsTimeBoundaryInspector.getMinTime());
+ Assertions.assertEquals(UTC_MIDNIGHT,
projectionsTimeBoundaryInspector.getMinTime());
if (isRealtime || segmentSortedByTime) {
- Assertions.assertEquals(TIMESTAMP.plusHours(1).plusMinutes(1),
projectionsTimeBoundaryInspector.getMaxTime());
+ Assertions.assertEquals(UTC_01H31M,
projectionsTimeBoundaryInspector.getMaxTime());
} else {
// min and max time are inexact for non time ordered segments
- Assertions.assertEquals(
- TIMESTAMP.plusHours(1).plusMinutes(1).plusMillis(1),
- projectionsTimeBoundaryInspector.getMaxTime()
- );
+ Assertions.assertEquals(UTC_01H31M.plusMillis(1),
projectionsTimeBoundaryInspector.getMaxTime());
}
// timeseries query only works on base table if base table is sorted by
time
@@ -1707,14 +1819,14 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
query,
queryMetrics,
makeArrayResultSet(
- new Object[]{TIMESTAMP.getMillis(), "aaaa"},
- new Object[]{TIMESTAMP.plusMinutes(2).getMillis(), "aaaa"},
- new Object[]{TIMESTAMP.plusMinutes(4).getMillis(), "aaaa"},
- new Object[]{TIMESTAMP.plusMinutes(6).getMillis(), "baaa"},
- new Object[]{TIMESTAMP.plusMinutes(8).getMillis(), "baaa"},
- new Object[]{TIMESTAMP.plusMinutes(10).getMillis(), "baaa"},
- new Object[]{TIMESTAMP.plusHours(1).getMillis(), "aaaa"},
- new Object[]{TIMESTAMP.plusHours(1).plusMinutes(1).getMillis(),
"aaaa"}
+ new Object[]{UTC_MIDNIGHT.getMillis(), "aaaa"},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(2).getMillis(), "aaaa"},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(4).getMillis(), "aaaa"},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(6).getMillis(), "baaa"},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(8).getMillis(), "baaa"},
+ new Object[]{UTC_MIDNIGHT.plusMinutes(10).getMillis(), "baaa"},
+ new Object[]{UTC_01H.getMillis(), "aaaa"},
+ new Object[]{UTC_01H31M.getMillis(), "aaaa"}
)
);
}
@@ -1780,8 +1892,13 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
queryMetrics.assertProjection();
- final List<ResultRow> results = resultRows.toList();
- assertGroupByResultsAnyOrder(expectedResults, results);
+ final Object[] results =
resultRows.toList().stream().map(ResultRow::getArray).map(Arrays::toString).toArray();
+ Arrays.sort(results);
+
+ final Object[] expectedResultsArray =
expectedResults.stream().map(Arrays::toString).toArray();
+ Arrays.sort(expectedResultsArray);
+ // print a full diff of all differing elements.
+ Assertions.assertEquals(Arrays.toString(expectedResultsArray),
Arrays.toString(results));
final Sequence<ResultRow> resultRowsNoProjection = groupingEngine.process(
query.withOverriddenContext(Map.of(QueryContexts.NO_PROJECTIONS,
true)),
@@ -1791,8 +1908,11 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
null
);
- final List<ResultRow> resultsNoProjection =
resultRowsNoProjection.toList();
- assertGroupByResultsAnyOrder(expectedResults, resultsNoProjection);
+ final Object[] resultsNoProjection =
resultRowsNoProjection.toList().stream().map(ResultRow::getArray).map(Arrays::toString).toArray();
+ Arrays.sort(resultsNoProjection);
+ // print a full diff of all differing elements.
+ Assertions.assertEquals(Arrays.toString(expectedResultsArray),
Arrays.toString(resultsNoProjection));
+
}
private void testTimeseries(
@@ -1849,14 +1969,6 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
}
}
- private void assertGroupByResultsAnyOrder(Set<Object[]> expected,
List<ResultRow> actual)
- {
- Assertions.assertEquals(expected.size(), actual.size());
- for (ResultRow row : actual) {
- Assertions.assertTrue(expected.contains(row.getArray()), "missing row:"
+ Arrays.deepToString(row.getArray()));
- }
- }
-
private void assertTimeseriesResults(
RowSignature querySignature,
List<Object[]> expected,
@@ -1914,7 +2026,7 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
IncrementalIndexSchema.builder()
.withDimensionsSpec(dimensionsSpec)
.withRollup(false)
-
.withMinTimestamp(TIMESTAMP.getMillis())
+
.withMinTimestamp(UTC_MIDNIGHT.getMillis())
.withProjections(autoSchema ?
AUTO_PROJECTIONS : PROJECTIONS)
.build()
)
@@ -1922,20 +2034,25 @@ public class CursorFactoryProjectionTest extends
InitializedNullHandlingTest
.rows(ROWS);
}
- private static IndexBuilder makeRollupBuilder(DimensionsSpec dimensionsSpec,
AggregatorFactory[] aggs, boolean autoSchema)
+ private static IndexBuilder makeRollupBuilder(
+ DimensionsSpec dimensionsSpec,
+ AggregatorFactory[] aggs,
+ boolean autoSchema
+ )
{
File tmp = FileUtils.createTempDir();
CLOSER.register(tmp::delete);
return IndexBuilder.create()
.tmpDir(tmp)
.schema(
- IncrementalIndexSchema.builder()
-
.withDimensionsSpec(dimensionsSpec)
- .withMetrics(aggs)
- .withRollup(true)
-
.withMinTimestamp(TIMESTAMP.getMillis())
- .withProjections(autoSchema ?
AUTO_ROLLUP_PROJECTIONS : ROLLUP_PROJECTIONS)
- .build()
+ IncrementalIndexSchema
+ .builder()
+ .withDimensionsSpec(dimensionsSpec)
+ .withMetrics(aggs)
+ .withRollup(true)
+ .withMinTimestamp(UTC_MIDNIGHT.getMillis())
+ .withProjections(autoSchema ?
AUTO_ROLLUP_PROJECTIONS : ROLLUP_PROJECTIONS)
+ .build()
)
.writeNullColumns(true)
.rows(ROLLUP_ROWS);
diff --git
a/processing/src/test/java/org/apache/druid/segment/projections/ProjectionsTest.java
b/processing/src/test/java/org/apache/druid/segment/projections/ProjectionsTest.java
index 7be42c487e9..46fb8912c07 100644
---
a/processing/src/test/java/org/apache/druid/segment/projections/ProjectionsTest.java
+++
b/processing/src/test/java/org/apache/druid/segment/projections/ProjectionsTest.java
@@ -26,13 +26,16 @@ import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.Intervals;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.query.aggregation.LongSumAggregatorFactory;
+import org.apache.druid.query.expression.TestExprMacroTable;
import org.apache.druid.query.filter.EqualityFilter;
import org.apache.druid.query.filter.LikeDimFilter;
import org.apache.druid.segment.AggregateProjectionMetadata;
import org.apache.druid.segment.CursorBuildSpec;
+import org.apache.druid.segment.VirtualColumn;
import org.apache.druid.segment.VirtualColumns;
import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.segment.column.RowSignature;
+import org.apache.druid.segment.virtual.ExpressionVirtualColumn;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.junit.jupiter.api.Assertions;
@@ -88,6 +91,107 @@ class ProjectionsTest
Assertions.assertEquals(expected, projectionMatch);
}
+ @Test
+ void testSchemaMatchDifferentTimeZone_hourlyMatches()
+ {
+ VirtualColumn ptHourlyFloor = new ExpressionVirtualColumn(
+ "__ptHourly",
+ "timestamp_floor(__time, 'PT1H', null, 'America/Los_Angeles')",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ VirtualColumn hourlyFloor = new ExpressionVirtualColumn(
+ "__hourly",
+ "timestamp_floor(__time, 'PT1H', null, null)",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ RowSignature baseTable = RowSignature.builder()
+ .addTimeColumn()
+ .add("a", ColumnType.LONG)
+ .add("b", ColumnType.STRING)
+ .add("c", ColumnType.LONG)
+ .build();
+ AggregateProjectionMetadata spec = new AggregateProjectionMetadata(
+ AggregateProjectionSpec.builder("some_projection")
+ .virtualColumns(hourlyFloor)
+ .groupingColumns(new
LongDimensionSchema("__hourly"), new LongDimensionSchema("a"))
+ .aggregators(new
LongSumAggregatorFactory("c_sum", "c"))
+ .build()
+ .toMetadataSchema(),
+ 12345
+ );
+ CursorBuildSpec cursorBuildSpec = CursorBuildSpec.builder()
+
.setVirtualColumns(VirtualColumns.create(ptHourlyFloor))
+
.setPhysicalColumns(Set.of("__time", "c"))
+
.setPreferredOrdering(List.of())
+
.setAggregators(List.of(new LongSumAggregatorFactory("c", "c")))
+ .build();
+
+ ProjectionMatch projectionMatch = Projections.matchAggregateProjection(
+ spec.getSchema(),
+ cursorBuildSpec,
+ Intervals.ETERNITY,
+ new RowSignatureChecker(baseTable)
+ );
+ ProjectionMatch expected = new ProjectionMatch(
+ CursorBuildSpec.builder()
+ .setAggregators(List.of(new
LongSumAggregatorFactory("c", "c")))
+ .setVirtualColumns(VirtualColumns.create(ptHourlyFloor))
+ .setPhysicalColumns(Set.of("__time", "c_sum"))
+ .setPreferredOrdering(List.of())
+ .build(),
+ Map.of("c", "c_sum")
+ );
+ Assertions.assertEquals(expected, projectionMatch);
+ }
+
+ @Test
+ void testSchemaMatchDifferentTimeZone_dailyDoesNotMatch()
+ {
+ VirtualColumn ptDailyFloor = new ExpressionVirtualColumn(
+ "__ptDaily",
+ "timestamp_floor(__time, 'P1D', null, 'America/Los_Angeles')",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ VirtualColumn dailyFloor = new ExpressionVirtualColumn(
+ "__daily",
+ "timestamp_floor(__time, 'P1D', null, null)",
+ ColumnType.LONG,
+ TestExprMacroTable.INSTANCE
+ );
+ RowSignature baseTable = RowSignature.builder()
+ .addTimeColumn()
+ .add("a", ColumnType.LONG)
+ .add("b", ColumnType.STRING)
+ .add("c", ColumnType.LONG)
+ .build();
+ AggregateProjectionMetadata spec = new AggregateProjectionMetadata(
+ AggregateProjectionSpec.builder("some_projection")
+ .virtualColumns(dailyFloor)
+ .groupingColumns(new
LongDimensionSchema("__daily"), new LongDimensionSchema("a"))
+ .aggregators(new
LongSumAggregatorFactory("c_sum", "c"))
+ .build()
+ .toMetadataSchema(),
+ 12345
+ );
+ CursorBuildSpec cursorBuildSpec = CursorBuildSpec.builder()
+
.setVirtualColumns(VirtualColumns.create(ptDailyFloor))
+
.setPhysicalColumns(Set.of("__time", "c"))
+
.setPreferredOrdering(List.of())
+
.setAggregators(List.of(new LongSumAggregatorFactory("c", "c")))
+ .build();
+
+ ProjectionMatch projectionMatch = Projections.matchAggregateProjection(
+ spec.getSchema(),
+ cursorBuildSpec,
+ Intervals.ETERNITY,
+ new RowSignatureChecker(baseTable)
+ );
+ Assertions.assertNull(projectionMatch);
+ }
+
@Test
void testSchemaMatchFilter()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]