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-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 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 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 defaultValue = element.defaultValue();
					if (defaultValue.isPresent()) {
						sli = val(defaultValue.get()).as(slv.displayName());
					} else {
						continue;
					}
				}
			}
			slis.add(sli);
		}

		return changed ? slis : items;
	}

	public static void resolveVirtualElements(Select select, CdsStructuredType root) {
		List items = select.items();
		List slis = resolveVirtualElements(items, root);
		if (items != slis) {
			select.columns(slis);
		}
	}

	public static void unfoldInline(Select select, CdsStructuredType root) {
		AllElementsResolver resolver = new AllElementsResolver(root);
		List unfolded = select.items().stream()
				.flatMap(c -> c.unfold(resolver::mapPrefixToAllElements)).collect(Collectors.toList());

		select.columns(unfolded);
	}

	public static void simplify(CdsStructuredType rowType, Select select) {
		select.where().ifPresent(w -> {
			CqnPredicate simplified = CQL.copy(w, new Simplifier() {
				@Override
				public Predicate comparison(Value lhs, CqnComparisonPredicate.Operator op, Value rhs) {
					if (rhs.isNullValue() && neverNull(lhs) || lhs.isNullValue() && neverNull(rhs)) {
						if (op == Operator.IS) {
							return FALSE;
						} else if (op == Operator.IS_NOT) {
							return TRUE;
						}
					}

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

				private boolean neverNull(CqnValue val) {
					if (val.isLiteral()) {
						return true;
					}
					if (!val.isRef()) {
						return false;
					}
					CqnElementRef ref = val.asRef();
					if (ref.size() > 1) {
						return false;
					}
					CdsElement element = element(rowType, ref);

					return element.isNotNull() || element.isKey();
				}
			});
			select.where(simplified);
		});

		if (!select.orderBy().isEmpty()) {
			List sortSpecs = new ArrayList<>(select.orderBy());
			sortSpecs.removeIf(sp -> sp.value().isLiteral());
			if (sortSpecs.size() != select.orderBy().size()) {
				select.orderBy(sortSpecs);
			}
		}
	}

	public static Select resolveExpands(Select select, CdsStructuredType target, boolean assocsInStar) {
		resolveExpandAllAssociations(select, target);
		return copy(select, new ExpandToOneResolver(target, null, assocsInStar)); // TODO don't copy
	}

	private static void resolveExpandAllAssociations(Select select, CdsStructuredType type) {
		boolean resolved = false;
		List items = new ArrayList<>(select.items());
		for (Iterator iter = items.iterator(); iter.hasNext();) {
			CqnSelectListItem sli = iter.next();
			if (sli.isExpand() && sli.asExpand().ref().firstSegment().equals("*")) {
				resolved = true;
				iter.remove();
				type.associations().forEach(a -> items.add(CQL.to(a.getName()).expand()));
				break;
			}
		}
		if (resolved) {
			select.columns(items);
		}
	}

	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 (assoc.equals("*")) {
				return false;
			}
			CdsType type = rt.getElement(assoc).getType();
			if (type.isAssociation() && !CdsModelUtils.isSingleValued(type)) {
				return false;
			}
			if (type.isStructured()) {
				rt = type.as(CdsStructuredType.class);
			} else {
				rt = rt.getTargetOf(assoc);
			}
		}
		return true;
	}

	public static boolean isOneToManyPath(CdsStructuredType rowType, List segments) {
		CdsStructuredType rt = rowType;
		boolean toMany = false;
		for (Segment seg : segments) {
			String assoc = seg.id();
			if (assoc.equals("*") || CdsModelUtils.isManyTo(rt.getElement(assoc).getType())) {
				return false;
			}
			if (!toMany && !CdsModelUtils.isSingleValued(rt.getElement(assoc).getType())) {
				toMany = true;
			}
			rt = rt.getTargetOf(assoc);
		}
		return toMany;
	}

	private static final class ExpandToOneResolver implements LeanModifier {
		private final CdsStructuredType rowType;
		private final String prefix;
		private final boolean assocsInStar;

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

		@Override
		public CqnSelectListItem expand(CqnExpand expand) {
			CqnStructuredTypeRef ref = expand.ref();
			CdsElement element = element(rowType, ref.segments());
			String expandMethod = element.getAnnotationValue(ANNOTATION_JAVA_EXPAND + ".using", "");
			if (expandMethod.equals(EXPAND_USING_PARENT_KEYS)) {
				logger.debug("Expand to-one {} using parent-keys (annotation)", ref);
				return expand;
			}
			List segments = ref.segments();
			if (!isToOnePath(rowType, segments)) {
				return expand;
			}
			CdsElement assoc = CdsModelUtils.element(rowType, segments);
			CdsStructuredType target = assoc.getType().as(CdsAssociationType.class).getTarget();
			List resolved = resolveStar(expand.items(), emptyList(), target, assocsInStar);
			if (!selectsRefsOnly(target, resolved)) {
				if (logger.isDebugEnabled()) {
					logger.debug(
							"Cannot optimize to-one {} expand because there are no element references on the expand item list",
							ref);
				}
				return expand;
			}
			return resolveToOneExpand(assoc, expand.ref(), resolved, expand.alias().orElse(null));
		}

		private static boolean isExpandAll(CqnExpand exp) {
			return exp.ref().firstSegment().equals("*");
		}

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

		private boolean isConcreteRef(CdsStructuredType target, CqnSelectListItem i) {
			if (i.isExpand()) {
				CqnExpand exp = i.asExpand();
				if (isExpandAll(exp)) {
					return false;
				}
				CdsStructuredType expTarget = CdsModelUtils.target(target, exp.ref().segments());
				List items = resolveStar(exp.items(), emptyList(), expTarget, assocsInStar);
				return selectsRefsOnly(expTarget, items);
			}
			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()) {
					return !element.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, CqnStructuredTypeRef ref,
				List items, String expandAlias) {
			CdsStructuredType target = assoc.getType().as(CdsAssociationType.class).getTarget();
			Set keyNames = CdsModelUtils.keyNames(target);
			String refPath = expandAlias != null ? expandAlias : ref.path();
			items = resolveStar(items, emptyList(), target, true);
			items = resolveVirtualElements(items, target);
			AtomicBoolean containsKey = new AtomicBoolean(false);
			items = ExpressionVisitor.copy(items, new LeanModifier() {
				@Override
				public CqnSelectListItem selectListItem(Value value, String alias) {
					if (value.isExpression()) {
						value = ExpressionVisitor.copy(value, new LeanModifier() {
							@Override
							public CqnValue ref(CqnElementRef ref) {
								List segments = new ArrayList<>(ref.segments());
								segments.addAll(0, CQL.get(prefix(refPath)).segments());

								return CQL.get(segments);
							}
						});
					} else if (!containsKey.get() && value.isRef() && keyNames.contains(value.asRef().lastSegment())) {
						containsKey.set(true);
					}
					String expandName = expandAlias != null ? expandAlias : ref.lastSegment();
					String elementName = alias != null ? alias : value.asRef().lastSegment();
					return value.as(prefix(expandName) + "." + elementName);
				}

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

				@Override
				public CqnSelectListItem expand(CqnExpand expand) {
					CqnStructuredTypeRef expRef = expand.ref();
					String alias = prefix(refPath) + "." + expand.alias().orElse(expRef.lastSegment());
					expand = CQL.to(expand.ref().segments()).as(alias).expand(expand.items()).orderBy(expand.orderBy())
							.limit(expand.top(), expand.skip());
					if (isToOnePath(target, expRef.segments())) {
						return new ExpandToOneResolver(target, null, assocsInStar).expand(expand);
					}
					return expand;
				}
			});
			if (!containsKey.get() && !keyNames.isEmpty()) {
				String alias = expandAlias != null ? expandAlias : ref.lastSegment();
				items.add(0, CQL.get(keyNames.iterator().next()).as(alias + ".?"));
			}
			return to(ref.segments()).inline(items);
		}
	}

	public static boolean isMediaType(CdsStructuredType type, CqnSelectListItem sli) {
		if (!sli.isRef()) {
			return false;
		}
		CdsElement element = element(type, sli.asRef());

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

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

	public static CqnPredicate linkKeysToOuterQuery(CdsStructuredType target) {
		return target.concreteNonAssociationElements()
				.filter(e -> e.isKey() && !e.getName().equals(DraftAdapter.IS_ACTIVE_ENTITY))
				.sorted(comparing(CdsElement::getName)).map(e -> outerToInner(e.getName())).collect(and());
	}

	private static CqnPredicate outerToInner(String name) {
		CqnElementRef outer = ElementRefImpl.elementRef(CqnExistsSubquery.OUTER, name);
		CqnElementRef inner = ElementRefImpl.elementRef(name);
		return ComparisonPredicate.eq(outer, inner);
	}

	public static void removeVirtualElements(Select select, CdsStructuredType rowType) {
		select.columns(select.items().stream().filter(i -> !isVirtual(i, rowType)).collect(toList()));
		select.orderBy(select.orderBy().stream().filter(o -> !isVirtual(o.value(), rowType)).collect(toList()));
		select.groupBy(select.groupBy().stream().filter(i -> !isVirtual(i, rowType)).collect(toList()));
	}

	private static boolean isVirtual(CqnSelectListItem i, CdsStructuredType rowType) {
		return i.isValue() && isVirtual(i.asValue().value(), rowType);
	}

	private static boolean isVirtual(CqnValue value, CdsStructuredType rowType) {
		return value.isRef() && element(rowType, value.asRef()).isVirtual();
	}

	public static Stream toManyExpands(CdsStructuredType type, List items) {
		return items.stream().filter(CqnSelectListItem::isExpand).map(CqnSelectListItem::asExpand)
				.filter(e -> !isToOnePath(type, e.ref().segments()));
	}

	public static CqnStructuredTypeRef targetRef(CqnSelect select) {
		if (select.from().isRef()) {
			return whereToInfixFilter(select.ref(), select.where()).asRef();
		}
		return null;
	}

	public static StructuredType targetRef(CqnUpdate update) {
		return whereToInfixFilter(update.ref(), update.where());
	}

	private static StructuredType whereToInfixFilter(CqnStructuredTypeRef ref, Optional where) {
		StructuredType reference = CQL.to(RefSegmentBuilder.copy(ref.segments()));
		where.ifPresent(w -> {
			Optional targetFilter = ref.targetSegment().filter();
			reference.filter(CQL.and(targetFilter.orElse(CqnBoolLiteral.TRUE), w));
		});
		return reference;
	}

	public static boolean containsPathExpression(Optional pred) {
		AtomicBoolean path = new AtomicBoolean();
		pred.ifPresent(p -> p.accept(new CqnVisitor() {
			@Override
			public void visit(CqnElementRef ref) {
				if (ref.size() > 1) {
					path.set(true);
				}
			}
		}));
		return path.get();
	}

	private static boolean containsParameters(Optional pred) {
		return containsParameters(pred.orElse(null));
	}

	private static boolean containsParameters(CqnPredicate pred) {
		if (pred == null) {
			return false;
		}
		AtomicBoolean param = new AtomicBoolean();
		pred.accept(new CqnVisitor() {
			@Override
			public void visit(CqnParameter p) {
				param.set(true);
			}
		});
		return param.get();
	}

	public static boolean hasInfixFilter(CqnReference ref) {
		for (Segment segment : ref.segments()) {
			if (segment.filter().isPresent()) {
				return true;
			}
		}
		return false;
	}

	public static  S anonymizeStatement(S statement) {
		S statementCopy = copy(statement, new LeanModifier() {
			@Override
			public CqnValue literal(CqnLiteral literal) {
				return literal.isBoolean() ? literal : ANONYM_MARKER;
			}

			@Override
			public Predicate exists(Select subQuery) {
				return CQL.exists(anonymizeStatement(subQuery));
			}

			@Override
			public CqnStructuredTypeRef ref(CqnStructuredTypeRef ref) {
				List segments = new ArrayList<>(ref.size());
				ref.segments().forEach(seg -> segments.add(RefSegmentImpl.refSegment(seg.id(), seg.filter().map(f -> {
					if (f instanceof CqnExistsSubquery) {
						CqnSelect subquery = ((CqnExistsSubquery) f).subquery();
						return new ExistsSubquery(anonymizeStatement(subquery));
					}
					return f;
				}).orElse(null))));
				StructuredTypeRef typeRef = typeRef(segments);
				ref.alias().ifPresent(typeRef::as);
				return typeRef;
			}
		});

		// clear all entries
		if (statementCopy instanceof CqnXsert) {
			((CqnXsert) statementCopy).entries().clear();
		} else if (statementCopy instanceof CqnUpdate) {
			((CqnUpdate) statementCopy).entries().clear();
		}

		return statementCopy;

	}

	public static CqnSelect batchSelect(CqnSelect select, List> valueSets) {
		boolean paramsInWhere = containsParameters(select.where());
		CqnSelect batchSelect = CQL.copy(select, new LeanModifier() {
			@Override
			public CqnStructuredTypeRef ref(CqnStructuredTypeRef ref) {
				AtomicBoolean paramsReplaced = new AtomicBoolean();
				List segments = new ArrayList<>(ref.size());
				ref.segments().forEach(seg -> segments.add(RefSegmentImpl.refSegment(seg.id(), seg.filter().map(f -> {
					if (containsParameters(f)) {
						if (paramsInWhere || paramsReplaced.getAndSet(true)) {
							throw new UnsupportedOperationException(
									"Batch select is only supported with parameters in one path segment filter or the where clause");
						}
						return bindValues(f, valueSets);
					}
					return f;
				}).orElse(null))));
				StructuredTypeRef typeRef = typeRef(segments);
				ref.alias().ifPresent(typeRef::as);
				return typeRef;
			}
		});
		CqnPredicate p = bindValues(select.where(), valueSets);
		((Select) batchSelect).where(p);
		return batchSelect;
	}

	public static Optional getCdsType(CdsStructuredType rowType, CqnValue value) {
		TypePropagationVisitor visitor = new TypePropagationVisitor(rowType);
		value.accept(visitor);
		String typeName = visitor.stack.pop();

		return UNDEFINED.equals(typeName) ? Optional.empty() : baseType(typeName);
	}

	@SuppressWarnings("unchecked")
	public static  S copyShallow(S statement) {
		if (statement.isSelect()) {
			return (S) SelectBuilder.copyShallow(statement.asSelect());
		} else if (statement.isUpdate()) {
			return (S) UpdateBuilder.copyShallow(statement.asUpdate());
		} else if (statement.isDelete()) {
			return (S) DeleteBuilder.copyShallow(statement.asDelete());
		}

		throw new UnsupportedOperationException("Shallow copy of " + statement + " is not supported");
	}

	private static class TypePropagationVisitor implements CqnVisitor {
		private final Stack stack = new Stack<>();
		private final CdsStructuredType rowType;

		public TypePropagationVisitor(CdsStructuredType rowType) {
			this.rowType = rowType;
		}

		// leafs

		@Override
		public void visit(CqnLiteral literal) {
			pushTypeOf(literal);
		}

		@Override
		public void visit(CqnNullValue nil) {
			pushTypeOf(nil);
		}

		@Override
		public void visit(CqnParameter param) {
			pushTypeOf(param);
		}

		@Override
		public void visit(CqnPlain plain) {
			pushTypeOf(plain);
		}

		@Override
		public void visit(CqnElementRef ref) {
			Optional typeName = ref.type();
			if (typeName.isPresent()) {
				stack.push(typeName.get());
				return;
			}

			CdsType t = element(rowType, ref).getType();
			if (t.isSimple()) {
				CdsBaseType type = t.as(CdsSimpleType.class).getType();
				if (type != null) {
					push(type.cdsName());
				} else {
					undefined();
				}
			} else {
				undefined();
			}
		}

		// nodes

		@Override
		public void visit(CqnFunc func) {
			List args = stack.pop(func.args().size());

			Optional typeName = func.type();
			if (typeName.isPresent()) {
				stack.push(typeName.get());
				return;
			}

			String lowerName = func.func().toLowerCase(Locale.US);
			switch (lowerName) {
			case "min":
			case "max":
				push(args.get(0));
				break;
			case "count":
			case "countdistinct":
				push(CdsBaseType.INT64.cdsName());
				break;
			case "tolower":
			case "toupper":
			case "substring":
				push(CdsBaseType.STRING.cdsName());
				break;
			default:
				undefined();
			}
		}

		@Override
		public void visit(CqnArithmeticExpression expr) {
			stack.pop(2);

			pushTypeOf(expr);
		}

		@Override
		public void visit(CqnArithmeticNegation neg) {
			String arg = stack.pop();

			Optional typeName = neg.type();
			if (typeName.isPresent()) {
				stack.push(typeName.get());
				return;
			}

			push(arg);
		}

		@Override
		public void visit(CqnExpression expr) {
			if (!(expr instanceof Xpr)) {
				throw new IllegalStateException("Unexpected token type: " + expr.getClass().getCanonicalName());
			}

			Xpr xpr = (Xpr) expr;
			stack.pop(xpr.length());

			pushTypeOf(expr);
		}

		private void push(String type) {
			stack.push(type);
		}

		private void pushTypeOf(CqnValue val) {
			String type = val.type().orElse(UNDEFINED);
			push(type);
		}

		private void undefined() {
			push(UNDEFINED);
		}
	}

	private static Optional baseType(String cdsTypeName) {
		CdsBaseType type = null;
		try {
			type = cdsType(cdsTypeName);
		} catch (CdsException e) {
			logger.warn("Failed to cast to {}", cdsTypeName);
		}
		return Optional.ofNullable(type);
	}
}