com.sap.cds.jdbc.hana.search.HanaSearchResolver Maven / Gradle / Ivy
/************************************************************************
* © 2020-2024 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.jdbc.hana.search;
import static com.sap.cds.DataStoreConfiguration.SEARCH_HANA_FUZZINESS;
import static com.sap.cds.DataStoreConfiguration.SEARCH_HANA_FUZZINESS_DEFAULT;
import static com.sap.cds.DataStoreConfiguration.SEARCH_HANA_FUZZY;
import static com.sap.cds.reflect.CdsBaseType.HANA_CLOB;
import static com.sap.cds.reflect.CdsBaseType.LARGE_STRING;
import static com.sap.cds.util.CdsSearchUtils.moveSearchToWhere;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sap.cds.DataStoreConfiguration;
import com.sap.cds.impl.parser.token.CqnBoolLiteral;
import com.sap.cds.impl.util.Stack;
import com.sap.cds.jdbc.generic.AbstractSearchResolver;
import com.sap.cds.jdbc.spi.SearchResolver;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.cqn.CqnConnectivePredicate;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnNegation;
import com.sap.cds.ql.cqn.CqnPassThroughSearchPredicate;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference.Segment;
import com.sap.cds.ql.cqn.CqnSearchTermPredicate;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnVisitor;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CdsSearchUtils;
/**
* A {@link SearchResolver} implementation that renders a CONTAINS|SCORE
* function in an EXISTS subquery.
*/
public abstract class HanaSearchResolver extends AbstractSearchResolver {
private static final Logger logger = LoggerFactory.getLogger(HanaSearchResolver.class);
private static Pattern wildcardPattern = Pattern.compile("(? searchableRefs) {
Set like = new TreeSet<>(ElementRefComparator.INSTANCE);
Map scoreOrContains = new TreeMap<>(ElementRefComparator.INSTANCE);
/*
* Locale resolution only needs to be performed if a language/locale has been
* given in the session context
*/
boolean languageGiven = locale != null;
/*
* On HANA (prior to HANA CLoud Q1/2021) CONTAINS cannot search over columns
* originating from a subquery. The current draft implementation depends on a
* subquery added late in the SQL generation cycle. Thus, the CONTAINS
* expression must not operate on the main branch of the statement which would
* access the draft subquery's columns but needs to be pushed down to an EXISTS
* subquery where the CONTAINS is not affected by other subqueries.
*/
boolean pushToSubquery = false;
/*
* if any ref is navigating any 1:n association enforce pushed down to subquery
*/
boolean navigatesToManyAssoc = false;
for (CqnElementRef ref : searchableRefs) {
CdsElement element = CdsModelUtils.element(targetType, ref);
CdsType type = element.getType();
if (type.isSimpleType(LARGE_STRING) || type.isSimpleType(HANA_CLOB)) {
handleLargeStringElement(like, scoreOrContains, targetType, ref, element);
} else if (element.isLocalized()) {
pushToSubquery = handleLocalizedElement(targetType, like, scoreOrContains, languageGiven, ref, element);
} else {
handleRegularElement(targetType, like, scoreOrContains, ref, element);
}
pushToSubquery = pushToSubquery || needsPushToSubquery(element);
navigatesToManyAssoc = navigatesToManyAssoc || navigatesToManyAssoc(targetType, ref);
}
attachSearchExpressionsToStatement(select, search, targetType, scoreOrContains, pushToSubquery, like,
navigatesToManyAssoc);
}
protected abstract void handleLargeStringElement(Set like,
Map scoreOrContains, CdsStructuredType targetType, CqnElementRef ref, CdsElement element);
protected abstract boolean handleLocalizedElement(CdsStructuredType targetType, Set like,
Map scoreOrContains, boolean languageGiven,
CqnElementRef ref, CdsElement element);
protected abstract void handleRegularElement(CdsStructuredType targetType, Set like,
Map scoreOrContains, CqnElementRef ref, CdsElement element);
private void attachSearchExpressionsToStatement(CqnSelect select, CqnPredicate search, CdsStructuredType targetType,
Map scoreOrContains, boolean pushToSubquery, Set like, boolean navigatesToManyAssoc) {
CqnPredicate filter = CqnBoolLiteral.FALSE;
if (!scoreOrContains.isEmpty()) {
CqnPredicate searchPredicate = searchToHana(scoreOrContains, search);
if (navigatesToManyAssoc || pushToSubquery || !like.isEmpty()) {
searchPredicate = pushDownToExistsSubquery(targetType, searchPredicate, true);
}
filter = CQL.or(filter, searchPredicate);
}
if (!like.isEmpty()) {
if (logger.isDebugEnabled()) {
String names = refNames(like);
logger.debug("The following searchable element refs of {} that can be searched with LIKE: {}.",
targetType, names);
}
CqnPredicate likePredicate = CdsSearchUtils.searchToLikeExpression(like, search);
if (navigatesToManyAssoc) {
likePredicate = pushDownToExistsSubquery(targetType, likePredicate, true);
}
filter = CQL.or(filter, likePredicate);
}
moveSearchToWhere(select, filter);
}
private String refNames(Collection refs) {
return refs.stream().map(ref -> ref.asValue().displayName()).collect(Collectors.joining(","));
}
public record SearchString(String searchString, boolean containsWildcards, boolean containsRaw) {
}
protected static SearchString toSearchString(CqnPredicate expression, boolean fuzzy) {
Stack stack = new Stack<>();
boolean[] wildcards = new boolean[] { false };
boolean[] raw = new boolean[] { false };
CqnVisitor visitor = new CqnVisitor() {
@Override
public void visit(CqnSearchTermPredicate search) {
String searchTerm = search.searchTerm();
boolean phrase = searchTerm.trim().contains(" ");
boolean wildcardInSearchTerm = wildcardPattern.matcher(searchTerm).find();
wildcards[0] |= wildcardInSearchTerm;
if (!wildcardInSearchTerm && !fuzzy) {
searchTerm = "*" + searchTerm + "*";
}
if (phrase) {
searchTerm = "\"" + searchTerm + "\"";
}
stack.push(searchTerm);
}
@Override
public void visit(CqnPassThroughSearchPredicate search) {
raw[0] = true;
stack.push(search.searchString());
}
@Override
public void visit(CqnConnectivePredicate connective) {
int n = connective.predicates().size();
String delimiter = connective.operator() == CqnConnectivePredicate.Operator.AND ? " " : " OR ";
stack.push(String.join(delimiter, stack.pop(n)));
}
@Override
public void visit(CqnNegation cqnNegation) {
stack.push("-" + stack.pop());
}
};
expression.accept(visitor);
if (stack.isEmpty()) {
throw new IllegalStateException("the search term stack must not be empty!");
}
return new SearchString(stack.pop(), wildcards[0], raw[0]);
}
private static class ElementRefComparator implements Comparator {
static final Comparator INSTANCE = new ElementRefComparator();
@Override
public int compare(CqnElementRef ref1, CqnElementRef ref2) {
List extends Segment> segs1 = ref1.segments();
List extends Segment> segs2 = ref2.segments();
int n1 = segs1.size();
int n2 = segs2.size();
if (n1 > n2) {
return 1;
}
if (n1 < n2) {
return -1;
}
for (int i = 0; i < n1; i++) {
String id1 = segs1.get(i).id();
String id2 = segs2.get(i).id();
int cmp = id1.compareTo(id2);
if (cmp != 0) {
return cmp;
}
}
return 0;
}
}
protected abstract CqnPredicate searchToHana(Map searchableRefs, CqnPredicate expression);
protected abstract boolean needsPushToSubquery(CdsElement element);
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy