com.sap.cds.ql.impl.MatchPredicateNormalizer Maven / Gradle / Ivy
The newest version!
/************************************************************************
* © 2021-2023 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.ql.impl;
import static com.sap.cds.impl.builder.model.Conjunction.and;
import static com.sap.cds.impl.builder.model.StructuredTypeRefImpl.typeRef;
import static com.sap.cds.impl.parser.token.RefSegmentImpl.refSegment;
import static com.sap.cds.ql.CQL.not;
import static com.sap.cds.ql.cqn.CqnMatchPredicate.Quantifier.ALL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.sap.cds.impl.AssociationAnalyzer;
import com.sap.cds.impl.builder.model.ElementRefImpl;
import com.sap.cds.impl.builder.model.ExistsSubquery;
import com.sap.cds.impl.builder.model.MatchPredicate;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredTypeRef;
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.CqnMatchPredicate;
import com.sap.cds.ql.cqn.CqnMatchPredicate.Quantifier;
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.CqnStatement;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnValidationException;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.ql.cqn.CqnVisitor;
import com.sap.cds.ql.cqn.Modifier;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsElementNotFoundException;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.util.CdsModelUtils;
public class MatchPredicateNormalizer {
public static final CqnReference.Segment $OUTER = refSegment(CqnExistsSubquery.OUTER);
private final AssociationAnalyzer associationAnalyzer = new AssociationAnalyzer();
private final CdsModel model;
private final CdsStructuredType target;
public MatchPredicateNormalizer(CdsModel model, CdsStructuredType target) {
this.model = model;
this.target = target;
}
public S normalize(S s) {
s = CQL.copy(s, new Path2Nested());
s = CQL.copy(s, new Match2Exists());
return s;
}
public Predicate normalize(CqnPredicate pred) {
Predicate copy = CQL.copy(pred, new Path2Nested());
copy = CQL.copy(copy, new Match2Exists());
return copy;
}
private class Path2Nested implements Modifier {
@Override
public Predicate match(CqnMatchPredicate match) {
return convertToNestedIfNecessary(target, match.ref(), match.predicate().orElse(null), match.quantifier());
}
private Predicate convertToNestedIfNecessary(CdsStructuredType rowType, CqnStructuredTypeRef ref,
CqnPredicate pred, Quantifier quantifier) {
List extends Segment> segments = ref.segments();
int n = segments.size();
for (int i = 1; i < n; i++) {
Segment segment = segments.get(i - 1);
CdsElement element = rowType.getElement(segment.id());
CdsType elementType = element.getType();
if (elementType.isStructured()) {
rowType = elementType.as(CdsStructuredType.class);
} else if (elementType.isAssociation()) {
rowType = elementType.as(CdsAssociationType.class).getTarget();
if (!CdsModelUtils.isSingleValued(elementType)) {
// 1:n in the middle
// author.books.publisher.journals[title='xyz']
// 0: author, 1: books
CqnStructuredTypeRef prefix = typeRef(segments.subList(0, i), null);
CqnStructuredTypeRef suffix = typeRef(segments.subList(i, n), null);
Predicate nested = convertToNestedIfNecessary(rowType, suffix, pred, quantifier);
return MatchPredicate.match(prefix, quantifier, nested);
}
}
}
return MatchPredicate.match(ref, quantifier, pred);
}
}
private class Match2Exists implements Modifier {
@Override
public CqnStructuredTypeRef ref(CqnStructuredTypeRef ref) {
return resolveInfixFilters(model.getStructuredType(ref.firstSegment()), ref);
}
private CqnStructuredTypeRef resolveInfixFilters(CdsStructuredType rowType, CqnStructuredTypeRef ref) {
Iterator extends Segment> iter = ref.segments().iterator();
List segments = new ArrayList<>(ref.size());
CqnReference.Segment seg = iter.next();
segments.add(normalizeSegment(seg, rowType));
while (iter.hasNext()) {
seg = iter.next();
rowType = navigate(rowType, seg.id());
segments.add(normalizeSegment(seg, rowType));
}
return CQL.to(segments).asRef();
}
@Override
public CqnSelectListItem expand(CqnExpand expand) {
resolveInfixFilters(target.getTargetOf(expand.ref().firstSegment()), expand.ref());
return expand;
}
@Override
public Predicate match(CqnMatchPredicate match) {
CqnStructuredTypeRef ref = match.ref();
CqnPredicate pred = match.predicate().orElse(null);
Quantifier quantifier = match.quantifier();
CdsElement association = getAssociation(ref);
CdsEntity target = association.getType().as(CdsAssociationType.class).getTarget();
StructuredTypeRef from = absoluteRef(target, ref);
rejectToManyPathInFilter(target, pred);
CqnPredicate where = innerToOuter(getPrefix(ref), association);
if (pred != null) {
pred = normalize(target, pred);
if (quantifier == ALL) {
pred = not(pred);
}
where = and(where, pred);
}
CqnSelect subquery = Select.from(from).where(where);
ExistsSubquery exists = new ExistsSubquery(subquery);
if (quantifier == ALL) {
return exists.not();
}
return exists;
}
private void rejectToManyPathInFilter(CdsEntity entity, CqnPredicate pred) {
if (pred == null) {
return;
}
CqnVisitor visitor = new CqnVisitor() {
@Override
public void visit(CqnElementRef ref) {
CdsStructuredType structType = entity;
for (Segment segment : ref.segments()) {
CdsType elType = structType.getElement(segment.id()).getType();
if (elType.isStructured()) {
structType = elType.as(CdsStructuredType.class);
} else if (elType.isAssociation()) {
CdsAssociationType assoc = elType.as(CdsAssociationType.class);
structType = assoc.getTarget();
if (CdsModelUtils.isToMany(assoc)) {
String message = MessageFormat.format(
"Association ''{0}'' with to-many target cardinality in anyMatch/allMatch is not supported",
segment.id());
throw new CqnValidationException(message);
}
}
}
}
};
pred.accept(visitor);
}
private CqnPredicate innerToOuter(List outerPrefix, CdsElement association) {
CqnPredicate on = associationAnalyzer.getOnCondition(association);
Modifier m = new Modifier() {
@Override
public CqnValue ref(CqnElementRef ref) {
List segments = new ArrayList<>(ref.segments());
if (ref.firstSegment().equals(association.getName())) {
segments.remove(0);
} else if (!CdsModelUtils.isContextElementRef(ref)) {
segments.addAll(0, outerPrefix);
}
return ElementRefImpl.elementRef(segments, null, null);
}
};
on = ExpressionVisitor.copy(on, m);
return on;
}
private StructuredTypeRef absoluteRef(CdsEntity target, CqnStructuredTypeRef ref) {
CqnPredicate filter = ref.targetSegment().filter().map(ExpressionVisitor::copy).orElse(null);
return CQL.entity(target.getQualifiedName()).filter(filter).asRef();
}
private CqnReference.Segment normalizeSegment(CqnReference.Segment seg, CdsStructuredType rowType) {
CqnPredicate filter = seg.filter().map(f -> normalize(rowType, f)).orElse(null);
return refSegment(seg.id(), filter);
}
private Predicate normalize(CdsStructuredType rowType, CqnPredicate pred) {
return new MatchPredicateNormalizer(model, rowType).normalize(pred);
}
private List getPrefix(CqnStructuredTypeRef ref) {
List extends Segment> all = ref.segments();
int size = all.size();
List extends Segment> start = all.subList(0, size - 1);
List prefix = new ArrayList<>(size);
prefix.add($OUTER);
prefix.addAll(start);
return prefix;
}
private CdsElement getAssociation(CqnStructuredTypeRef ref) {
CdsStructuredType rowType = target;
String root = rowType.getQualifiedName();
CdsElement element = CdsModelUtils.element(rowType, ref.segments());
CdsType elementType = element.getType();
if (!elementType.isAssociation()) {
String message = MessageFormat.format(
"The reference {0}.{1} does not terminate in an association and can''t be used with the anyMatch predicate.",
root, ref.path());
throw new CqnValidationException(message);
}
return element;
}
}
private static CdsStructuredType navigate(CdsStructuredType rowType, String id) {
try {
CdsType type = rowType.getElement(id).getType();
if (type.isStructured()) {
return type.as(CdsStructuredType.class);
}
if (type.isAssociation()) {
return type.as(CdsAssociationType.class).getTarget();
}
} catch (CdsElementNotFoundException e) {
throw e;
}
String message = MessageFormat.format(
"The reference {0}.{1} does not terminate in an association and can''t be used with the anyMatch predicate.",
rowType.getQualifiedName(), id);
throw new CqnValidationException(message);
}
}