All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.util.CqnStatementUtils Maven / Gradle / Ivy

There is a newer version: 3.8.0
Show newest version
/*******************************************************************
 * © 2020 SAP SE or an SAP affiliate company. All rights reserved. *
 *******************************************************************/
package com.sap.cds.util;

import static com.sap.cds.impl.builder.model.LiteralImpl.literal;
import static com.sap.cds.impl.parser.token.CqnBoolLiteral.FALSE;
import static com.sap.cds.ql.CQL.copy;
import static com.sap.cds.ql.CQL.to;
import static com.sap.cds.ql.impl.SelectListValueBuilder.select;
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.simpleType;
import static com.sap.cds.reflect.impl.CdsSimpleTypeBuilder.undefinedType;
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.keyNames;
import static com.sap.cds.util.CdsModelUtils.target;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.annotations.VisibleForTesting;
import com.sap.cds.impl.builder.model.Conjunction;
import com.sap.cds.impl.builder.model.Disjunction;
import com.sap.cds.impl.builder.model.ExpressionImpl;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.ElementRef;
import com.sap.cds.ql.Expand;
import com.sap.cds.ql.Predicate;
import com.sap.cds.ql.RefSegment;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredTypeRef;
import com.sap.cds.ql.Update;
import com.sap.cds.ql.Value;
import com.sap.cds.ql.cqn.CqnComparisonPredicate;
import com.sap.cds.ql.cqn.CqnConnectivePredicate.Operator;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnLimit;
import com.sap.cds.ql.cqn.CqnModifier;
import com.sap.cds.ql.cqn.CqnPredicate;
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.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.impl.ExpressionVisitor;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.ql.impl.UpdateBuilder;
import com.sap.cds.ql.impl.XsertBuilder;
import com.sap.cds.reflect.CdsAssociationType;
import com.sap.cds.reflect.CdsElement;
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.CdsStructuredTypeBuilder;

public class CqnStatementUtils {

	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::isValue).map(CqnSelectListItem::asValue)
				.filter(i -> i.value().isRef());
	}

	public static void ensureKeysAreSelected(CdsStructuredType rowType, CqnSelect select) {
		List keys = rowType.concreteNonAssociationElements().filter(CdsElement::isKey) //
				.map(CdsElement::getName).collect(toList());
		CqnVisitor visitor = new CqnVisitor() {
			@Override
			public void visit(CqnElementRef elementRef) {
				if (!keys.isEmpty() && elementRef.segments().size() == 1) {
					keys.remove(elementRef.firstSegment());
				}
			}
		};
		select.items().forEach(c -> c.accept(visitor));
		if (!keys.isEmpty()) {
			SelectBuilder query = (SelectBuilder) select;
			keys.forEach(k -> {
				String alias = "@key_" + k;
				query.addItem(select(k).as(alias).build());
				query.addExclude(alias);
			});
		}
	}

	public static boolean containsRef(List items) {
		return items.parallelStream().anyMatch(i -> {
			if (i.isValue() && i.asValue().value().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 ExpressionImpl.join(predicates);
	}

	public static  S resolveKeyPlaceholder(CdsStructuredType rowType, S statement) {
		return CQL.copy(statement, new CqnModifier() {
			@Override
			public Value ref(ElementRef ref) {
				if (isKeyPlaceholder(ref)) {
					boolean singleKey = rowType.keyElements().count() == 1;
					if (!singleKey) {
						throw new CqnValidationException(
								"The entity " + rowType + " must have a single key to be filtered with 'byId'");
					}
					CdsElement key = rowType.keyElements().findFirst().get(); // NOSONAR
					String keyName = key.getName();
					ref.rootSegment().id(keyName);
				}
				return ref;
			}

			private boolean isKeyPlaceholder(CqnElementRef ref) {
				return ref.segments().size() == 1 && CqnElementRef.$KEY.equals(ref.firstSegment());
			}
		});
	}

	public static void moveKeyValuesToWhere(CdsStructuredType rowType, CqnUpdate update) {
		CqnPredicate filter = extractTargetFilter(rowType, update, true);
		((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);

		CqnSelect outer = Select.from(inner).columns(Count.ALL);

		return outer;
	}

	private static Select countAll(CqnStatement statement) {
		CqnStructuredTypeRef ref = statement.ref();
		Select select = Select.from(ref);
		select.columns(Count.ALL);

		return select;
	}

	public interface NegationResolver extends CqnModifier {
		default Predicate negation(Predicate p) {
			return p.not();
		}
	}

	private static class Simplifier implements NegationResolver, LogicalOpertionsSimplifier {
// empty
	}

	public interface Count {
		public static final 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 CqnModifier {
		default Predicate connective(Operator op, List predicates) {
			if (op == 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) {
		if (containsSelectStar(items)) {
			Stream elements = rowType.concreteNonAssociationElements();
			List resolved = elements.flatMap(e -> paths(e)).filter(n -> !excluding.contains(n))
					.map(n -> select(n).as(n).build()).collect(toList());

			if (items != null) {
				items.stream().filter(sli -> !sli.isStar()).forEach(resolved::add);
			}

			return resolved;
		}

		return items;
	}

	private static Stream paths(CdsElement element) {
		String name = element.getName();
		CdsType type = element.getType();
		if (type.isStructured()) {
			CdsStructuredType struct = type.as(CdsStructuredType.class);
			return struct.concreteNonAssociationElements().flatMap(e -> paths(e)).map(n -> name + "." + n);
		}

		return Stream.of(name);
	}

	public static CqnSelect resolveStar(CqnSelect select, CdsStructuredType rowType) {
		List items = select.items();
		List resolved = resolveStar(items, select.excluding(), rowType);
		if (items != resolved) {
			return SelectBuilder.copy(select).columns(resolved);
		}

		return select;
	}

	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(), "");
		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));
		} 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 = val.type().map(t -> simpleType(cdsType(t))).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.computeIfAbsent(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(), ""), false, false,
				false, false);
	}

	private static void addSelectListElements(CdsStructuredTypeBuilder structBuilder, CdsStructuredType root,
			CqnSelectList selectList) {
		List refSegments = selectList.ref().segments();
		if (refSegments.get(0).id().equals("*")) {
			// TODO support expand all associations
			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.ref().lastSegment()).type(type);
			structBuilder.addElement(eb);
		} else {
			throw new UnsupportedOperationException("Unsupported select list type: " + selectList);
		}
	}

	private static boolean containsSelectStar(List columns) {
		if (columns == null || columns.isEmpty()) {
			return true;
		}

		return columns.parallelStream().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.isValue()) {
				CqnSelectListValue slv = sli.asValue();
				CqnValue value = slv.value();
				if (value.isRef()) {
					CdsElement element = element(target, value.asRef());
					if (element.isVirtual() && element.getType().isSimple()) {
						changed = true;
						CdsSimpleType type = element.getType();
						Optional defaultValue = type.defaultValue();
						if (defaultValue.isPresent()) {
							sli = literal(defaultValue.get()).as(slv.displayName());
						} else {
							continue;
						}
					}
				}
			}
			slis.add(sli);
		}

		return changed ? slis : items;
	}

	public static CqnSelect resolveVirtualElements(CqnSelect select, CdsStructuredType root) {

		List items = select.items();
		List slis = resolveVirtualElements(items, root);

		return items == slis ? select : Select.copy(select).columns(slis);
	}

	public static CqnSelect unfoldInline(CqnSelect select, CdsStructuredType root) {
		AllElementsResolver resolver = new AllElementsResolver(root);
		Stream unfolded = select.items().stream()
				.flatMap(c -> c.unfold(resolver::mapPrefixToAllElements));

		return SelectBuilder.copy(select).columns(unfolded);
	}

	public static CqnSelect simplify(CdsStructuredType rowType, CqnSelect select) {
		if (select.where().isPresent()) {
			return CQL.copy(select, new Simplifier() {
				@Override
				public Predicate comparison(Value lhs, CqnComparisonPredicate.Operator op, Value rhs) {
					if (op == CqnComparisonPredicate.Operator.IS && lhs.isRef()) {
						CdsElement element = CdsModelUtils.element(rowType, lhs.asRef());
						if (element.isNotNull() || element.isKey()) {
							return FALSE;
						}
					}

					return CQL.comparison(lhs, op, rhs);
				}

			});
		}
		return select;

	}

	public static CqnSelect resolveToOneExpands(CqnSelect select, CdsStructuredType target) {
		return copy(select, new ExpandToOneResolver(target, null));
	}

	private static class AllElementsResolver {
		private CdsStructuredType rowType;

		AllElementsResolver(CdsStructuredType rowType) {
			this.rowType = rowType;
		}

		private Stream mapPrefixToAllElements(List prefix) {
			CdsStructuredType currentEntity = rowType;
			for (Segment seg : prefix) {
				currentEntity = currentEntity.getTargetOf(seg.id());
			}
			return currentEntity.concreteNonAssociationElements().map(CdsElement::getName);
		}
	}

	public static boolean isToOnePath(CdsStructuredType rowType, List segments) {
		CdsStructuredType rt = rowType;
		for (Segment seg : segments) {
			String assoc = seg.id();
			if (!CdsModelUtils.isSingleValued(rt.getElement(assoc).getType())) {
				return false;
			}
			rt = rt.getTargetOf(assoc);
		}
		return true;
	}

	private static final class ExpandToOneResolver implements CqnModifier {
		private final CdsStructuredType rowType;
		private final String prefix;

		private ExpandToOneResolver(CdsStructuredType rowType, String prefix) {
			this.rowType = rowType;
			this.prefix = prefix;
		}

		@Override
		public CqnSelectListItem expand(StructuredTypeRef ref, List items,
				List orderBy, CqnLimit limit) {
			if (!"*".equals(ref.firstSegment()) && isToOnePath(rowType, ref.segments())) {
				CdsElement assoc = CdsModelUtils.element(rowType, ref.segments());
				CdsStructuredType target = assoc.getType().as(CdsAssociationType.class).getTarget();
				if (selectsRefsOnly(target, items)) {
					return resolveToOneExpand(assoc, ref, items);
				}
			}
			return CqnModifier.super.expand(ref, items, orderBy, limit);
		}

		private boolean selectsRefsOnly(CdsStructuredType target, List items) {
			return items.stream().allMatch(i -> isConcreteRef(target, i));
		}

		private boolean isConcreteRef(CdsStructuredType target, CqnSelectListItem i) {
			if (!i.isValue()) {
				return false;
			}

			CqnValue value = i.asValue().value();
			if (value.isRef()) {
				CdsElement element = element(target, value.asRef());
				if (element.isVirtual() && element.getType().isSimple()) {
					CdsSimpleType type = element.getType();
					return !type.defaultValue().isPresent();
				}

				return true;
			}

			return usesRef(value);
		}

		private boolean usesRef(CqnValue value) {
			AtomicBoolean usesRef = new AtomicBoolean(false);

			value.accept(new CqnVisitor() {
				@Override
				public void visit(CqnElementRef elementRef) {
					usesRef.set(true);
				}
			});

			return usesRef.get();
		}

		private CqnSelectListItem resolveToOneExpand(CdsElement assoc, StructuredTypeRef ref,
				List items) {
			CdsStructuredType target = assoc.getType().as(CdsAssociationType.class).getTarget();
			String refPath = ref.segments().stream().map(RefSegment::id).collect(Collectors.joining("."));
			items = resolveStar(items, emptyList(), target);
			items = resolveVirtualElements(items, target);
			items = ExpressionVisitor.copy(items, new CqnModifier() {
				@Override
				public CqnSelectListItem selectListItem(Value value, String alias) {
					if (value.isExpression()) {
						value = ExpressionVisitor.copy(value, new CqnModifier() {
							@Override
							public Value ref(ElementRef ref) {
								List segments = new ArrayList<>(ref.segments());
								segments.addAll(0, CQL.get(prefix(refPath)).segments());

								return CQL.get(segments);
							}
						});
					}
					String name = alias != null ? alias : value.asRef().lastSegment();
					return value.as(prefix(assoc.getName()) + "." + name);
				}

				private String prefix(String name) {
					return prefix != null ? prefix + "." + name : name;
				}

				@Override
				public CqnSelectListItem expand(StructuredTypeRef ref, List items,
						List orderBy, CqnLimit limit) {
					return new ExpandToOneResolver(target, assoc.getName()).expand(ref, items, orderBy, limit);
				}
			});

			return to(ref.segments()).inline(items);
		}

	}

	public static boolean isMediaType(CdsStructuredType type, CqnSelectListItem sli) {
		if (!sli.isValue()) {
			return false;
		}
		CqnValue value = sli.asValue().value();
		if (!value.isRef()) {
			return false;
		}
		CqnElementRef ref = value.asRef();
		CdsElement element = CdsModelUtils.element(type, ref);

		return element.findAnnotation("Core.MediaType").isPresent();
	}

	@VisibleForTesting
	static  List> getEntries(T s) {
		if (s.isInsert()) {
			return s.asInsert().entries();
		} else if (s.isUpdate()) {
			return s.asUpdate().entries();
		} else if (s.isUpsert()) {
			return s.asUpsert().entries();
		}
		return Collections.emptyList();
	}

	static  void setEntries(T s, List> entries) {
		if (s instanceof XsertBuilder) {
			((XsertBuilder) s).setEntries(entries);
		} else if (s instanceof UpdateBuilder) {
			((UpdateBuilder) s).entries(entries);
		}
	}
}