com.sap.cds.jdbc.hana.search.HanaSearchResolverUsingContains Maven / Gradle / Ivy
The newest version!
/*******************************************************************
* © 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_MODE_LOCALIZED_ASSOC;
import static com.sap.cds.impl.builder.model.Conjunction.and;
import static com.sap.cds.impl.parser.token.CqnBoolLiteral.TRUE;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_CDS_PERSISTENCE_EXISTS;
import static com.sap.cds.util.CdsSearchUtils.getSearchableElements;
import static com.sap.cds.util.CqnStatementUtils.simplifyPredicate;
import static java.util.stream.Collectors.toSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sap.cds.DataStoreConfiguration;
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.ql.CQL;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnListValue;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference;
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.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;
public class HanaSearchResolverUsingContains extends HanaSearchResolver {
private static final Logger logger = LoggerFactory.getLogger(HanaSearchResolverUsingContains.class);
public HanaSearchResolverUsingContains(DataStoreConfiguration config, CdsModel cdsModel, Locale locale) {
super(config, cdsModel, locale);
}
@Override
public CqnPredicate resolve(CqnPredicate searchExpression, CdsStructuredType rowType, boolean exactSearch) {
Collection searchableElementRefs = CdsSearchUtils.searchableElementRefs(rowType);
return CdsSearchUtils.searchToLikeExpression(searchableElementRefs, searchExpression);
}
@Override
protected CqnPredicate searchToHana(Map containsRefs, CqnPredicate expression) {
CqnListValue refs = ListValue.of(containsRefs.keySet());
String searchString = toSearchString(simplifyPredicate(expression), false).searchString();
return CQL.booleanFunc("CONTAINS", List.of(refs, CQL.val(searchString)));
}
@Override
protected boolean needsPushToSubquery(CdsElement element) {
return isDeclaredByActiveEntity(element);
}
private static boolean isDeclaredByActiveEntity(CdsElement element) {
CdsStructuredType declaringType = element.getDeclaringType();
if (isActiveEntity(declaringType)) {
logger.debug("""
Fallback to search with LIKE. Entity {} is draft-enabled. This causes a subquery in \
SQL, which prevents the usage of CONTAINS.\
""", declaringType);
return true;
} else {
return false;
}
}
private static boolean isActiveEntity(CdsStructuredType targetType) {
return DraftUtils.isDraftEnabled(targetType) && !DraftUtils.isDraftView(targetType);
}
@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);
}
@Override
protected void handleLargeStringElement(Set like, Map contains,
CdsStructuredType targetType, CqnElementRef ref, CdsElement element) {
logger.debug(
"Searching large string element {} in entity {} using LIKE since CONTAINS is not supported for (N)CLOB columns on SAP HANA",
ref, targetType);
like.add(ref);
}
@Override
protected boolean handleLocalizedElement(CdsStructuredType targetType, Set like,
Map contains, boolean languageGiven,
CqnElementRef ref, CdsElement element) {
if (languageGiven) {
if (isReachableViaLocalizedAssoc(element)) {
contains.put(ref, element);
contains.put(localizedRef(ref), element);
return true; // push to subquery
} else {
like.add(ref);
}
} else {
contains.put(ref, element);
}
return false;
}
@Override
protected void handleToManyElements(CdsStructuredType targetType, Set like,
Map contains, boolean languageGiven, CqnElementRef ref,
CdsElement element) {
if (element.isLocalized()) {
like.add(ref);
} else {
contains.put(ref, element);
}
}
@Override
protected void handleRegularElement(CdsStructuredType targetType, Set like,
Map contains, CqnElementRef ref, CdsElement element) {
if (targetType instanceof CdsEntity e && isComputed(e, ref)) {
like.add(ref);
} else {
contains.put(ref, element);
}
}
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, target entities can have calculated
// elements
if (!targetEntity.isView()) {
return targetEntity.findElement(ref.path()).map(CdsElement::isCalculated).orElse(false);
}
Optional targetQuery = targetEntity.query();
if (targetQuery.isEmpty()) {
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 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);
}
@Override
protected String defaultSearchMode() {
return SEARCH_MODE_LOCALIZED_ASSOC;
}
}