com.sap.cds.ql.impl.CqnNormalizer Maven / Gradle / Ivy
/************************************************************************
* © 2020-2024 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.ql.impl;
import static com.sap.cds.DataStoreConfiguration.SELECT_STAR;
import static com.sap.cds.DataStoreConfiguration.SELECT_STAR_COLUMNS;
import static com.sap.cds.DataStoreConfiguration.UNIVERSAL_CSN;
import static com.sap.cds.util.CqnStatementUtils.isToOnePath;
import static com.sap.cds.util.CqnStatementUtils.removeVirtualElements;
import static com.sap.cds.util.CqnStatementUtils.resolveExpands;
import static com.sap.cds.util.CqnStatementUtils.resolveKeyPlaceholder;
import static com.sap.cds.util.CqnStatementUtils.resolveStar;
import static com.sap.cds.util.CqnStatementUtils.resolveStructureComparison;
import static com.sap.cds.util.CqnStatementUtils.simplify;
import static com.sap.cds.util.CqnStatementUtils.unfoldInline;
import static com.sap.cds.util.NestedStructsResolver.resolveNestedStructs;
import static com.sap.cds.util.PathExpressionResolver.resolvePath;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.sap.cds.CqnTableFunction;
import com.sap.cds.DataStoreConfiguration;
import com.sap.cds.SessionContext;
import com.sap.cds.impl.Context;
import com.sap.cds.impl.DraftUtils;
import com.sap.cds.impl.builder.model.ExistsSubquery;
import com.sap.cds.jdbc.hana.HanaDbContext;
import com.sap.cds.jdbc.spi.SearchResolver;
import com.sap.cds.jdbc.spi.StatementResolver;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Source;
import com.sap.cds.ql.TableFunction;
import com.sap.cds.ql.cqn.CqnAnalyzer;
import com.sap.cds.ql.cqn.CqnDelete;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnExistsSubquery;
import com.sap.cds.ql.cqn.CqnExpand;
import com.sap.cds.ql.cqn.CqnFilterableStatement;
import com.sap.cds.ql.cqn.CqnInsert;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnSearchTermPredicate;
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.CqnSortSpecification;
import com.sap.cds.ql.cqn.CqnSource;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.ql.cqn.CqnUpsert;
import com.sap.cds.ql.cqn.Modifier;
import com.sap.cds.ql.hana.HierarchySubset;
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.CqnCalculatedElementsSubstitutor;
import com.sap.cds.util.CqnStatementUtils;
import com.sap.cds.util.EtagPredicateNormalizer;
import com.sap.cds.util.OnConditionAnalyzer;
public class CqnNormalizer {
private final CdsModel cdsModel;
private final DataStoreConfiguration config;
private final CqnAnalyzer analyzer;
private final Context context;
private final StatementResolver statementResolver;
private SessionContext sessionContext;
private SearchResolver searchResolver;
public CqnNormalizer(Context context) {
this.context = context;
this.cdsModel = context.getCdsModel();
this.sessionContext = context.getSessionContext();
this.analyzer = CqnAnalyzer.create(cdsModel);
this.config = context.getDataStoreConfiguration();
this.statementResolver = context.getDbContext().getStatementResolver();
}
private SearchResolver getSearchResolver(Map hints) {
// Lazy load SearchResolver, as HanaSearchResolver is stateful
// TODO: make Hana search resolver is stateless, by removing 'inSubquery'
if (null == searchResolver || hints.get(HanaDbContext.NO_USE_HEX_PLAN) == Boolean.TRUE
|| hints.get(HanaDbContext.USE_HEX_PLAN) == Boolean.TRUE) {
this.searchResolver = context.getSearchResolver(hints);
}
return searchResolver;
}
public CqnSelect resolveTransformations(CqnSelect select) {
if (!select.transformations().isEmpty()) {
Select> copy = statementResolver.applyTransformations((Select>) select);
copy = normalizeSelect(copy, select.hints());
return copy;
}
return select;
}
public CqnSelect normalize(CqnSelect select) {
select = statementResolver.preOptimize(select);
Map hints = select.hints();
return normalizeSelect(select, hints);
}
private Select> normalizeSelect(CqnSelect select, Map hints) {
Select> copy = normalizeSource(select, hints);
CdsStructuredType target = CqnStatementUtils.targetType(cdsModel, copy);
boolean includeAssocs = includeManagedAssocs(copy);
resolveStar(copy, target, includeAssocs);
resolveNestedStructs(copy, target, includeAssocs);
resolveKeyPlaceholder(target, copy);
// TODO don't copy
if (copy.from().isRef()) {
copy = resolveStructureComparison(cdsModel, target, copy);
}
copy = resolveExpands(copy, target, includeAssocs);
unfoldInline(copy, target);
substituteAliasesInOderingSpec(copy);
removeVirtualElements(copy, target);
copy = resolveEtag(target, copy);
copy = (Select>) getSearchResolver(hints).resolve(copy);
copy = resolveMatchPredicates(target, copy);
copy = (Select>) DraftUtils.resolveConstantElements(cdsModel, target, copy);
simplify(target, copy);
return copy;
}
private Select> normalizeSource(CqnSelect select, Map hints) {
Select> copy;
CqnSource source = select.from();
if (source.isRef()) {
// SB.copy copies the ref
copy = SelectBuilder.copy(select);
} else if (source.isSelect()) {
CqnSelect subquery = source.asSelect();
// a SearchResolver may push down a search expression
select.search().ifPresent(s -> getSearchResolver(hints).pushDownSearchToSubquery(select, subquery));
final CqnSelect inner = normalizeSelect(subquery, hints);
copy = SelectBuilder.from(inner);
moveAllQueryPartsFromOldSelectToNewOuterSelect(select, copy);
} else if (source.isTableFunction()) {
CqnTableFunction tableFunction = select.from().asTableFunction();
resolveSearch(tableFunction, hints);
final TableFunction inner = normalizeTableFunction((TableFunction) tableFunction, hints);
copy = SelectBuilder.from(inner);
moveAllQueryPartsFromOldSelectToNewOuterSelect(select, copy);
} else {
throw new IllegalStateException("unexpected source for select: " + source.getClass().getName());
}
return copy;
}
private void resolveSearch(CqnTableFunction tableFunction, Map hints) {
if (tableFunction instanceof HierarchySubset hierarchy) {
hierarchy.startWhere().ifPresent(s -> {
CdsStructuredType rowType = CqnStatementUtils.rowType(cdsModel, tableFunction.source());
CqnPredicate startWhere = CQL.copy(s, new Modifier() {
@Override
public CqnPredicate searchTerm(CqnSearchTermPredicate search) {
return getSearchResolver(hints).resolve(search, rowType, true);
}
});
hierarchy.startWhere(startWhere);
});
}
}
private TableFunction normalizeTableFunction(TableFunction tableFunction, Map hints) {
CqnSource source = tableFunction.source();
Source> normalizedSource;
if (source == null) {
normalizedSource = null;
} else if (source.isRef()) {
CqnStructuredTypeRef ref = source.asRef();
if (ref.size() == 1 && ref.rootSegment().filter().isEmpty()) {
normalizedSource = (Source>) source;
} else {
normalizedSource = normalizeSelect(Select.from(ref).columns(CQL.star()), hints);
}
} else if (source.isSelect()) {
normalizedSource = normalizeSelect(source.asSelect(), hints);
} else if (source.isTableFunction()) {
normalizedSource = (Source>) normalizeTableFunction((TableFunction) source.asTableFunction(), hints);
} else {
throw new IllegalStateException("unexpected source for select: " + source.getClass().getName());
}
return tableFunction.copy(normalizedSource);
}
private S resolveEtag(CdsStructuredType target, S statement) {
return new EtagPredicateNormalizer(cdsModel, target).normalize(statement);
}
private void substituteAliasesInOderingSpec(Select> source) {
List sortSpecifications = source.orderBy();
if (!sortSpecifications.isEmpty()) {
source.orderBy(sortSpecifications.stream().map(spec -> {
if (spec.value().isRef()) {
return source.items().stream().flatMap(CqnSelectListItem::ofValue)
.filter(v -> v.alias().isPresent())
.filter(v -> spec.value().asRef().path().equals(v.displayName())).findFirst()
.map(v -> CQL.sort(v.value(), spec.order())).orElse(spec);
} else {
return spec;
}
}).toList());
}
}
private boolean includeManagedAssocs(CqnSelect select) {
return select.from().isRef() && config.getProperty(UNIVERSAL_CSN, false)
&& SELECT_STAR_COLUMNS.equals(config.getProperty(SELECT_STAR));
}
public CqnSelect resolveForwardMappedAssocs(CqnSelect select) {
CdsStructuredType target = CqnStatementUtils.targetType(cdsModel, select);
return resolveForwardMappedAssocs(select, target);
}
private CqnSelect resolveForwardMappedAssocs(CqnSelect select, CdsStructuredType target) {
return SelectBuilder.copy(select, new Modifier() {
@Override
public List items(List items) {
List resolved = resolveForwardMappedAssocs(target, items);
return Modifier.super.items(resolved);
}
@Override
public CqnSelectListItem expand(CqnExpand expand) {
if (!expand.ref().lastSegment().equals("*")) {
CdsStructuredType expTarget = CdsModelUtils.target(target, expand.ref().segments());
List resolved = resolveForwardMappedAssocs(expTarget, expand.items());
return CQL.copy(expand).items(resolved);
}
return expand;
}
}, false);
}
/*
* Returns a list of slis based on items w/o associations. Forward-mapped
* associations are resolved to FK elements with a structuring alias.
* Reverse-mapped associations cause a UnsupportedOperationException.
*/
private List resolveForwardMappedAssocs(CdsStructuredType target,
List items) {
List slis = new ArrayList<>();
for (CqnSelectListItem item : items) {
if (item.isRef()) {
CdsElement element = CdsModelUtils.element(target, item.asRef());
if (element.getType().isAssociation()) {
slis.addAll(resolveForwardMappedAssoc(target, item.asValue(), element));
continue;
}
}
slis.add(item);
}
return slis;
}
/*
* Returns the FK elements of forward-mapped to-one associations. Reverse-mapped
* associations cause a UnsupportedOperationException.
*/
private List resolveForwardMappedAssoc(CdsStructuredType target, CqnSelectListValue refValue,
CdsElement element) {
List slis = new ArrayList<>();
CqnElementRef ref = refValue.value().asRef();
if (isToOnePath(target, ref.segments()) && !CdsModelUtils.isReverseAssociation(element)) {
OnConditionAnalyzer onCondAnalyzer = new OnConditionAnalyzer(element, false);
onCondAnalyzer.getFkMapping().forEach((fk, r) -> {
if (!fk.contains(".") && r.isRef() && r.asRef().firstSegment().equals(ref.lastSegment())) {
ElementRef> elementRef = CQL.to(ref.segments().subList(0, ref.segments().size() - 1)).get(fk);
slis.add(elementRef.as(refValue.displayName() + "." + r.asRef().displayName()));
}
});
} else {
throw new UnsupportedOperationException("Association " + element.getQualifiedName()
+ " is not supported on the select list. Only forward-mapped to-one associations are supported on the select list");
}
return slis;
}
private S resolveMatchPredicates(CdsStructuredType target, S stmt) {
return new MatchPredicateNormalizer(cdsModel, target).normalize(stmt);
}
private void moveAllQueryPartsFromOldSelectToNewOuterSelect(CqnSelect select, Select> outer) {
if (select.isDistinct()) {
outer.distinct();
}
if (select.hasInlineCount()) {
outer.inlineCount();
}
outer.columns(select.items());
select.where().ifPresent(outer::where);
select.search().ifPresent(s -> {
Predicate searchExpression = (Predicate) s;
Collection searchableElements = ((SelectBuilder>) outer).searchableElements();
outer.search(e -> searchExpression, searchableElements);
});
select.having().ifPresent(outer::having);
outer.groupBy(select.groupBy());
outer.orderBy(select.orderBy());
outer.limit(select.top(), select.skip());
outer.excluding(select.excluding());
select.getLock().ifPresent(l -> outer.lock(l.timeout().get()));
outer.hints(select.hints());
}
public CqnInsert normalize(CqnInsert insert) {
insert = resolvePath(cdsModel, insert, sessionContext);
return insert;
}
public CqnUpsert normalize(CqnUpsert upsert) {
upsert = resolvePath(cdsModel, upsert, sessionContext);
return upsert;
}
public CqnUpdate normalize(CqnUpdate update) {
update = resolvePath(cdsModel, update);
CdsEntity target = CdsModelUtils.entity(cdsModel, update.ref());
update = CQL.copy(update, new CqnCalculatedElementsSubstitutor(target));
update = resolveEtag(target, update);
return norm(update);
}
public CqnDelete normalize(CqnDelete delete) {
delete = resolvePath(cdsModel, delete);
CdsEntity target = CdsModelUtils.entity(cdsModel, delete.ref());
delete = CQL.copy(delete, new CqnCalculatedElementsSubstitutor(target));
delete = resolveEtag(target, delete);
return norm(delete);
}
public CqnExistsSubquery normalize(CqnExistsSubquery existsSubquery) {
CqnSelect select = statementResolver.preOptimize(existsSubquery.subquery());
Map hints = select.hints();
Select> copy = normalizeSource(select, hints);
CdsStructuredType target = CqnStatementUtils.targetType(cdsModel, copy);
copy = (Select>) getSearchResolver(hints).resolve(copy);
copy = resolveMatchPredicates(target, copy);
resolveKeyPlaceholder(target, copy);
simplify(target, (Select>) copy);
return new ExistsSubquery(copy);
}
private S norm(S stmt) {
CdsEntity target = analyzer.analyze(stmt.ref()).targetEntity();
stmt = resolveKeyPlaceholder(target, stmt);
stmt = resolveStructureComparison(cdsModel, target, stmt);
stmt = resolveMatchPredicates(target, stmt);
return stmt;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy