
com.sap.cds.util.CqnStatementUtils Maven / Gradle / Ivy
/************************************************************************
* © 2020-2023 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.util;
import static com.sap.cds.impl.builder.model.Conjunction.and;
import static com.sap.cds.impl.builder.model.ExpressionImpl.bindValues;
import static com.sap.cds.impl.builder.model.LiteralImpl.val;
import static com.sap.cds.impl.builder.model.StructuredTypeRefImpl.typeRef;
import static com.sap.cds.impl.parser.token.CqnBoolLiteral.FALSE;
import static com.sap.cds.impl.parser.token.CqnBoolLiteral.TRUE;
import static com.sap.cds.ql.CQL.copy;
import static com.sap.cds.ql.CQL.to;
import static com.sap.cds.reflect.CdsBaseType.cdsType;
import static com.sap.cds.reflect.impl.CdsAnnotatableImpl.CdsAnnotationImpl.annotation;
import static com.sap.cds.reflect.impl.CdsArrayedTypeBuilder.arrayedType;
import static com.sap.cds.reflect.impl.CdsElementBuilder.element;
import static com.sap.cds.reflect.impl.CdsSimpleTypeBuilder.undefinedType;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_JAVA_EXPAND;
import static com.sap.cds.reflect.impl.reader.model.CdsConstants.ANNOTATION_PERSISTENCE_NAME;
import static com.sap.cds.util.CdsModelUtils.element;
import static com.sap.cds.util.CdsModelUtils.entity;
import static com.sap.cds.util.CdsModelUtils.isReverseAssociation;
import static com.sap.cds.util.CdsModelUtils.keyNames;
import static com.sap.cds.util.CdsModelUtils.target;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sap.cds.CdsException;
import com.sap.cds.impl.builder.model.ComparisonPredicate;
import com.sap.cds.impl.builder.model.Conjunction;
import com.sap.cds.impl.builder.model.CqnNull;
import com.sap.cds.impl.builder.model.Disjunction;
import com.sap.cds.impl.builder.model.ElementRefImpl;
import com.sap.cds.impl.builder.model.ExistsSubquery;
import com.sap.cds.impl.parser.token.CqnBoolLiteral;
import com.sap.cds.impl.parser.token.RefSegmentBuilder;
import com.sap.cds.impl.parser.token.RefSegmentImpl;
import com.sap.cds.impl.util.Stack;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.Expand;
import com.sap.cds.ql.FilterableStatement;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.StructuredTypeRef;
import com.sap.cds.ql.Update;
import com.sap.cds.ql.Value;
import com.sap.cds.ql.cqn.CqnArithmeticExpression;
import com.sap.cds.ql.cqn.CqnArithmeticNegation;
import com.sap.cds.ql.cqn.CqnComparisonPredicate;
import com.sap.cds.ql.cqn.CqnComparisonPredicate.Operator;
import com.sap.cds.ql.cqn.CqnConnectivePredicate;
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.CqnExpression;
import com.sap.cds.ql.cqn.CqnFunc;
import com.sap.cds.ql.cqn.CqnLiteral;
import com.sap.cds.ql.cqn.CqnNullValue;
import com.sap.cds.ql.cqn.CqnParameter;
import com.sap.cds.ql.cqn.CqnPlain;
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.CqnSelectList;
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.CqnSyntaxException;
import com.sap.cds.ql.cqn.CqnUpdate;
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.CqnXsert;
import com.sap.cds.ql.impl.DeleteBuilder;
import com.sap.cds.ql.impl.ExpressionVisitor;
import com.sap.cds.ql.impl.LeanModifier;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.ql.impl.SelectListValueBuilder;
import com.sap.cds.ql.impl.UpdateBuilder;
import com.sap.cds.ql.impl.Xpr;
import com.sap.cds.ql.impl.XsertBuilder;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsBaseType;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsKind;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsSimpleType;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.reflect.CdsType;
import com.sap.cds.reflect.impl.CdsElementBuilder;
import com.sap.cds.reflect.impl.CdsSimpleTypeBuilder;
import com.sap.cds.reflect.impl.CdsStructuredTypeBuilder;
import com.sap.cds.reflect.impl.DraftAdapter;
public class CqnStatementUtils {
private static final Logger logger = LoggerFactory.getLogger(CqnStatementUtils.class);
private static final String UNDEFINED = "undefined";
private static final String EXPAND_USING_PARENT_KEYS = "parent-keys";
public static final String $JSON = "$json";
private static final CqnValue ANONYM_MARKER = CQL.constant("?");
private static final List $JSON_LIST = Collections.singletonList(CQL.get($JSON).withoutAlias());
private CqnStatementUtils() {
}
public static boolean containsExpand(CqnSelect select) {
return select.items().stream().anyMatch(i -> i instanceof Expand>);
}
public static Stream selectedRefs(CqnSelect select) {
return select.items().stream().filter(CqnSelectListItem::isRef).map(CqnSelectListItem::asValue);
}
public static void selectHidden(Collection elements, CqnSelect select) {
SelectBuilder> query = (SelectBuilder>) select;
elements.forEach(k -> {
String alias = "@" + k.replace(".", "_");
query.addItem(CQL.get(k).as(alias));
query.addExclude(alias);
});
}
public static String hiddenName(String k) {
return "@" + k.replace(".", "_");
}
public static boolean containsRef(List items) {
return items.stream().anyMatch(i -> {
if (i.isRef()) {
return true;
}
if (i.isSelectList()) {
List slItems = i.asSelectList().items();
return containsSelectStar(slItems) || containsRef(slItems);
}
return false;
});
}
public static CqnPredicate extractTargetFilter(CdsStructuredType rowType, CqnUpdate update, boolean useParameters) {
List predicates = new ArrayList<>();
update.where().ifPresent(predicates::add);
Set keys = keyNames(rowType);
update.elements().filter(keys::contains)
.map(key -> CQL.get(key).eq(useParameters ? CQL.param(key) : update.data().get(key)))
.forEach(predicates::add);
update.entries().forEach(data -> keys.forEach(data::remove));
return predicates.stream().collect(Conjunction.and());
}
public static void resolveStructureComparison(CdsStructuredType rowType, FilterableStatement, ?> statement) {
statement.where().map(w -> CQL.copy(w, new StructureComparisonModifier(rowType))).ifPresent(statement::where);
}
public static S resolveStructureComparison(CdsStructuredType rowType, S statement) {
return CQL.copy(statement, new StructureComparisonModifier(rowType));
}
private static class StructureComparisonModifier implements LeanModifier {
private final CdsStructuredType rowType;
StructureComparisonModifier(CdsStructuredType rowType) {
this.rowType = rowType;
}
@Override
public Predicate comparison(Value> lhs, Operator op, Value> rhs) {
if (lhs.isRef()) {
// ref = val
CqnElementRef ref = lhs.asRef();
if (isStructured(rowType, ref)) {
return unfoldComparison(ref, op, rhs);
}
} else if (rhs.isRef()) {
// val = ref
CqnElementRef ref = rhs.asRef();
if (isStructured(rowType, ref)) {
return unfoldComparison(ref, op, lhs);
}
}
return CQL.comparison(lhs, op, rhs);
}
private Predicate unfoldComparison(CqnElementRef ref, Operator op, Value> other) {
Collector connector;
switch (op) {
case EQ:
case IS:
connector = Conjunction.and();
break;
case NE:
case IS_NOT:
connector = Disjunction.or();
break;
default:
throw badOperator(op);
}
Map val;
if (other.isNullValue()) {
val = Collections.emptyMap();
} else if (other.isLiteral() && other.asLiteral().isStructured()) {
val = other.asLiteral().asStructured().value();
} else {
throw new CqnSyntaxException(
"A structured element can only be compared with a structured value or with NULL");
}
CdsElement struct = elementOf(rowType, ref);
Stream> suffixes = structureOf(struct, false);
Stream preds = suffixes.map(suffix -> deepComparison(ref.segments(), suffix, op, val));
return (Predicate) preds.collect(connector);
}
}
private static CqnSyntaxException badOperator(Operator op) {
String message = MessageFormat.format("Unsupported operator {0} for comparing structures", op);
return new CqnSyntaxException(message);
}
private static Predicate deepComparison(List extends Segment> prefix, List suffix, Operator op,
Map data) {
suffix = suffix.subList(1, suffix.size());
List segments = new ArrayList<>(prefix.size() + suffix.size());
segments.addAll(prefix);
suffix.forEach(id -> segments.add(CQL.refSegment(id)));
ElementRef> ref = CQL.get(segments);
Object v = DataUtils.getPath(data, suffix.toArray(new String[suffix.size()]));
CqnValue val = v == null ? CqnNull.NULL : CQL.val(v);
return CQL.comparison(ref, op, val);
}
private static CdsElement elementOf(CdsStructuredType rowType, CqnElementRef asRef) {
return CdsModelUtils.element(rowType, asRef);
}
private static boolean isStructured(CdsStructuredType rowType, CqnElementRef asRef) {
CdsElement element = CdsModelUtils.element(rowType, asRef);
return element.getType().isStructured();
}
@SuppressWarnings("unchecked")
public static T resolveKeyPlaceholder(CdsStructuredType rowType,
S statement) {
return (T) CQL.copy(statement, new ReplaceKeyPlaceholderModifier(rowType));
}
public static void resolveKeyPlaceholder(CdsStructuredType rowType, FilterableStatement, ?> statement) {
statement.where().map(w -> CQL.copy(w, new ReplaceKeyPlaceholderModifier(rowType))).ifPresent(statement::where);
}
private static final class ReplaceKeyPlaceholderModifier implements LeanModifier {
private final CdsStructuredType rowType;
private ReplaceKeyPlaceholderModifier(CdsStructuredType rowType) {
this.rowType = rowType;
}
@Override
public CqnValue ref(CqnElementRef ref) {
if (isKeyPlaceholder(ref)) {
Iterator keyIter = rowType.keyElements().iterator();
CdsElement key = keyIter.next();
if (keyIter.hasNext()) {
throw new CqnValidationException(
"The entity " + rowType + " must have a single key to be filtered with 'byId'");
}
return CQL.get(key.getName());
}
return ref;
}
private boolean isKeyPlaceholder(CqnElementRef ref) {
return CqnElementRef.$KEY.equals(ref.firstSegment());
}
}
public static void moveKeyValuesToWhere(CdsStructuredType rowType, CqnUpdate update, boolean useParameters) {
CqnPredicate filter = extractTargetFilter(rowType, update, useParameters);
((Update>) update).where(filter);
}
public static CqnSelect countAllQuery(CqnUpdate update) {
Select> select = countAll(update);
update.where().ifPresent(select::where);
return select;
}
public static CqnSelect inlineCountQuery(CqnSelect select) {
CqnStructuredTypeRef ref = select.ref();
Select> inner = Select.from(ref);
if (select.isDistinct()) {
inner.columns(select.items());
inner.distinct();
} else {
inner.columns(CQL.plain("1").as("one"));
}
select.where().ifPresent(inner::where);
select.search().ifPresent(inner::search);
inner.groupBy(select.groupBy());
select.having().ifPresent(inner::having);
return Select.from(inner).columns(Count.ALL);
}
public static Select> countAll(CqnStatement statement) {
CqnStructuredTypeRef ref = statement.ref();
Select> select = Select.from(ref);
select.columns(Count.ALL);
return select;
}
public interface NegationResolver extends LeanModifier {
@Override
default Predicate negation(Predicate p) {
return p.not();
}
}
private static class Simplifier implements NegationResolver, LogicalOpertionsSimplifier {
// empty
}
public interface Count {
CqnSelectListItem ALL = CQL.func("COUNT", CQL.plain("*")).type(Long.class).as("count");
long getCount();
}
public static Predicate simplifyPredicate(CqnPredicate pred) {
return ExpressionVisitor.copy(pred, new Simplifier() {
});
}
public interface LogicalOpertionsSimplifier extends LeanModifier {
@Override
default Predicate connective(CqnConnectivePredicate.Operator op, List predicates) {
if (op == CqnConnectivePredicate.Operator.AND) {
return predicates.stream().collect(Conjunction._and());
} else {
return predicates.stream().collect(Disjunction._or());
}
}
}
public static List resolveStar(List items, Collection excluding,
CdsStructuredType rowType, boolean includeAssocs) {
if (containsSelectStar(items)) {
// TODO make this DOC-Store agnostic
if (has$jsonElement(rowType)) {
return $JSON_LIST;
}
List resolved = elementsOf(rowType, includeAssocs).map(CqnStatementUtils::elementRef)
.filter(n -> !excluding.contains(n.displayName())).collect(toList());
items.stream().filter(sli -> !sli.isStar()).forEach(resolved::add);
return resolved;
}
return items;
}
static CqnSelectListValue elementRef(List ids) {
String alias = ids.stream().collect(joining("."));
List segments = ids.stream().map(RefSegmentImpl::refSegment).collect(toList());
return SelectListValueBuilder.select(ElementRefImpl.elementRef(segments, alias, null));
}
static Stream> elementsOf(CdsStructuredType rowType, boolean includeAssocs) {
return rowType.elements()
.filter(e -> !e.isVirtual()
&& (!e.getType().isAssociation() || includeAssocs && CdsModelUtils.managedToOne(e.getType())))
.flatMap(e -> structureOf(e, includeAssocs));
}
static Stream> structureOf(CdsElement e, boolean includeAssocs) {
CdsType type = e.getType();
String name = e.getName();
if (type.isStructured()) {
CdsStructuredType struct = type.as(CdsStructuredType.class);
return elementsOf(struct, includeAssocs).map(ids -> prefix(name, ids));
}
return Stream.of(asList(name));
}
private static List prefix(String prefix, List suffix) {
List list = new ArrayList<>(1 + suffix.size());
list.add(prefix);
list.addAll(suffix);
return list;
}
private static boolean has$jsonElement(CdsStructuredType rowType) {
return rowType.findElement($JSON).isPresent();
}
public static CqnSelect resolveStar(CqnSelect select, CdsStructuredType rowType) {
return resolveStar(select, rowType, false);
}
public static CqnSelect resolveStar(CqnSelect select, CdsStructuredType rowType, boolean includeAssocs) {
List items = select.items();
List resolved = resolveStar(items, select.excluding(), rowType, includeAssocs);
if (items != resolved) {
return SelectBuilder.copy(select).columns(resolved);
}
return select;
}
public static void resolveStar(Select> select, CdsStructuredType rowType, boolean includeAssocs) {
List items = select.items();
List resolved = resolveStar(items, select.excluding(), rowType, includeAssocs);
if (items != resolved) {
select.columns(resolved);
}
}
public static boolean isSelectStar(List columns) {
if (columns == null || columns.isEmpty()) {
return true;
}
return columns.size() == 1 && columns.get(0).isStar();
}
public static CdsStructuredType targetType(CdsModel model, CqnSelect select) {
return rowType(model, select.from());
}
public static CdsStructuredType rowType(CdsModel model, CqnSource source) {
if (source.isSelect()) {
return rowType(model, source.asSelect());
}
CqnStructuredTypeRef ref = source.asRef();
return CdsModelUtils.entity(model, ref);
}
public static CdsStructuredType rowType(CdsModel model, CqnSelect query) {
CdsStructuredType targetType;
CqnSource source = query.from();
if (source.isRef()) {
if (isSelectStar(query.items()) && query.excluding().isEmpty()) {
return entity(model, source.asRef());
}
targetType = entity(model, source.asRef());
} else if (source.isSelect()) {
targetType = rowType(model, source.asSelect());
} else {
throw new UnsupportedOperationException("Joins are not supported");
}
return rowType(targetType, query.items(), query.excluding()).build();
}
private static CdsStructuredTypeBuilder> rowType(CdsStructuredType targetType, List items,
Collection excluding) {
CdsStructuredTypeBuilder> structBuilder = new CdsStructuredTypeBuilder<>(emptyList(), "", "", CdsKind.ENTITY,
null);
items.stream().filter(i -> !i.isValue() || !excluding.contains(i.asValue().displayName()))
.forEach(i -> addElements(structBuilder, targetType, i));
return structBuilder;
}
private static void addElements(CdsStructuredTypeBuilder> structBuilder, CdsStructuredType type,
CqnSelectListItem sli) {
if (sli.isStar()) {
type.concreteNonAssociationElements().map(CdsElementBuilder::copy)
.forEach(eb -> structBuilder.addElement(eb));
type.associations().filter(a -> !isReverseAssociation(a)).map(CdsElementBuilder::copy)
.forEach(eb -> structBuilder.addElement(eb));
} else if (sli.isValue()) {
CqnSelectListValue slv = sli.asValue();
String displayName = slv.displayName();
addElement(structBuilder, type, slv, displayName);
} else if (sli.isSelectList()) {
addSelectListElements(structBuilder, type, sli.asSelectList());
} else {
throw new UnsupportedOperationException("Unsupported item type " + sli);
}
}
private static void addElement(CdsStructuredTypeBuilder> structBuilder, CdsStructuredType root,
CqnSelectListValue slv, String displayName) {
CqnValue val = slv.value();
CdsElementBuilder> builder;
if (val.isRef()) {
CdsElement element = element(root, val.asRef());
builder = CdsElementBuilder.copy(element);
} else { // non-ref values
CdsSimpleType type = getCdsType(root, val).map(CdsSimpleTypeBuilder::simpleType).orElse(undefinedType());
builder = element(displayName).type(type);
}
Optional alias = slv.alias();
alias.ifPresent(a -> builder.annotation(annotation(ANNOTATION_PERSISTENCE_NAME, a)));
addElement(structBuilder, builder, displayName);
}
private static void addElement(CdsStructuredTypeBuilder> outer, CdsElementBuilder> element,
String displayName) {
int i = displayName.indexOf('.');
if (i == -1) {
element.name(displayName);
outer.addElement(element);
} else {
String prefix = displayName.substring(0, i);
CdsElementBuilder> structuredElement = outer.putStructuredElementIfAbsent(prefix,
CqnStatementUtils::structuredElement);
String suffix = displayName.substring(i + 1);
CdsStructuredTypeBuilder> inner = (CdsStructuredTypeBuilder>) structuredElement.getTypeBuilder();
addElement(inner, element, suffix);
}
}
private static CdsElementBuilder> structuredElement(String name) {
return new CdsElementBuilder<>(emptyList(), name,
new CdsStructuredTypeBuilder<>(emptyList(), "", "", CdsKind.TYPE, null), false, false, false, false,
null, null);
}
private static void addSelectListElements(CdsStructuredTypeBuilder> structBuilder, CdsStructuredType root,
CqnSelectList selectList) {
List extends Segment> refSegments = selectList.ref().segments();
if (refSegments.get(0).id().equals("*")) {
// TODO support expand all associations
// TODO docstore: is this related to expand all on docstore?
return;
}
CdsStructuredType target = target(root, refSegments);
if (selectList.isInline()) {
selectList.items().stream().forEach(i -> addElements(structBuilder, target, i));
} else if (selectList.isExpand()) {
CdsStructuredTypeBuilder> typeBuilder = rowType(target, selectList.items(), emptyList());
CdsType type = isToOnePath(root, refSegments) ? typeBuilder.build() : arrayedType(typeBuilder);
CdsElementBuilder> eb = element(selectList.displayName()).type(type);
structBuilder.addElement(eb);
} else {
throw new UnsupportedOperationException("Unsupported select list type: " + selectList);
}
}
private static boolean containsSelectStar(List items) {
if (items == null || items.isEmpty()) {
return true;
}
return items.stream().anyMatch(CqnSelectListItem::isStar);
}
private static List resolveVirtualElements(List items,
CdsStructuredType target) {
boolean changed = false;
List slis = new ArrayList<>(items.size());
for (CqnSelectListItem sli : items) {
if (sli.isRef()) {
CqnSelectListValue slv = sli.asValue();
CdsElement element = element(target, slv.value().asRef());
if (element.isVirtual() && element.getType().isSimple()) {
changed = true;
Optional
© 2015 - 2025 Weber Informatics LLC | Privacy Policy