| /* |
| * 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); |
| } |
| } |