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

com.sap.cds.ql.impl.ExpandProcessor Maven / Gradle / Ivy

The newest version!
/************************************************************************
 * © 2023-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cds.ql.impl;

import static com.sap.cds.util.CqnStatementUtils.HIDDEN_PREFIX;
import static com.sap.cds.util.CqnStatementUtils.hasWhereExistsFilter;
import static com.sap.cds.util.CqnStatementUtils.isNoAggregation;
import static com.sap.cds.util.CqnStatementUtils.selected;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Joiner;
import com.sap.cds.CdsDataStore;
import com.sap.cds.Result;
import com.sap.cds.Row;
import com.sap.cds.impl.builder.model.ExpandBuilder;
import com.sap.cds.impl.builder.model.InPredicate;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.cqn.CqnExpand;
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.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.reflect.CdsAnnotation;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.impl.reader.model.CdsConstants;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CqnStatementUtils;
import com.sap.cds.util.DataUtils;
import com.sap.cds.util.OnConditionAnalyzer;
import com.sap.cds.util.PathExpressionResolver;

public class ExpandProcessor {
	private static final Logger logger = LoggerFactory.getLogger(ExpandProcessor.class);

	private static final String EXPAND_USING_PARENT_KEYS = "parent-keys";
	private static final String EXPAND_USING_LOAD_SINGLE = "load-single";
	private static final String EXPAND_USING_JOIN = "join";
	private static final String EXPAND_USING_SUBQUERY = "subquery";
	private static final String FK_PREFIX = HIDDEN_PREFIX + "fk:";

	private final CdsModel model;
	private final CqnStructuredTypeRef parentRef;
	private final CqnStructuredTypeRef expandRef;
	private final ExpandBuilder expand;
	private final long queryTop;
	private final long querySkip;
	private final Map queryHints;

	private final Map mappingAliases;
	private Map elementMapping;

	private String expandMethod;

	private ExpandProcessor(CdsModel model, CqnStructuredTypeRef parentRef, Map mappingAliases,
			CqnExpand expand, String expandMethod, long queryTop, long querySkip, Map queryHints) {
		this.model = model;
		this.expand = (ExpandBuilder) expand;
		this.expandRef = expand.ref();
		this.parentRef = parentRef;
		this.mappingAliases = mappingAliases;
		this.expandMethod = expandMethod;
		this.queryTop = queryTop;
		this.querySkip = querySkip;
		this.queryHints = queryHints;
	}

	public static ExpandProcessor create(CqnSelect query, CdsModel model, CqnStructuredTypeRef parentRef, CdsStructuredType parentType,
			Map parentKeyAliases, CqnExpand expand, boolean pathExpand) {
		boolean toOne = CqnStatementUtils.isToOnePath(parentType, expand.ref().segments());
		CdsElement assoc = CdsModelUtils.element(parentType, expand.ref().segments());
		String expandMethod = determinExpandMethod(parentRef, parentType, expand, assoc, toOne || !pathExpand);
		ExpandProcessor expandProcessor = new ExpandProcessor(model, parentRef, parentKeyAliases, expand, expandMethod,
				query.top(), query.skip(), query.hints());
		expandProcessor.computeElementMapping(assoc);

		return expandProcessor;
	}

	public Map getMappingAliases() {
		return mappingAliases;
	}

	private static String determinExpandMethod(CqnStructuredTypeRef parentRef, CdsStructuredType type, CqnExpand expand, CdsElement assoc,
			boolean parentKeys) {
		if (parentKeys || expand.hasLimit() || ((ExpandBuilder) expand).lazy() || hasWhereExistsFilter(parentRef)) {
			return EXPAND_USING_PARENT_KEYS;
		}
		// to-many
		return (String) assoc.findAnnotation(CdsConstants.ANNOTATION_JAVA_EXPAND + ".using")
				.map(CdsAnnotation::getValue).orElseGet(() -> pathExpandMethod(type, expand));
	}

	private static String pathExpandMethod(CdsStructuredType type, CqnExpand expand) {
		if (CqnStatementUtils.isOneToManyPath(type, expand.ref().segments())) {
			return EXPAND_USING_JOIN;
		}
		return EXPAND_USING_SUBQUERY; // many-to-many
	}

	private void computeElementMapping(CdsElement assoc) {
		try {
			elementMapping = fkMapping(expandRef, assoc);
		} catch (Exception e) {
			this.expandMethod = EXPAND_USING_PARENT_KEYS;
			if (isPathExpand()) {
				logger.debug("Cannot optimize to-many " + assoc.getQualifiedName() + " expand due to on condition", e);
			}
			elementMapping = Collections.emptyMap();
		}
	}

	private static Map fkMapping(CqnStructuredTypeRef ref, CdsElement toManyAssoc) {
		HashMap mapping = new HashMap<>();
		new OnConditionAnalyzer(toManyAssoc, true).getFkMapping().forEach((k, val) -> {
			List segments = ref.stream().map(CqnReference.Segment::id).collect(toList());
			if (val.isRef() && !CdsModelUtils.isContextElementRef(val.asRef())) {
				segments.set(segments.size() - 1, val.asRef().lastSegment());
				mapping.put(k, Joiner.on('.').join(segments));
			}
		});
		return mapping;
	}

	public boolean isPathExpand() {
		return switch (expandMethod) {
			case EXPAND_USING_JOIN, EXPAND_USING_SUBQUERY -> true;
			default -> false;
		};
	}

	public boolean isLoadSingle() {
		return EXPAND_USING_LOAD_SINGLE.equals(expandMethod);
	}

	public ExpandBuilder getExpand() {
		return expand;
	}

	public boolean hasCountAndLimit() {
		return expand.hasInlineCount() && expand.hasLimit();
	}

	public void addMappingKeys(CqnSelect select) {
		if (!elementMapping.isEmpty()) {
			List missing = elementMapping.values().stream().filter(e -> !mappingAliases.containsKey(e)).toList();
			if (!missing.isEmpty()) {
				boolean addMissing = !select.isDistinct() && isNoAggregation(select);
				mappingAliases.putAll(selected(missing, select, addMissing));
			}
		}
	}

	public void expand(List> rows, CdsDataStore dataStore, Map paramValues) {
		if (logger.isDebugEnabled()) {
			logger.debug("Expand to-many {} using {}", expand.ref(), expandMethod);
		}
		CqnSelect query = pathExpandQuery(rows);
		List expResult = dataStore.execute(query, paramValues).list();

		boolean addCount = expand.hasInlineCount() && !expand.hasLimit();
		Map mapping = aliasedMapping();
		DataUtils.merge(rows, expResult, expand.displayName(), mapping, HIDDEN_PREFIX, addCount);
	}

	public void inlineCount(List> rows, CdsDataStore dataStore, Map paramValues) {
		CqnSelect countQuery = countQuery();
		Result counts = dataStore.execute(countQuery, paramValues);
		Map mapping = aliasedMapping();
		DataUtils.addCounts(rows, counts.list(), expand.displayName(), mapping);
	}

	private Map aliasedMapping() {
		Map mapping = new HashMap<>(elementMapping.size());
		elementMapping.forEach((k,v) -> mapping.put(mappingAliases.getOrDefault(k, k), mappingAliases.getOrDefault(v, v)));
		return mapping;
	}

	private CqnSelect pathExpandQuery(List> rows) {
		List expItems = addFks(expand.items());
		StructuredType target = to(parentRef, expand.ref());
		CqnSelect expQuery = Select.from(target).columns(expItems).orderBy(expand.orderBy()).hints(queryHints);
		if (querySkip > 0 || rows.size() == queryTop) { // result might be limited by top
			((Select) expQuery).where(fkFilter(rows));
		}
		if (EXPAND_USING_SUBQUERY.equals(expandMethod)) {
			// INNER JOIN can lead to duplicates if backlink references multiple entities
			// -> transform ref path to where exists (semi-join) - degrades performance (CAP/issue #12541)
			expQuery = PathExpressionResolver.resolvePath(model, expQuery);
		}
		return expQuery;
	}

	private CqnPredicate fkFilter(List> rows) {
		List refList = new ArrayList<>(elementMapping.keySet());
		if (refList.isEmpty()) {
			return CQL.TRUE;
		}
		List parentKeys = refList.stream().map(fk -> mappingAliases.get((elementMapping.get(fk)))).toList();

		return InPredicate.in(refList, rows, parentKeys);
	}

	private CqnSelect countQuery() {
		List items = addFks(new ArrayList<>());
		List groupingColumns = items.stream().map(v -> v.asValue().value()).collect(toList());

		items.add(CQL.count().as("count"));
		return Select.from(to(parentRef, expand.ref())).columns(items).groupBy(groupingColumns);
	}

	private List addFks(List expItems) {
		List items = new ArrayList<>(expItems);
		elementMapping.keySet().forEach(fk -> {
			String alias = mappingAliases.computeIfAbsent(fk, v -> FK_PREFIX + v);
			items.add(CQL.get(fk).as(alias));
		});

		return items;
	}

	private static StructuredType to(CqnReference ref1, CqnReference ref2) {
		List segments = new ArrayList<>(ref1.segments().size() + ref2.size());
		segments.addAll(ref1.segments());
		segments.addAll(ref2.segments());

		return CQL.to(segments);
	}

	public Map getQueryHints() {
		return queryHints;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy