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