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

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

There is a newer version: 3.4.0
Show newest version
/*******************************************************************
 * © 2023 SAP SE or an SAP affiliate company. All rights reserved. *
 *******************************************************************/
package com.sap.cds.ql.impl;

import static com.sap.cds.util.CqnStatementUtils.hiddenName;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
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.Row;
import com.sap.cds.impl.builder.model.ExpandBuilder;
import com.sap.cds.impl.parser.token.CqnBoolLiteral;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.cqn.CqnComparisonPredicate;
import com.sap.cds.ql.cqn.CqnExpand;
import com.sap.cds.ql.cqn.CqnListValue;
import com.sap.cds.ql.cqn.CqnLiteral;
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.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_JOIN = "join";
	private static final String EXPAND_USING_SUBQUERY = "subquery";
	private static final String FK_PREFIX = "@fk:";

	private final CdsModel model;
	private final CqnStructuredTypeRef parentRef;
	private final CqnStructuredTypeRef expandRef;
	private final ExpandBuilder expand;
	private final long queryLimit;

	private Map elementMapping;
	private Map aliasMapping;

	private String expandMethod;

	private ExpandProcessor(CdsModel model, CqnStructuredTypeRef parentRef, CdsStructuredType parentType,
			CqnExpand expand, String expandMethod, long queryLimit) {
		this.model = model;
		this.expand = (ExpandBuilder) expand;
		this.expandRef = expand.ref();
		this.parentRef = parentRef;
		this.expandMethod = expandMethod;
		this.queryLimit = queryLimit;
	}

	public static ExpandProcessor create(CdsModel model, CqnStructuredTypeRef parentRef, CdsStructuredType parentType,
			CqnExpand expand, boolean pathExpand, long queryLimit) {
		boolean toOne = CqnStatementUtils.isToOnePath(parentType, expand.ref().segments());
		CdsElement assoc = CdsModelUtils.element(parentType, expand.ref().segments());

		String expandMethod = determinExpandMethod(parentType, expand, assoc, toOne || !pathExpand);
		ExpandProcessor expandProcessor = new ExpandProcessor(model, parentRef, parentType, expand, expandMethod,
				queryLimit);
		if (expandProcessor.isPathExpand()) {
			expandProcessor.computeElementMapping(assoc);
		}
		return expandProcessor;
	}

	private static String determinExpandMethod(CdsStructuredType type, CqnExpand expand, CdsElement assoc,
			boolean parentKeys) {
		if (parentKeys || expand.hasLimit() || ((ExpandBuilder) expand).lazy()) {
			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);
			aliasMapping = new HashMap<>(elementMapping.size());
			elementMapping.forEach((fk, v) -> {
				String alias = FK_PREFIX + fk;
				aliasMapping.put(alias, "@" + v.replace('.', '_'));
			});
		} catch (Exception e) {
			logger.debug("Cannot optimize to-many " + assoc.getQualifiedName() + " expand due to on condition", e);
			this.expandMethod = EXPAND_USING_PARENT_KEYS;
		}
	}

	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() && !val.asRef().firstSegment().startsWith("$")) {
				segments.set(segments.size() - 1, val.asRef().lastSegment());
				mapping.put(k, Joiner.on('.').join(segments));
			}
		});
		return mapping;
	}

	public boolean isPathExpand() {
		return EXPAND_USING_JOIN.equals(expandMethod) || EXPAND_USING_SUBQUERY.equals(expandMethod);
	}

	public boolean isParentKeyExpand() {
		return EXPAND_USING_PARENT_KEYS.equals(expandMethod) || expandMethod == null;
	}

	public CqnExpand getExpand() {
		return expand;
	}

	public void addMappingKeys(CqnSelect select) {
		if (isPathExpand()) {
			CqnStatementUtils.selectHidden(elementMapping.values(), select);
		}
	}

	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();

		DataUtils.merge(rows, expResult, expand.displayName(), aliasMapping, FK_PREFIX);
	}

	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());
		if (rows.size() == queryLimit) { // 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) {
		if (!elementMapping.keySet().isEmpty()) {
			List predicates = new ArrayList<>(rows.size());
			List fks = new ArrayList<>(elementMapping.keySet());
			CqnListValue fkElements = CQL.list(fks.stream().map(CQL::get).collect(toList()));
			List parentKeys = fks.stream().map(fk -> hiddenName(elementMapping.get(fk))).collect(toList());
			rows.forEach(row -> addFilter(predicates, fkElements, parentKeys, row));

			return CQL.or(predicates);
		} else {
			return CqnBoolLiteral.TRUE;
		}
	}

	private static void addFilter(List predicates, CqnListValue fkElements, List parentKeys,
			Map row) {
		List> fkValues = new ArrayList<>(parentKeys.size());
		for (String parentKey : parentKeys) {
			Object fkValue = row.get(parentKey);
			if (fkValue == null) {
				return; // parent might not be present (CAP/issue #14257)
			}
			fkValues.add(CQL.val(fkValue));
		}
		predicates.add(CQL.comparison(fkElements, CqnComparisonPredicate.Operator.EQ, CQL.list(fkValues)));
	}

	private List addFks(List expItems) {
		List items = new ArrayList<>(expItems);
		elementMapping.keySet().forEach(fk -> items.add(CQL.get(fk).as(FK_PREFIX + fk)));

		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);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy