Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cds.jdbc.hana.search.HanaSearchResolver Maven / Gradle / Ivy
/************************************************************************
* © 2020-2023 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.jdbc.hana.search;
import static com.sap.cds.impl.builder.model.Conjunction.and;
import static com.sap.cds.impl.parser.token.CqnBoolLiteral.FALSE;
import static com.sap.cds.impl.parser.token.CqnBoolLiteral.TRUE;
import static com.sap.cds.reflect.CdsBaseType.LARGE_STRING;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_CDS_PERSISTENCE_EXISTS;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_CDS_SEARCH_MODE;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_VALUE_SEARCH_MODE_VIEW;
import static com.sap.cds.util.CdsSearchUtils.getSearchableElements;
import static com.sap.cds.util.CdsSearchUtils.moveSearchToWhere;
import static com.sap.cds.util.CqnStatementUtils.simplifyPredicate;
import static java.util.stream.Collectors.toSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sap.cds.impl.DraftUtils;
import com.sap.cds.impl.builder.model.ElementRefImpl;
import com.sap.cds.impl.builder.model.ListValue;
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.Predicate;
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.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference;
import com.sap.cds.ql.cqn.CqnReference.Segment;
import com.sap.cds.ql.cqn.CqnSearchPredicate;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnSelectListValue;
import com.sap.cds.ql.cqn.CqnSource;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.ql.cqn.CqnVisitor;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CdsSearchUtils;
import com.sap.cds.util.CqnStatementUtils;
/**
* A {@link SearchResolver} implementation that renders a CONTAINS function in
* an EXISTS subquery.
*/
public class HanaSearchResolver extends AbstractSearchResolver {
private static final Logger logger = LoggerFactory.getLogger(HanaSearchResolver.class);
public HanaSearchResolver(CdsModel cdsModel, Locale locale) {
super(cdsModel, locale);
}
private static CqnElementRef concatRefs(CqnElementRef prefix, CqnElementRef suffix) {
int tail = suffix.size();
List segs = new ArrayList<>(prefix.size() + tail - 1);
segs.addAll(prefix.segments());
segs.addAll(suffix.segments().subList(1, tail));
return ElementRefImpl.elementRef(segs, null, null);
}
private static boolean isActiveEntity(CdsStructuredType targetType) {
return DraftUtils.isDraftEnabled(targetType) && !DraftUtils.isDraftView(targetType);
}
@Override
protected void resolve(CqnSelect select, CqnPredicate search, CdsStructuredType targetType,
Collection searchableRefs) {
// subqueries are resolved
CdsEntity targetEntity = (CdsEntity) targetType;
Set likeMainQuery = new TreeSet<>(ElementRefComparator.INSTANCE); // normal, LIKE, MQ
Set containsSubquery = new TreeSet<>(ElementRefComparator.INSTANCE); // CONTAINS, SQ
Set containsMainQuery = new TreeSet<>(ElementRefComparator.INSTANCE); // CONTAINS, MQ
/*
* 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 isActiveEntityOfDraft = false;
/*
* if any ref is navigating any 1:n association enforce pushed down to subquery
*/
boolean navigatesToManyAssoc = false;
if (ANNOTATION_VALUE_SEARCH_MODE_VIEW.equalsIgnoreCase(getSearchModeAnnotation(targetType))) {
logger.debug(
"All searchable refs in targetEntity {} will be searched with LIKE on the main query" + "due to annotation {}.",
targetEntity, ANNOTATION_CDS_SEARCH_MODE);
likeMainQuery.addAll(searchableRefs);
} else {
for (CqnElementRef ref : searchableRefs) {
CdsElement element = CdsModelUtils.element(targetEntity, ref);
if (element.getType().isSimpleType(LARGE_STRING)) {
logger.debug(
"The searchable ref {} in targetEntity {} will be searched with LIKE since it's type " + "LargeString is mapped to NCLOB on HANA. NCLOB cannot be searched " + "with CONTAINS.",
ref, targetEntity);
likeMainQuery.add(ref);
} else if (isComputed(targetEntity, ref)) {
likeMainQuery.add(ref);
} else if (element.isLocalized()) {
// localized elements are not computed
handleLocalizedElement(likeMainQuery, containsSubquery, containsMainQuery, languageGiven, ref,
element);
} else {
containsMainQuery.add(ref);
}
isActiveEntityOfDraft = isActiveEntityOfDraft || isDeclaredByActiveEntity(element);
navigatesToManyAssoc = navigatesToManyAssoc || navigatesToManyAssoc(targetEntity, ref);
}
}
attachContainsAndLikeExpressionsToStatement(select, search, targetEntity, likeMainQuery, containsSubquery, containsMainQuery,
isActiveEntityOfDraft, navigatesToManyAssoc);
}
private void handleLocalizedElement(Set likeMainQuery, Set containsSubquery,
Set containsMainQuery, boolean languageGiven, CqnElementRef ref, CdsElement element) {
if (languageGiven) {
if (isReachableViaLocalizedAssoc(element)) {
containsSubquery.add(ref);
containsSubquery.add(localizedRef(ref));
} else {
likeMainQuery.add(ref);
}
} else {
containsMainQuery.add(ref);
}
}
private void attachContainsAndLikeExpressionsToStatement(CqnSelect select, CqnPredicate search, CdsEntity targetEntity,
Set likeMainQuery, Set containsSubquery, Set containsMainQuery,
boolean isActiveEntityOfDraft, boolean navigatesToManyAssoc) {
CqnPredicate filter = CqnBoolLiteral.FALSE;
Collection containsRefs = new TreeSet<>(ElementRefComparator.INSTANCE);
containsRefs.addAll(containsMainQuery);
containsRefs.addAll(containsSubquery);
if (logger.isDebugEnabled() && !containsRefs.isEmpty()) {
String names = refNames(containsRefs);
logger.debug("The following searchable element refs of {} that can be searched with CONTAINS: {}.",
targetEntity, names);
}
CqnPredicate contains = searchToHanaContains(containsRefs, search);
if (navigatesToManyAssoc || !containsSubquery.isEmpty() || !likeMainQuery.isEmpty() || isActiveEntityOfDraft) {
contains = pushDownToExistsSubquery(targetEntity, contains, true);
}
filter = CQL.or(filter, contains);
if (logger.isDebugEnabled() && !likeMainQuery.isEmpty()) {
String names = refNames(likeMainQuery);
logger.debug("The following searchable element refs of {} that can be searched with LIKE: {}.",
targetEntity, names);
}
CqnPredicate like = CdsSearchUtils.searchToLikeExpression(likeMainQuery, search);
if (navigatesToManyAssoc) {
like = pushDownToExistsSubquery(targetEntity, like, true);
}
filter = CQL.or(filter, like);
moveSearchToWhere(select, filter);
}
private boolean isComputed(CdsEntity targetEntity, CqnElementRef ref) {
if (Boolean.TRUE.equals(targetEntity.getAnnotationValue(ANNOTATION_CDS_PERSISTENCE_EXISTS, false))) {
logger.debug(
"The searchable ref {} is treated as 'computed' as the targetEntity {} is annotated with " + "{} and we cannot analyze computed refs.",
ref, targetEntity, ANNOTATION_CDS_PERSISTENCE_EXISTS);
return true;
}
//only views can have computed fields :-)
if (!targetEntity.isView()) {
return false;
}
Optional targetQuery = targetEntity.query();
if (!targetQuery.isPresent()) {
logger.debug(
"The searchable ref {} is treated as 'computed' as the targetEntity {} is a view with an unsupported query.",
ref, targetEntity);
return true;
}
CqnSelect query = targetQuery.get();
CqnSource source = query.from();
if (!source.isRef()) {
logger.debug(
"The searchable ref {} is treated as 'computed' as the query {} of the targetEntity {} selects " + "from a source which is not a ref {}.",
ref, query, targetEntity, source);
return true;
}
String startSegName = ref.firstSegment();
Optional match = query.items().stream().flatMap(CqnSelectListItem::ofValue)
.filter(slv -> slv.displayName().equals(startSegName)).findFirst();
if (match.isPresent()) {
CqnSelectListValue slv = match.get();
if (!slv.isRef()) {
return true;
}
ref = concatRefs(slv.asRef(), ref);
}
CqnStructuredTypeRef typeRef = query.ref();
CdsEntity sourceEntity = CdsModelUtils.entity(model, typeRef);
return isComputed(sourceEntity, ref);
}
private String refNames(Collection refs) {
return refs.stream().map(ref -> ref.asValue().displayName()).collect(Collectors.joining(","));
}
@Override
public void pushDownSearchToSubquery(CqnSelect select, CqnSelect subquery) {
CqnPredicate merged = and(subquery.search(), select.search()).orElse(TRUE);
CqnSource source = subquery.from();
if (source.isRef()) {
// innermost
CdsEntity targetEntity = CdsModelUtils.entity(model, subquery.ref());
// we only want to search the elements on the select list of the subquery
Collection resolved = CqnStatementUtils.resolveStar(subquery.items(),
subquery.excluding(), targetEntity, false);
Set exposed = resolved.stream().flatMap(CqnSelectListItem::ofRef).map(CqnReference::lastSegment)
.collect(toSet());
Set intersection = getSearchableElements(subquery, targetEntity).stream()
.map(CqnReference::lastSegment).filter(exposed::contains).collect(toSet());
((SelectBuilder>) subquery.asSelect()).search(t -> (Predicate) merged, intersection);
} else {
((SelectBuilder>) subquery.asSelect()).search(merged);
}
((SelectBuilder>) select).search((CqnPredicate) null);
}
private static boolean isDeclaredByActiveEntity(CdsElement element) {
CdsStructuredType declaringType = element.getDeclaringType();
if (isActiveEntity(declaringType)) {
logger.debug("Fallback to search with LIKE. Entity {} is draft-enabled which causes a subquery in"
+ "SQL which prevents the usage of CONTAINS.", declaringType);
return true;
} else {
return false;
}
}
private CqnPredicate searchToHanaContains(Collection searchableRefs, CqnPredicate expression) {
// no searchable elements -> no contains function!
if (searchableRefs.isEmpty()) {
return FALSE;
}
CqnValue refs = ListValue.of(searchableRefs);
CqnValue searchExpression = cqnSearchPredicateToHanaContainsSearchString(simplifyPredicate(expression));
return CQL.booleanFunc("CONTAINS", Arrays.asList(refs, searchExpression));
}
private CqnValue cqnSearchPredicateToHanaContainsSearchString(CqnPredicate expression) {
Stack stack = new Stack<>();
CqnVisitor visitor = new CqnVisitor() {
@Override
public void visit(CqnSearchPredicate search) {
String searchTerm = search.searchTerm();
if (searchTerm.trim().contains(" ")) {
stack.push("*\"" + searchTerm + "\"*");
} else {
stack.push("*" + searchTerm + "*");
}
}
@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 CQL.val(stack.pop());
}
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;
}
}
}