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

com.sap.cds.jdbc.hana.search.HanaSearchResolver Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/************************************************************************
 * © 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 segs1 = ref1.segments();
			List 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;
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy