All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.handler.sql.SolrFilter Maven / Gradle / Ivy

/*
 * 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 fieldNames;
    private final RexBuilder builder;

    Translator(List 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 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 gte = getFieldValuePair(gteNode);
      Pair 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 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 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 andStrings = new ArrayList<>();
      List notStrings = new ArrayList<>();

      List ands = new ArrayList<>();
      List 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 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 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 getFieldValuePair(RexNode node) {
      if (!(node instanceof RexCall)) {
        throw new AssertionError("expected RexCall for predicate but found: " + node);
      }

      RexCall call = (RexCall) node;
      Pair 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 translateBinary(RexCall call) {
      List 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 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 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 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 reverseAggMappings;

    HavingTranslator(List fieldNames, Map 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 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 andStrings = new ArrayList<>();
      List notStrings = new ArrayList<>();

      List ands = new ArrayList<>();
      List 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 translateBinary(RexCall call) {
      List 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 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 b = translateBinary2(right, left);
        if (b != null) {
          return b;
        }
      }

      throw new AssertionError("cannot translate call " + call);
    }

    @Override
    protected String translateComparison(RexNode node) {
      Pair 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);
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy