/**
* Copyright (C) 2014-2016 LinkedIn Corp. (pinot-core@linkedin.com)
*
* Licensed 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 com.linkedin.pinot.broker.requesthandler;
import com.linkedin.pinot.common.request.FilterOperator;
import com.linkedin.pinot.common.utils.request.FilterQueryTree;
import com.linkedin.pinot.core.common.predicate.RangePredicate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* FilterOptimizer to merge intersecting range predicates:
* <ul>
* <li> Given a filter query tree with range predicates on the same column, this optimizer merges the
* predicates joined by AND by taking their intersection. </li>
* <li> Pulls up merged predicates in the absence of other predicates on other columns. </li>
* <li> Currently implemented to work for time column only. This is because the broker currently
* does not know the data type for any columns, except for time column. </li>
* </ul>
*/
public class RangeMergeOptimizer extends FilterQueryTreeOptimizer {
private static final String DUMMY_STRING = "__dummy_string__";
@Override
public FilterQueryTree optimize(FilterQueryOptimizerRequest request) {
return optimizeRanges(request.getFilterQueryTree(), request.getTimeColumn());
}
/**
* Recursive method that performs the actual optimization of merging range predicates.
*
* @param current Current node being visited in the DFS of the filter query tree.
* @param timeColumn Name of time column
* @return Returns the optimized filter query tree
*/
@Nonnull
private static FilterQueryTree optimizeRanges(@Nonnull FilterQueryTree current, @Nullable String timeColumn) {
if (timeColumn == null) {
return current;
}
List<FilterQueryTree> children = current.getChildren();
if (children == null || children.isEmpty()) {
return current;
}
// For OR, we optimize all its children, but do not propagate up.
FilterOperator operator = current.getOperator();
if (operator == FilterOperator.OR) {
int length = children.size();
for (int i = 0; i < length; i++) {
children.set(i, optimizeRanges(children.get(i), timeColumn));
}
return current;
}
// After this point, since the node has children, it can only be an 'AND' node (only OR/AND supported).
assert operator == FilterOperator.AND;
List<FilterQueryTree> newChildren = new ArrayList<>();
List<String> intersect = null;
for (FilterQueryTree child : children) {
FilterQueryTree newChild = optimizeRanges(child, timeColumn);
if (newChild.getOperator() == FilterOperator.RANGE && newChild.getColumn().equals(timeColumn)) {
List<String> value = newChild.getValue();
intersect = (intersect == null) ? value : intersectRanges(intersect, value);
} else {
newChildren.add(newChild);
}
}
if (newChildren.isEmpty()) {
return new FilterQueryTree(timeColumn, intersect, FilterOperator.RANGE, null);
} else {
if (intersect != null) {
newChildren.add(new FilterQueryTree(timeColumn, intersect, FilterOperator.RANGE, null));
}
return new FilterQueryTree(null, null, FilterOperator.AND, newChildren);
}
}
/**
* Helper method to compute intersection of two ranges.
* Assumes that values are 'long'. This is OK as this feature is used only for time-column.
*
* @param range1 First range
* @param range2 Second range
* @return Intersection of the given ranges.
*/
public static List<String> intersectRanges(List<String> range1, List<String> range2) {
// Build temporary range predicates to parse the string range values.
RangePredicate predicate1 = new RangePredicate(DUMMY_STRING, range1);
RangePredicate predicate2 = new RangePredicate(DUMMY_STRING, range2);
String lowerString1 = predicate1.getLowerBoundary();
String upperString1 = predicate1.getUpperBoundary();
long lower1 = (lowerString1.equals(RangePredicate.UNBOUNDED)) ? Long.MIN_VALUE : Long.valueOf(lowerString1);
long upper1 = (upperString1.equals(RangePredicate.UNBOUNDED)) ? Long.MAX_VALUE : Long.valueOf(upperString1);
String lowerString2 = predicate2.getLowerBoundary();
String upperString2 = predicate2.getUpperBoundary();
long lower2 = (lowerString2.equals(RangePredicate.UNBOUNDED)) ? Long.MIN_VALUE : Long.valueOf(lowerString2);
long upper2 = (upperString2.equals(RangePredicate.UNBOUNDED)) ? Long.MAX_VALUE : Long.valueOf(upperString2);
final StringBuilder stringBuilder = new StringBuilder();
if (lower1 > lower2) {
stringBuilder.append(
(predicate1.includeLowerBoundary() ? RangePredicate.LOWER_INCLUSIVE : RangePredicate.LOWER_EXCLUSIVE));
stringBuilder.append(lower1);
} else if (lower1 < lower2) {
stringBuilder.append(
(predicate2.includeLowerBoundary() ? RangePredicate.LOWER_INCLUSIVE : RangePredicate.LOWER_EXCLUSIVE));
stringBuilder.append(lower2);
} else {
if (lower1 == Long.MIN_VALUE) { // lower1 == lower2
stringBuilder.append(RangePredicate.LOWER_EXCLUSIVE + RangePredicate.UNBOUNDED); // * always has '('
} else {
stringBuilder.append(
(predicate1.includeLowerBoundary() && predicate2.includeLowerBoundary()) ? RangePredicate.LOWER_INCLUSIVE
: RangePredicate.LOWER_EXCLUSIVE);
stringBuilder.append(lower1);
}
}
stringBuilder.append(RangePredicate.DELIMITER);
if (upper1 < upper2) {
stringBuilder.append(upper1);
stringBuilder.append(
(predicate1.includeUpperBoundary() ? RangePredicate.UPPER_INCLUSIVE : RangePredicate.UPPER_EXCLUSIVE));
} else if (upper1 > upper2) {
stringBuilder.append(upper2);
stringBuilder.append(
(predicate2.includeUpperBoundary() ? RangePredicate.UPPER_INCLUSIVE : RangePredicate.UPPER_EXCLUSIVE));
} else {
if (upper1 == Long.MAX_VALUE) { // upper1 == upper2
stringBuilder.append(RangePredicate.UNBOUNDED + RangePredicate.UPPER_EXCLUSIVE); // * always has ')'
} else {
stringBuilder.append(upper1);
stringBuilder.append(
(predicate1.includeUpperBoundary() && predicate2.includeUpperBoundary()) ? RangePredicate.UPPER_INCLUSIVE
: RangePredicate.UPPER_EXCLUSIVE);
}
}
return Collections.singletonList(stringBuilder.toString());
}
}