blob: cab72bd20de86cd6f070bd5616c2d249d7b24f63 [file] [log] [blame]
/*
* 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.query.aggregation.post;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.apache.druid.java.util.common.guava.Comparators;
import org.apache.druid.math.expr.Expr;
import org.apache.druid.math.expr.ExprMacroTable;
import org.apache.druid.math.expr.InputBindings;
import org.apache.druid.math.expr.Parser;
import org.apache.druid.query.aggregation.AggregatorFactory;
import org.apache.druid.query.aggregation.PostAggregator;
import org.apache.druid.query.cache.CacheKeyBuilder;
import org.apache.druid.segment.column.ValueType;
import org.apache.druid.utils.CollectionUtils;
import javax.annotation.Nullable;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class ExpressionPostAggregator implements PostAggregator
{
private static final Comparator<Comparable> DEFAULT_COMPARATOR = Comparator.nullsFirst(
(Comparable o1, Comparable o2) -> {
if (o1 instanceof Long && o2 instanceof Long) {
return Long.compare((long) o1, (long) o2);
} else if (o1 instanceof Number && o2 instanceof Number) {
return Double.compare(((Number) o1).doubleValue(), ((Number) o2).doubleValue());
} else {
return o1.compareTo(o2);
}
}
);
private final String name;
private final String expression;
private final Comparator<Comparable> comparator;
@Nullable
private final String ordering;
// type is ignored from equals and friends because it is computed by decorate, and all post-aggs should be decorated
// prior to usage (and is currently done so in the query constructors of all queries which can have post-aggs)
@Nullable
private final ValueType outputType;
private final ExprMacroTable macroTable;
private final Map<String, Function<Object, Object>> finalizers;
private final Supplier<Expr> parsed;
private final Supplier<Set<String>> dependentFields;
private final Supplier<byte[]> cacheKey;
/**
* Constructor for serialization.
*/
@JsonCreator
public ExpressionPostAggregator(
@JsonProperty("name") String name,
@JsonProperty("expression") String expression,
@JsonProperty("ordering") @Nullable String ordering,
@JacksonInject ExprMacroTable macroTable
)
{
this(
name,
expression,
ordering,
macroTable,
ImmutableMap.of(),
Parser.lazyParse(expression, macroTable)
);
}
private ExpressionPostAggregator(
final String name,
final String expression,
@Nullable final String ordering,
final ExprMacroTable macroTable,
final Map<String, Function<Object, Object>> finalizers,
final Supplier<Expr> parsed
)
{
this(
name,
expression,
ordering,
null, // in the future this will be computed by decorate
macroTable,
finalizers,
parsed,
Suppliers.memoize(() -> parsed.get().analyzeInputs().getRequiredBindings())
);
}
private ExpressionPostAggregator(
final String name,
final String expression,
@Nullable final String ordering,
@Nullable final ValueType outputType,
final ExprMacroTable macroTable,
final Map<String, Function<Object, Object>> finalizers,
final Supplier<Expr> parsed,
final Supplier<Set<String>> dependentFields
)
{
Preconditions.checkArgument(expression != null, "expression cannot be null");
this.name = name;
this.expression = expression;
this.ordering = ordering;
// allow nulls to match previous behavior when type was never specified, however this should be non-nullable
// in the future, when expression support type inference
this.outputType = outputType;
// comparator should be specialized to output type ... someday
this.comparator = ordering == null ? DEFAULT_COMPARATOR : Ordering.valueOf(ordering);
this.macroTable = macroTable;
this.finalizers = finalizers;
this.parsed = parsed;
this.dependentFields = dependentFields;
this.cacheKey = Suppliers.memoize(() -> {
return new CacheKeyBuilder(PostAggregatorIds.EXPRESSION)
.appendCacheable(parsed.get())
.appendString(ordering)
.build();
});
}
@Override
public Set<String> getDependentFields()
{
return dependentFields.get();
}
@Override
public Comparator getComparator()
{
return comparator;
}
@Override
public Object compute(Map<String, Object> values)
{
// Maps.transformEntries is lazy, will only finalize values we actually read.
final Map<String, Object> finalizedValues = Maps.transformEntries(
values,
(String k, Object v) -> {
final Function<Object, Object> finalizer = finalizers.get(k);
return finalizer != null ? finalizer.apply(v) : v;
}
);
return parsed.get().eval(InputBindings.withMap(finalizedValues)).value();
}
@Override
@JsonProperty
public String getName()
{
return name;
}
@Override
public ValueType getType()
{
// computed by decorate
return outputType;
}
@Override
public ExpressionPostAggregator decorate(final Map<String, AggregatorFactory> aggregators)
{
return new ExpressionPostAggregator(
name,
expression,
ordering,
null, // this should be computed from expression output type once it supports output type inference
macroTable,
CollectionUtils.mapValues(aggregators, aggregatorFactory -> aggregatorFactory::finalizeComputation),
parsed,
dependentFields
);
}
@JsonProperty("expression")
public String getExpression()
{
return expression;
}
@Nullable
@JsonProperty("ordering")
public String getOrdering()
{
return ordering;
}
@Override
public String toString()
{
return "ExpressionPostAggregator{" +
"name='" + name + '\'' +
", expression='" + expression + '\'' +
", ordering=" + ordering +
'}';
}
@Override
public byte[] getCacheKey()
{
return cacheKey.get();
}
public enum Ordering implements Comparator<Comparable>
{
/**
* Ensures the following order: numeric > NaN > Infinite.
*
* The name may be referenced via Ordering.valueOf(String) in the constructor {@link
* ExpressionPostAggregator#ExpressionPostAggregator(String, String, String, ValueType, ExprMacroTable, Map, Supplier, Supplier)}.
*/
@SuppressWarnings("unused")
numericFirst {
@Override
public int compare(Comparable lhs, Comparable rhs)
{
if (lhs instanceof Long && rhs instanceof Long) {
return Long.compare(((Number) lhs).longValue(), ((Number) rhs).longValue());
} else if (lhs instanceof Number && rhs instanceof Number) {
double d1 = ((Number) lhs).doubleValue();
double d2 = ((Number) rhs).doubleValue();
if (Double.isFinite(d1) && !Double.isFinite(d2)) {
return 1;
}
if (!Double.isFinite(d1) && Double.isFinite(d2)) {
return -1;
}
return Double.compare(d1, d2);
} else {
return Comparators.<Comparable>naturalNullsFirst().compare(lhs, rhs);
}
}
}
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ExpressionPostAggregator that = (ExpressionPostAggregator) o;
if (!comparator.equals(that.comparator)) {
return false;
}
if (!Objects.equals(name, that.name)) {
return false;
}
if (!Objects.equals(expression, that.expression)) {
return false;
}
if (!Objects.equals(ordering, that.ordering)) {
return false;
}
return true;
}
@Override
public int hashCode()
{
return Objects.hash(name, expression, comparator, ordering);
}
}