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.ql.impl.ExpandProcessor Maven / Gradle / Ivy
/*******************************************************************
* © 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);
}
}