blob: f33bdbade0273ff971390b8312c869bbbaf73f8d [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.solr.handler.sql;
import java.lang.invoke.MethodHandles;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptCost;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.core.Filter;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexInputRef;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.Pair;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of a {@link org.apache.calcite.rel.core.Filter} relational expression in Solr.
*/
class SolrFilter extends Filter implements SolrRel {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final Pattern CALCITE_TIMESTAMP_REGEX = Pattern.compile("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$");
private final RexBuilder builder;
SolrFilter(
RelOptCluster cluster,
RelTraitSet traitSet,
RelNode child,
RexNode condition) {
super(cluster, traitSet, child, condition);
assert getConvention() == SolrRel.CONVENTION;
assert getConvention() == child.getConvention();
builder = child.getCluster().getRexBuilder();
}
@Override
public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) {
return super.computeSelfCost(planner, mq).multiplyBy(0.1);
}
public SolrFilter copy(RelTraitSet traitSet, RelNode input, RexNode condition) {
return new SolrFilter(getCluster(), traitSet, input, condition);
}
public void implement(Implementor implementor) {
implementor.visitChild(0, getInput());
if (getInput() instanceof SolrAggregate) {
HavingTranslator translator = new HavingTranslator(SolrRules.solrFieldNames(getRowType()), implementor.reverseAggMappings, builder);
String havingPredicate = translator.translateMatch(condition);
implementor.setHavingPredicate(havingPredicate);
} else {
Translator translator = new Translator(SolrRules.solrFieldNames(getRowType()), builder);
String query = translator.translateMatch(condition);
implementor.addQuery(query);
implementor.setNegativeQuery(query.startsWith("-"));
}
}
private static class Translator {
protected final List<String> fieldNames;
private final RexBuilder builder;
Translator(List<String> fieldNames, RexBuilder builder) {
this.fieldNames = fieldNames;
this.builder = builder;
}
protected String translateMatch(RexNode condition) {
if (log.isDebugEnabled()) {
log.debug("translateMatch condition={} {}", condition.getKind(), condition.getClass().getName());
}
final SqlKind kind = condition.getKind();
if (condition.isA(SqlKind.SEARCH)) {
return translateSearch(condition);
} else if (kind.belongsTo(SqlKind.COMPARISON) || kind == SqlKind.NOT) {
return translateComparison(condition);
} else if (condition.isA(SqlKind.AND)) {
return translateAndOrBetween(condition);
} else if (condition.isA(SqlKind.OR)) {
return "(" + translateOr(condition) + ")";
} else if (kind == SqlKind.LIKE) {
return translateLike(condition);
} else if (kind == SqlKind.IS_NOT_NULL || kind == SqlKind.IS_NULL) {
return translateIsNullOrIsNotNull(condition);
} else {
return null;
}
}
protected String translateAndOrBetween(RexNode condition) {
// see if this is a translated range query of greater than or equals and less than or equal on same field
// if so, then collapse into a single range criteria, e.g. field:[gte TO lte] instead of two ranges AND'd together
RexCall call = (RexCall) condition;
List<RexNode> operands = call.getOperands();
String query = null;
if (operands.size() == 2) {
RexNode lhs = operands.get(0);
RexNode rhs = operands.get(1);
if (lhs.getKind() == SqlKind.GREATER_THAN_OR_EQUAL && rhs.getKind() == SqlKind.LESS_THAN_OR_EQUAL) {
query = translateBetween(lhs, rhs);
} else if (lhs.getKind() == SqlKind.LESS_THAN_OR_EQUAL && rhs.getKind() == SqlKind.GREATER_THAN_OR_EQUAL) {
// just swap the nodes
query = translateBetween(rhs, lhs);
}
}
query = (query != null ? query : translateAnd(condition));
if (log.isDebugEnabled()) {
log.debug("translated query match={}", query);
}
return "(" + query + ")";
}
protected String translateBetween(RexNode gteNode, RexNode lteNode) {
Pair<String, RexLiteral> gte = getFieldValuePair(gteNode);
Pair<String, RexLiteral> lte = getFieldValuePair(lteNode);
String fieldName = gte.getKey();
String query = null;
if (fieldName.equals(lte.getKey()) && compareRexLiteral(gte.right, lte.right) < 0) {
query = fieldName + ":[" + toSolrLiteral(gte.getValue()) + " TO " + toSolrLiteral(lte.getValue()) + "]";
}
return query;
}
@SuppressWarnings("unchecked")
private int compareRexLiteral(final RexLiteral gte, final RexLiteral lte) {
return gte.getValue().compareTo(lte.getValue());
}
protected String translateIsNullOrIsNotNull(RexNode node) {
if (!(node instanceof RexCall)) {
throw new AssertionError("expected RexCall for predicate but found: " + node);
}
RexCall call = (RexCall) node;
List<RexNode> operands = call.getOperands();
if (operands.size() != 1) {
throw new AssertionError("expected 1 operand for " + node);
}
final RexNode left = operands.get(0);
if (left instanceof RexInputRef) {
String name = fieldNames.get(((RexInputRef) left).getIndex());
SqlKind kind = node.getKind();
return kind == SqlKind.IS_NOT_NULL ? "+" + name + ":*" : "(*:* -" + name + ":*)";
}
throw new AssertionError("expected field ref but found " + left);
}
protected String translateOr(RexNode condition) {
List<String> ors = new ArrayList<>();
for (RexNode node : RelOptUtil.disjunctions(condition)) {
String orQuery = translateMatch(node);
if (orQuery.startsWith("-")) {
orQuery = "(*:* "+orQuery+")";
}
ors.add(orQuery);
}
return String.join(" OR ", ors);
}
protected String translateAnd(RexNode node0) {
List<String> andStrings = new ArrayList<>();
List<String> notStrings = new ArrayList<>();
List<RexNode> ands = new ArrayList<>();
List<RexNode> nots = new ArrayList<>();
RelOptUtil.decomposeConjunction(node0, ands, nots);
for (RexNode node : ands) {
String andQuery = translateMatch(node);
if (andQuery.startsWith("-")) {
andQuery = "(*:* "+andQuery+")";
}
andStrings.add(andQuery);
}
String andString = String.join(" AND ", andStrings);
if (!nots.isEmpty()) {
for (RexNode node : nots) {
notStrings.add(translateMatch(node));
}
String notString = String.join(" NOT ", notStrings);
return "(" + andString + ") NOT (" + notString + ")";
} else {
return andString;
}
}
protected String translateLike(RexNode like) {
Pair<String, RexLiteral> pair = getFieldValuePair(like);
String terms = pair.getValue().toString().trim();
terms = terms.replace("'", "").replace('%', '*').replace('_', '?');
boolean wrappedQuotes = false;
if (!terms.startsWith("(") && !terms.startsWith("[") && !terms.startsWith("{")) {
// restore the * and ? after escaping
terms = "\"" + ClientUtils.escapeQueryChars(terms).replace("\\*", "*").replace("\\?", "?") + "\"";
wrappedQuotes = true;
}
String query = pair.getKey() + ":" + terms;
return wrappedQuotes ? "{!complexphrase}" + query : query;
}
protected String translateComparison(RexNode node) {
final SqlKind kind = node.getKind();
if (kind == SqlKind.NOT) {
RexNode negated = ((RexCall) node).getOperands().get(0);
return "-" + (negated.getKind() == SqlKind.LIKE ? translateLike(negated) : translateMatch(negated));
}
Pair<String, RexLiteral> binaryTranslated = getFieldValuePair(node);
final String key = binaryTranslated.getKey();
RexLiteral value = binaryTranslated.getValue();
switch (kind) {
case EQUALS:
return toEqualsClause(key, value, node);
case NOT_EQUALS:
return "-" + toEqualsClause(key, value, node);
case LESS_THAN:
return "(" + key + ": [ * TO " + toSolrLiteral(value) + " })";
case LESS_THAN_OR_EQUAL:
return "(" + key + ": [ * TO " + toSolrLiteral(value) + " ])";
case GREATER_THAN:
return "(" + key + ": { " + toSolrLiteral(value) + " TO * ])";
case GREATER_THAN_OR_EQUAL:
return "(" + key + ": [ " + toSolrLiteral(value) + " TO * ])";
case LIKE:
return translateLike(node);
case IS_NOT_NULL:
case IS_NULL:
return translateIsNullOrIsNotNull(node);
default:
throw new AssertionError("cannot translate " + node);
}
}
private String toEqualsClause(String key, RexLiteral value, RexNode node) {
SqlTypeName fieldTypeName = ((RexCall) node).getOperands().get(0).getType().getSqlTypeName();
String terms = toSolrLiteralForEquals(value, fieldTypeName).trim();
boolean wrappedQuotes = false;
if (!terms.startsWith("(") && !terms.startsWith("[") && !terms.startsWith("{")) {
terms = "\"" + ClientUtils.escapeQueryChars(terms) + "\"";
wrappedQuotes = true;
}
String clause = key + ":" + terms;
if (terms.contains("*") && wrappedQuotes) {
clause = "{!complexphrase}" + clause;
}
return clause;
}
// translate to a literal string value for Solr queries, such as translating a
// Calcite timestamp value into an ISO-8601 formatted timestamp that Solr likes
private String toSolrLiteral(RexLiteral literal) {
Object value2 = literal.getValue2();
SqlTypeName typeName = literal.getTypeName();
final String solrLiteral;
if (value2 instanceof Long && (typeName == SqlTypeName.TIMESTAMP || typeName == SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE)) {
// return as an ISO-8601 timestamp
solrLiteral = Instant.ofEpochMilli((Long) value2).toString();
} else {
solrLiteral = value2.toString();
}
return solrLiteral;
}
// special case handling for expressions like: WHERE timestamp = '2021-06-04 04:00:00'
// Calcite passes the right hand side as a string instead of as a Long
private String toSolrLiteralForEquals(RexLiteral literal, SqlTypeName fieldTypeName) {
Object value2 = literal.getValue2();
final String solrLiteral;
// oddly, for = criteria with a timestamp field, Calcite passes us a String instead of a Long as it does with other operators like >
if (value2 instanceof String && fieldTypeName == SqlTypeName.TIMESTAMP && CALCITE_TIMESTAMP_REGEX.matcher((String) value2).matches()) {
String timestamp = ((String) value2).replace(' ', 'T').replace("'", "");
if (Character.isDigit(timestamp.charAt(timestamp.length() - 1))) {
timestamp += "Z";
}
solrLiteral = timestamp;
} else {
solrLiteral = toSolrLiteral(literal);
}
return solrLiteral;
}
protected Pair<String, RexLiteral> getFieldValuePair(RexNode node) {
if (!(node instanceof RexCall)) {
throw new AssertionError("expected RexCall for predicate but found: " + node);
}
RexCall call = (RexCall) node;
Pair<String, RexLiteral> binaryTranslated = call.getOperands().size() == 2 ? translateBinary(call) : null;
if (binaryTranslated == null) {
throw new AssertionError("unsupported predicate expression: " + node);
}
return binaryTranslated;
}
/**
* Translates a call to a binary operator, reversing arguments if necessary.
*/
protected Pair<String, RexLiteral> translateBinary(RexCall call) {
List<RexNode> operands = call.getOperands();
if (operands.size() != 2) {
throw new AssertionError("Invalid number of arguments - " + operands.size());
}
final RexNode left = operands.get(0);
final RexNode right = operands.get(1);
final Pair<String, RexLiteral> a = translateBinary2(left, right);
if (a != null) {
return a;
}
// we can swap these if doing an equals / not equals
if (call.op.kind == SqlKind.EQUALS || call.op.kind == SqlKind.NOT_EQUALS) {
final Pair<String, RexLiteral> b = translateBinary2(right, left);
if (b != null) {
return b;
}
}
if (left.getKind() == SqlKind.CAST && right.getKind() == SqlKind.CAST) {
return translateBinary2(((RexCall) left).operands.get(0), ((RexCall) right).operands.get(0));
}
// for WHERE clause like: pdatex >= '2021-07-13T15:12:10.037Z'
if (left.getKind() == SqlKind.INPUT_REF && right.getKind() == SqlKind.CAST) {
final RexCall cast = ((RexCall) right);
if (cast.operands.size() == 1 && cast.operands.get(0).getKind() == SqlKind.LITERAL) {
return translateBinary2(left, cast.operands.get(0));
}
}
throw new AssertionError("cannot translate call " + call);
}
/**
* Translates a call to a binary operator. Returns whether successful.
*/
protected Pair<String, RexLiteral> translateBinary2(RexNode left, RexNode right) {
if (log.isDebugEnabled()) {
log.debug("translateBinary2 left={} right={}", left, right);
}
if (right.getKind() != SqlKind.LITERAL) {
if (log.isDebugEnabled()) {
log.debug("right != SqlKind.LITERAL, return null");
}
return null;
}
final RexLiteral rightLiteral = (RexLiteral) right;
switch (left.getKind()) {
case INPUT_REF:
final RexInputRef left1 = (RexInputRef) left;
String name = fieldNames.get(left1.getIndex());
return new Pair<>(name, rightLiteral);
case CAST:
return translateBinary2(((RexCall) left).operands.get(0), right);
// case OTHER_FUNCTION:
// String itemName = SolrRules.isItem((RexCall) left);
// if (itemName != null) {
// return translateOp2(op, itemName, rightLiteral);
// }
default:
return null;
}
}
/**
* A search node can be an IN or NOT IN clause or a BETWEEN
*/
protected String translateSearch(RexNode condition) {
final String fieldName = getSolrFieldName(condition);
RexCall expanded = (RexCall) RexUtil.expandSearch(builder, null, condition);
final RexNode peekAt0 = !expanded.operands.isEmpty() ? expanded.operands.get(0) : null;
if (expanded.op.kind == SqlKind.AND) {
// See if NOT IN was translated into a big AND not
if (peekAt0 instanceof RexCall) {
RexCall op0 = (RexCall) peekAt0;
if (op0.op.kind == SqlKind.NOT_EQUALS) {
return "*:* -" + fieldName + ":" + toOrSetOnSameField(expanded);
}
}
} else if (expanded.op.kind == SqlKind.OR) {
if (peekAt0 instanceof RexCall) {
RexCall op0 = (RexCall) peekAt0;
if (op0.op.kind == SqlKind.EQUALS) {
return fieldName + ":" + toOrSetOnSameField(expanded);
}
}
}
if (expanded.getKind() != SqlKind.SEARCH) {
// passing a search back to translateMatch would lead to infinite recursion ...
return translateMatch(expanded);
}
// don't know how to handle this search!
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unsupported search filter: " + condition);
}
protected String toOrSetOnSameField(RexCall search) {
String orClause = search.operands.stream().map(n -> {
RexCall next = (RexCall) n;
RexLiteral lit = (RexLiteral) next.getOperands().get(1);
return "\"" + toSolrLiteral(lit) + "\"";
}).collect(Collectors.joining(" OR "));
return "(" + orClause + ")";
}
protected String getSolrFieldName(RexNode node) {
RexCall call = (RexCall) node;
final RexNode left = call.getOperands().get(0);
if (left instanceof RexInputRef) {
return fieldNames.get(((RexInputRef) left).getIndex());
}
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Expected Solr field name for " + call.getKind() + " but found " + left);
}
}
private static class HavingTranslator extends Translator {
private final Map<String, String> reverseAggMappings;
HavingTranslator(List<String> fieldNames, Map<String, String> reverseAggMappings, RexBuilder builder) {
super(fieldNames, builder);
this.reverseAggMappings = reverseAggMappings;
}
@Override
protected String translateMatch(RexNode condition) {
if (condition.getKind().belongsTo(SqlKind.COMPARISON)) {
return translateComparison(condition);
} else if (condition.isA(SqlKind.AND)) {
return translateAnd(condition);
} else if (condition.isA(SqlKind.OR)) {
return translateOr(condition);
} else {
return null;
}
}
@Override
protected String translateOr(RexNode condition) {
List<String> ors = new ArrayList<>();
for (RexNode node : RelOptUtil.disjunctions(condition)) {
ors.add(translateMatch(node));
}
StringBuilder builder = new StringBuilder();
builder.append("or(");
for (int i = 0; i < ors.size(); i++) {
if (i > 0) {
builder.append(",");
}
builder.append(ors.get(i));
}
builder.append(")");
return builder.toString();
}
@Override
protected String translateAnd(RexNode node0) {
List<String> andStrings = new ArrayList<>();
List<String> notStrings = new ArrayList<>();
List<RexNode> ands = new ArrayList<>();
List<RexNode> nots = new ArrayList<>();
RelOptUtil.decomposeConjunction(node0, ands, nots);
for (RexNode node : ands) {
andStrings.add(translateMatch(node));
}
StringBuilder builder = new StringBuilder();
builder.append("and(");
for (int i = 0; i < andStrings.size(); i++) {
if (i > 0) {
builder.append(",");
}
builder.append(andStrings.get(i));
}
builder.append(")");
if (!nots.isEmpty()) {
for (RexNode node : nots) {
notStrings.add(translateMatch(node));
}
StringBuilder notBuilder = new StringBuilder();
for (int i = 0; i < notStrings.size(); i++) {
if (i > 0) {
notBuilder.append(",");
}
notBuilder.append("not(");
notBuilder.append(notStrings.get(i));
notBuilder.append(")");
}
return "and(" + builder.toString() + "," + notBuilder.toString() + ")";
} else {
return builder.toString();
}
}
/**
* Translates a call to a binary operator, reversing arguments if necessary.
*/
@Override
protected Pair<String, RexLiteral> translateBinary(RexCall call) {
List<RexNode> operands = call.getOperands();
if (operands.size() != 2) {
throw new AssertionError("Invalid number of arguments - " + operands.size());
}
final RexNode left = operands.get(0);
final RexNode right = operands.get(1);
final Pair<String, RexLiteral> a = translateBinary2(left, right);
if (a != null) {
if (reverseAggMappings.containsKey(a.getKey())) {
return new Pair<>(reverseAggMappings.get(a.getKey()), a.getValue());
}
return a;
}
if (call.op.kind == SqlKind.EQUALS || call.op.kind == SqlKind.NOT_EQUALS) {
final Pair<String, RexLiteral> b = translateBinary2(right, left);
if (b != null) {
return b;
}
}
throw new AssertionError("cannot translate call " + call);
}
@Override
protected String translateComparison(RexNode node) {
Pair<String, RexLiteral> binaryTranslated = getFieldValuePair(node);
switch (node.getKind()) {
case EQUALS:
String terms = binaryTranslated.getValue().getValue2().toString().trim();
return "eq(" + binaryTranslated.getKey() + "," + terms + ")";
case NOT_EQUALS:
return "not(eq(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + "))";
case LESS_THAN:
return "lt(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + ")";
case LESS_THAN_OR_EQUAL:
return "lteq(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + ")";
case GREATER_THAN:
return "gt(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + ")";
case GREATER_THAN_OR_EQUAL:
return "gteq(" + binaryTranslated.getKey() + "," + binaryTranslated.getValue() + ")";
default:
throw new AssertionError("cannot translate " + node);
}
}
}
}