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

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

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

import static com.sap.cds.impl.builder.model.ElementRefImpl.elementRef;
import static com.sap.cds.ql.cqn.CqnExistsSubquery.OUTER;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toList;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.sap.cds.Result;
import com.sap.cds.impl.ResultImpl;
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.StructuredType;
import com.sap.cds.ql.StructuredTypeRef;
import com.sap.cds.ql.Value;
import com.sap.cds.ql.cqn.CqnAnalyzer;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnExpand;
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.CqnSelectListValue;
import com.sap.cds.ql.cqn.CqnSortSpecification;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.ql.cqn.ResolvedSegment;
import com.sap.cds.ql.impl.ExpressionVisitor;
import com.sap.cds.ql.impl.LeanModifier;
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;

/**
 * Stateful implementation of projection resolver. Keeps track of statement that
 * was attempted to be resolved.
 */
public class ProjectionResolver {

	private static final String UNKNOWN = "$unknown$";

	private static final BiPredicate DEFAULT_STOP_CONDITION = (prev, stmnt) -> prev == stmnt;

	private final CdsModel model;
	private final CqnAnalyzer analyzer;
	private final Set stopConditions = new HashSet<>(singleton(DEFAULT_STOP_CONDITION));
	private final Deque projectionStack = new ArrayDeque<>();

	private T current;
	private T previous;

	private ProjectionResolver(CdsModel model, T statement) {
		this.model = model;
		this.analyzer = CqnAnalyzer.create(model);
		this.current = statement;
	}

	public static  ProjectionResolver create(CdsModel model, T statement) {
		return new ProjectionResolver<>(model, statement);
	}

	/**
	 * Adds the {@code Condition} on which the projection is considered resolved
	 *
	 * @param resolvedCondition the {@code Condition} on which resolvement stops
	 * @return the {@code ProjectionResolver instance}
	 */
	public ProjectionResolver condition(BiPredicate resolvedCondition) {
		stopConditions.add(resolvedCondition);
		return this;
	}

	/**
	 * Adds the {@code Condition} on which the projection is considered resolved
	 *
	 * @param resolvedCondition the {@code Condition} on which resolvement stops
	 * @return the {@code ProjectionResolver instance}
	 */
	public ProjectionResolver condition(TriPredicate resolvedCondition) {
		stopConditions.add(resolvedCondition);
		return this;
	}

	/**
	 * Resolves the statement against the views defined by the statements entity
	 * path and returns the {@code ProjectionResolver} object containing it. The
	 * statement is resolved until the {@code Condition} is not met.
	 *
	 * If the statement can't be resolved, the original statement is returned.
	 *
	 * @return resolver object, containing the resolved statement, or the original
	 *         one, if the projection could not be resolved.
	 */
	public ProjectionResolver resolveAll() {
		while (!stopResolvement()) {
			resolve();
		}
		return this;
	}

	private boolean stopResolvement() {
		for (Condition condition : stopConditions) {
			boolean test = false;
			if (condition.testNext()) {
				CqnStatement next = ProjectionResolver.create(model, current).resolve().getResolvedStatement();
				test = ((TriPredicate) condition).test(previous, current, next);
			} else {
				test = ((BiPredicate) condition).test(previous, current);
			}
			if (test) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Resolves any aliases from the statement to their original name. If aliases
	 * are resolved the original projection can be restored by using
	 * {@link #transform(List)} or {@link #transform(Result)}.
	 *
	 * @return resolver object
	 */
	public ProjectionResolver resolveAliases() {
		T resolved = removeAliases(current);
		// only set previous if alias removal wasn't a noop
		if (resolved != current) {
			finishResolvementStep(resolved);
		}
		return this;
	}

	/**
	 * Prepare select statement for further transformations.
	 *
	 * @param statement to be preprocessed
	 */
	private T removeAliases(T statement) {
		// project aliases in original statement
		if (statement.isSelect()) {
			// remove alias in select
			CdsEntity target = analyzer.analyze(statement.ref()).targetEntity();
			T selectWithoutAlias = CQL.copy(statement, new AliasRemover(target, null));

			// resolve aliases used in select
			Map aliasMap = calculateAliasMap(statement.asSelect());
			projectionStack.push(new Projection(null, statement.asSelect()));
			return CQL.copy(selectWithoutAlias, new AliasModifier(null, aliasMap, null));
		}
		return statement;
	}

	/**
	 * Resolves the current statement against its next projection layer.
	 *
	 * It resolves projections in all refs contained in the statement.
	 *
	 * If the statement can't be resolved, the original statement is returned.
	 *
	 * @return {@code ProjectionResolver} containing the resolved statement, or the
	 *         original one, if the projection could not be resolved.
	 */
	public ProjectionResolver resolve() {
		try {
			T resolved = previous == null ? removeAliases(current) : current;
			CdsEntity root = analyzer.analyze(resolved.ref()).rootEntity();
			if (root.query().isPresent() && isSupportedProjection(root, root.query().get())) {
				RefModifier refModifier = new RefModifier();
				int oldNumProjections = projectionStack.size();
				resolved = CQL.copy(resolved, refModifier);
				// we need to resolve the newly added projections
				Iterator iter = projectionStack.descendingIterator();
				for (int i = 0; i < oldNumProjections; ++i)
					iter.next();
				while (iter.hasNext()) {
					Projection projection = iter.next();
					// resolve selection set based on projection
					if (resolved.isSelect()) {
						resolved = CQL.copy(resolved,
								new SelectionModifier(projection.getEntity(), projection.getQuery()));
					}
					// resolve aliases in data
					// we need to adapt data also in case of a select star to remove draft fields
					if (resolved.isInsert() || resolved.isUpsert() || resolved.isUpdate()) {
						new DataAliasResolver(true).resolve(projection.getEntity(),
								CqnStatementUtils.getEntries(resolved),
								new Projection(projection.getEntity(), projection.getQuery()));
					}
					// if projection is selecting all elements no need to adapt
					if (CqnStatementUtils.isSelectStar(projection.getQuery().items())) {
						continue;
					}
					// adapt all aliases in references
					resolved = CQL.copy(resolved, new ExtendedAliasModifier(projection.getEntity(),
							calculateAliasMap(projection.getQuery()), projection));
				}
				finishResolvementStep(resolved);
				return this;
			}
		} catch (UnsupportedProjectionException e) {
			// nothing to do, finish with below return
		}

		if (previous == null) {
			projectionStack.clear();
		}
		finishResolvementStep(current);
		return this;
	}

	private void finishResolvementStep(T resolved) {
		this.previous = this.current;
		this.current = resolved;
	}

	/**
	 * Evaluates if the given projection is supported for resolvement.
	 *
	 * @param target the projection entity, which is created / defined by the
	 *               projection
	 * @param query  the projection query
	 * @return true, if the projection is supported for resolvement, false
	 *         otherwise.
	 */
	private boolean isSupportedProjection(CdsEntity target, CqnSelect query) {
		List items = query.items();
		return (items.isEmpty() || containsStar(items) || hasConcreteElement(target, items))
				&& query.groupBy().isEmpty() && !query.having().isPresent() && !query.where().isPresent()
				&& !query.from().isJoin() && !query.isDistinct() && !isTemporal(target);
	}

	private boolean containsStar(List items) {
		return items.stream().anyMatch(CqnSelectListItem::isStar);
	}

	private boolean hasConcreteElement(CdsEntity target, List items) {
		return slvs(items).anyMatch(slv -> slv.value().isRef() && isConcreteElementOf(target, slv));
	}

	private Stream slvs(List items) {
		return items.stream().flatMap(CqnSelectListItem::ofValue);
	}

	private boolean isConcreteElementOf(final CdsEntity entity, CqnSelectListValue slv) {
		return entity.findElement(slv.displayName()).map(e -> !e.isVirtual()).orElse(true);
	}

	private boolean isTemporal(CdsEntity entity) {
		// TODO Support cds.valid.from & cds.valid.to annotation conditions in a view
		return !current.isInsert() && entity.elements().anyMatch(
				e -> e.findAnnotation("cds.valid.from").isPresent() || e.findAnnotation("cds.valid.to").isPresent());
	}

	/**
	 * Resolves a StructuredTypeRef against its next projection layer of the root
	 * segment. The other segments are resolved as long as the association target of
	 * the prior segment does not match the actual target of the segment.
	 *
	 * @param ref               the ref
	 * @param currentProjection the projection of the current layer or {@code null}
	 *                          if the statement ref is resolved
	 * @return the resolved ref and the corresponding alias map
	 *
	 * @throws UnsupportedProjectionException if the ref can not be resolved
	 */
	private RefAndAliases resolveRefAndAliases(CqnStructuredTypeRef ref, Projection currentProjection) {
		StructuredType resolvedRef = null;
		CdsEntity currentRefTarget = null;
		Map previousAliasMap = new HashMap<>();
		Iterator iterator = analyzer.analyze(ref).iterator();
		while (iterator.hasNext()) {
			ResolvedSegment segment = iterator.next();
			CdsEntity segmentTarget = segment.entity();

			if (resolvedRef == null) {
				// first segment
				Optional queryOptional = segmentTarget.query();
				if (queryOptional.isPresent()) {
					currentRefTarget = analyzer.analyze(queryOptional.get()).targetEntity();
					resolvedRef = CQL.entity(currentRefTarget.getQualifiedName());
				} else {
					// first segment has no projection -> nothing to resolve
					return new RefAndAliases(ref, new HashMap<>());
				}
			} else {
				// transform association segment based on alias map from previous projection
				String id = segment.segment().id();
				String resolvedId = previousAliasMap.getOrDefault(id, id);
				resolvedRef = resolvedRef.to(resolvedId);
				try {
					currentRefTarget = currentRefTarget.getTargetOf(resolvedId);
				} catch (CdsElementNotFoundException e) {
					// mixed in association, that doesn't exist on lower projection level
					throw new UnsupportedProjectionException();
				}
			}

			CqnPredicate filter = segment.segment().filter().orElse(null);
			previousAliasMap.clear();

			// resolve all projections to get to the currentRefTarget
			Deque projectionsToResolve = getProjectionStack(segmentTarget, currentRefTarget);
			Iterator projectionIterator = projectionsToResolve.descendingIterator();
			while (projectionIterator.hasNext()) {
				Projection projection = projectionIterator.next();
				Map aliasMap = calculateAliasMap(projection.getQuery());
				if (filter != null) {
					// transform filter based on current projection
					CqnPredicate resolvedFilter = CQL.copy(filter,
							new AliasModifier(projection.getEntity(), aliasMap, currentProjection));
					resolvedRef = resolvedRef.filter(resolvedFilter);
					filter = resolvedFilter;
				}
				if (!iterator.hasNext()) {
					// only add new projections for the last ref segment because only
					// these are necessary to resolve the data
					if (currentProjection == null) {
						projectionStack.push(projection);
					} else {
						Deque projections = currentProjection.refs.get(ref);
						if (projections == null) {
							projections = new ArrayDeque<>();
							currentProjection.refs.put(ref, projections);
						}
						projections.push(projection);
					}
				}

				// replace the alias of the previous projection layer with the alias from this
				// projection layer
				previousAliasMap.replaceAll((k, v) -> aliasMap.getOrDefault(v, v));
				// add all new aliases
				aliasMap.entrySet().stream().filter(e -> !previousAliasMap.containsValue(e.getKey()))
						.forEach(e -> previousAliasMap.put(e.getKey(), e.getValue()));
			}
		}

		if (currentProjection != null) {
			currentProjection.aliases.put(ref, resolvedRef.asRef());
		}
		return new RefAndAliases(resolvedRef.asRef(), previousAliasMap);
	}

	private static class RefAndAliases {
		private final CqnStructuredTypeRef ref;
		private final Map aliases;

		public RefAndAliases(CqnStructuredTypeRef ref, Map aliases) {
			this.ref = ref;
			this.aliases = aliases;
		}
	}

	/**
	 * Resolves a rootless ElementRef, by prefixing it with the given root entity.
	 *
	 * @param root the root from which the ref branches off.
	 * @param ref  the ref
	 * @return the resolved ref
	 *
	 * @see #resolveRef(StructuredTypeRef)
	 */
	private ElementRef resolveRef(CdsEntity root, CqnElementRef ref, Projection currentProjection) {
		// prefix ref with root entity to enable resolvement
		StructuredTypeRef structuredType = prefix(root, skipLast(ref));
		RefAndAliases resolvedRefAndAliases = resolveRefAndAliases(structuredType, currentProjection);
		CqnStructuredTypeRef resolvedStructuredType = resolvedRefAndAliases.ref;
		Map aliases = resolvedRefAndAliases.aliases;

		// restore original ref type
		List resolvedSegments = skipFirst(resolvedStructuredType);
		ElementRef result = CQL.to(resolvedSegments)
				.get(aliases.getOrDefault(ref.lastSegment(), ref.lastSegment()));
		currentProjection.aliases.put(ref, result);

		return result;
	}

	/**
	 * Resolves a rootless ElementRef or StructuredTypeRef, by prefixing it with the
	 * given root entity.
	 *
	 * @param root the root from which the ref branches off.
	 * @param ref  the ref
	 * @return the resolved ref
	 *
	 * @see #resolveRef(StructuredTypeRef)
	 */
	private StructuredType resolveType(CdsEntity root, CqnStructuredTypeRef ref, Projection currentProjection) {
		StructuredTypeRef structuredType = prefix(root, ref.segments());
		RefAndAliases resolvedRefAndAliases = resolveRefAndAliases(structuredType, currentProjection);
		CqnStructuredTypeRef resolvedStructuredType = resolvedRefAndAliases.ref;

		// restore original ref type
		List resolvedSegments = skipFirst(resolvedStructuredType);

		return CQL.to(resolvedSegments);
	}

	private static StructuredTypeRef prefix(CdsEntity root, List list) {
		List segments = new ArrayList<>();
		segments.add(CQL.refSegment(root.getQualifiedName()));
		segments.addAll(list);

		return CQL.to(segments).asRef();
	}

	/**
	 * Calculates an alias map based on a projection query.
	 *
	 * @param query the projection query
	 * @return the alias map, containing only aliased ElementRefs as strings. The
	 *         alias name is mapped to the original name from the projection queries
	 *         target entity.
	 *
	 * @see #calculateAliasMap(CdsEntity, List)
	 */
	private Map calculateAliasMap(CqnSelect query) {
		CdsEntity queryTarget = analyzer.analyze(query).targetEntity();
		return calculateAliasMap(queryTarget, query.items());
	}

	/**
	 * Calculates an alias map based on a list of SLIs.
	 *
	 * For example: entity Foo as projection on Bar { b as f, a } This will return a
	 * map that contains a single entry which maps f to b. The alias map therefore
	 * follows projections in the direction towards the base entity.
	 *
	 * @param queryTarget the target entity of the projection query (here: Bar).
	 *                    This entity is the root from which the unaliased refs in
	 *                    the SLI list branch off.
	 *
	 * @param items       the list of aliased and unaliased SLIs
	 *
	 * @return the alias map, containing only aliased ElementRefs as strings. The
	 *         alias name is mapped to the original name from the projection queries
	 *         target entity.
	 */
	private Map calculateAliasMap(CdsEntity queryTarget, List items) {
		Map aliasMap = new HashMap<>();
		items.stream().flatMap(CqnSelectListItem::ofValue).forEach(slv -> {
			String alias = slv.alias().orElse(null);
			if (alias != null) {
				CqnValue val = slv.value();
				if (val.isRef()) {
					CqnElementRef ref = val.asRef();
					String original = ref.path();
					aliasMap.put(alias, original);

					// add mapping for flattened (4odata) structured elements & associated foreign
					// key elements
					boolean singleValuedAssociation = CdsModelUtils.findElement(queryTarget, ref)
							.map(e -> e.getType().isAssociation() && CdsModelUtils.isSingleValued(e.getType()))
							.orElse(true);
					if (singleValuedAssociation) {
						String prefix = original + "_";
						queryTarget.elements().map(e -> e.getName()).filter(e -> e.startsWith(prefix)).forEach(e -> {
							String elementInStruct = e.substring(prefix.length() - 1);
							aliasMap.put(alias + elementInStruct, e);
						});
					}
				} else {
					aliasMap.put(alias, UNKNOWN);
				}
			}
		});
		return aliasMap;
	}

	/**
	 * Removes aliases in SLI and SLIs of expands by replacing the aliased
	 * ElementRef with its alias name. This prepares the statement for processing by
	 * the AliasModifier.
	 *
	 * This is required in incoming statements, as when resolving projections we can
	 * only guarantee a unique set of alias names on a single projection level. In
	 * case we keep aliases present this can create ambiguous statements, for
	 * example in case the alias is referred to in an orderby clause.
	 */
	private class AliasRemover implements LeanModifier {

		private final CdsEntity target;
		private final Projection currentProjection;

		/**
		 * @param target the statement or expand target. It is the root entity from
		 *               which refs of the processed SLIs branch off.
		 */
		public AliasRemover(CdsEntity target, Projection currentProjection) {
			this.target = target;
			this.currentProjection = currentProjection;
		}

		@Override
		public CqnSelectListItem selectListItem(Value value, String alias) {
			if (value.isRef() && alias != null) {
				// prepare for alias modifier
				return CQL.get(alias);
			}
			return LeanModifier.super.selectListValue(value, alias);
		}

		@Override
		public CqnSelectListItem expand(CqnExpand expand) {
			List items = expand.items();
			CqnStructuredTypeRef ref = expand.ref();
			// no aliases in expand star
			if (ref.firstSegment().equals("*")) {
				return expand;
			}

			CdsEntity expandTarget = CdsModelUtils.target(target, ref.segments()).as(CdsEntity.class);
			// recursively apply alias remover to expands
			AliasRemover aliasRemover = new AliasRemover(expandTarget, currentProjection);
			List unaliasedItems = items.stream().map(i -> ExpressionVisitor.copy(i, aliasRemover))
					.collect(toList());

			// post process items with alias modifier
			Map expandAliasMap = calculateAliasMap(expandTarget, items);
			AliasModifier aliasModifier = new AliasModifier(null, expandAliasMap, currentProjection);
			List resolvedItems = unaliasedItems.stream()
					.map(i -> ExpressionVisitor.copy(i, aliasModifier)).collect(toList());
			List resolvedOrderBy = expand.orderBy().stream()
					.map(o -> ExpressionVisitor.copy(o, aliasModifier)).collect(toList());

			return CQL.to(ref.segments()).expand(resolvedItems).orderBy(resolvedOrderBy).limit(expand.top(),
					expand.skip());
		}

	}

	/**
	 * Given the following projection: entity Foo as projection on Bar { b as f }
	 * Given the following statement: SELECT Foo { f } orderby { f }
	 *
	 * The AliasModifier replaces ElementRefs from a projection entity (Foo) with
	 * their original ElementRefs from the projected entity (Bar). It achieves this
	 * based on the given aliasMap, which can be calculated from a projection query
	 * using the {@link ProjectionResolver#calculateAliasMap(CqnSelect)} method.
	 *
	 * The AliasModifier assumes that the statement that is modified does not
	 * contain any explicit aliases anymore. The statement should solely rely on
	 * names that can be obtained from the projection definition. To achieve this
	 * the {@link AliasRemover} can be used.
	 */
	private class AliasModifier implements LeanModifier {

		protected final CdsEntity target;
		protected final Map aliasMap;
		protected final Projection currentProjection;

		/**
		 * @param target   the projection entity (Foo, in above example). Can be null,
		 *                 in case it is guranteed that there are only single-segment
		 *                 element refs with aliases in the statement. This is for
		 *                 example the case after processing a statement with the
		 *                 {@link AliasRemover}.
		 *
		 * @param aliasMap the alias map calculated from the projection query, which
		 *                 defines the projection entity.
		 */
		public AliasModifier(CdsEntity target, Map aliasMap, Projection currentProjection) {
			this.target = target;
			this.aliasMap = aliasMap;
			this.currentProjection = currentProjection;
		}

		@Override
		public CqnValue ref(CqnElementRef ref) {
			// resolve single segment refs directly with alias map
			if (ref.size() == 1) {
				String id = ref.firstSegment();
				return CQL.get(aliasMap.getOrDefault(id, id));
			} else if (target != null) {
				return resolveRef(target, ref, currentProjection);
			} else {
				return ref;
			}
		}

		@Override
		public Predicate exists(Select subQuery) {
			subQuery.where().map(w -> CQL.copy(w, new LeanModifier() {
				@Override
				public CqnValue ref(CqnElementRef ref) {
					// resolve outer ref with single element suffix
					String id = ref.lastSegment();
					if (ref.size() == 2 && OUTER.equals(ref.firstSegment()) && aliasMap.containsKey(id)) {
						return elementRef(OUTER, aliasMap.get(id));
					}

					return ref;
				}
			})).ifPresent(subQuery::where);

			return CQL.exists(subQuery);
		}

	}

	/**
	 * The SelectionModifier modifies the SLIs of a statement based on the SLIs
	 * selected in a projection query.
	 *
	 * Its main task is to resolve a star select to all non-association elements
	 * selected in a projection query. It can also merge this with explicitly
	 * mentioned elements in the statement. In case the projection itself is a star
	 * select it does not resolve the star select of the statement.
	 *
	 * In addition it takes over all excluded elements from the projection into the
	 * statement.
	 */
	private class SelectionModifier implements LeanModifier {

		private final CdsEntity target;
		private final CqnSelect query;
		private final Map aliasMap;
		private final CdsEntity projectionTarget;

		/**
		 * @param target the projection entity, which is created / defined by the
		 *               projection
		 * @param query  the projection query
		 */
		public SelectionModifier(CdsEntity target, CqnSelect query) {
			this.target = target;
			this.query = query;
			this.aliasMap = calculateAliasMap(query);
			this.projectionTarget = analyzer.analyze(query).targetEntity();
		}

		@Override
		public List items(List items) {
			// if projection is selecting all elements no need to adapt
			if (CqnStatementUtils.isSelectStar(query.items())) {
				return items;
			}

			boolean hasStar = false;
			boolean hasExpandStar = false;
			Set selected = new HashSet<>();
			List slis = new ArrayList<>();
			for (CqnSelectListItem item : items) {
				if (item.isStar()) {
					hasStar = true;
					continue;
				} else if (item.isExpand() && item.asExpand().ref().firstSegment().equals("*")) {
					hasExpandStar = true;
					continue;
				}

				String displayName = null;
				if (item.isValue()) {
					displayName = item.asValue().displayName();
				} else if (item.isExpand()) {
					displayName = item.asExpand().displayName();
				}

				if (displayName == null || isAllowedSelection(displayName)) {
					if (displayName != null) {
						selected.add(displayName);
					}
					slis.add(item);
				}
			}

			// resolve stars and merge with explicitly mentioned elements
			if (hasStar || items.isEmpty()) {
				target.concreteNonAssociationElements().filter(e -> isAllowedSelection(e.getName()))
						.filter(e -> !selected.contains(e.getName())).map(e -> CQL.get(e.getName())).forEach(slis::add);
			}

			if (hasExpandStar) {
				target.associations().filter(a -> !a.isVirtual()).filter(e -> isAllowedSelection(e.getName()))
						.filter(e -> !selected.contains(e.getName())).map(e -> CQL.to(e.getName()).expand())
						.forEach(slis::add);
			}

			return slis;
		}

		@Override
		public Set excluding(Set excluding) {
			Set all = new HashSet<>(excluding);
			all.addAll(query.excluding());
			return all;
		}

		private boolean isAllowedSelection(String element) {
			String mapped = aliasMap.getOrDefault(element, element);
			boolean existsOnThisLevel = CdsModelUtils.findElement(target, CQL.get(element)).isPresent();
			boolean existsOnNextLevel = CdsModelUtils.findElement(projectionTarget, CQL.get(mapped)).isPresent();
			return existsOnThisLevel == existsOnNextLevel;
		}

	}

	/**
	 * Resolves the root of the statement ref to the next projection layer. All
	 * other path segments are resolved as long as the association target of the
	 * prior resolved segment does not match the actual segment target.
	 */
	private class RefModifier implements LeanModifier {

		@Override
		public CqnStructuredTypeRef ref(CqnStructuredTypeRef ref) {
			return resolveRefAndAliases(ref, null).ref;
		}

	}

	/**
	 * This modifier is an extended {@link AliasModifier}, which traverses expands
	 * and handles additional aspects besides ElementRefs.
	 *
	 * - It takes care of resolving aliases used in excluding element names. - It
	 * recursively traverses into expands to resolve refs while also applying the
	 * {@link SelectionModifier} there.
	 */
	private class ExtendedAliasModifier extends AliasModifier {

		/**
		 * @param target            the projection entity
		 * @param aliasMap          the alias map calculated from the projection query
		 * @param currentProjection the projection of the current layer
		 *
		 * @see AliasModifier#AliasModifier(CdsEntity, Map)
		 */
		public ExtendedAliasModifier(CdsEntity target, Map aliasMap, Projection currentProjection) {
			super(target, aliasMap, currentProjection);
		}

		@Override
		public CqnSelectListItem expand(CqnExpand expand) {
			CqnStructuredTypeRef ref = expand.ref();
			List items = expand.items();
			List orderBy = expand.orderBy();
			StructuredType resolvedType = resolveType(target, ref, currentProjection);
			// find matching projection for ref
			Optional> projections = currentProjection.refs.entrySet().stream()
					.filter(e -> skipFirst(e.getKey()).equals(ref.segments())).map(e -> e.getValue()).findAny();
			if (!projections.isPresent()) {
				return resolvedType.expand(items).orderBy(orderBy).limit(expand.top(), expand.skip());
			}

			CdsEntity expandTarget = CdsModelUtils.target(target, ref.asRef().segments()).as(CdsEntity.class);
			List resolvedItems = items;
			List resolvedOrderBy = orderBy;

			// resolve all projection layers of the ref target
			Iterator descendingIterator = projections.get().descendingIterator();
			while (descendingIterator.hasNext()) {
				Projection currentProjection = descendingIterator.next();
				CqnSelect expandQuery = currentProjection.getQuery();
				// resolve selection set based on projection
				List expandedItems = new SelectionModifier(expandTarget, expandQuery)
						.items(resolvedItems);

				// adapt all aliases in references
				ExtendedAliasModifier aliasModifier = new ExtendedAliasModifier(expandTarget,
						calculateAliasMap(expandQuery), currentProjection);
				resolvedItems = expandedItems.stream().map(i -> ExpressionVisitor.copy(i, aliasModifier))
						.collect(toList());
				resolvedOrderBy = resolvedOrderBy.stream().map(o -> ExpressionVisitor.copy(o, aliasModifier))
						.collect(toList());
				expandTarget = CdsModelUtils.entity(model, expandQuery.ref());
			}

			return resolvedType.expand(resolvedItems).orderBy(resolvedOrderBy).limit(expand.top(), expand.skip());
		}

		@Override
		public Set excluding(Set excluding) {
			return excluding.stream().map(e -> aliasMap.getOrDefault(e, e)).collect(Collectors.toSet());
		}

	}

	/**
	 * The DataAliasResolver maps nested entity data based on a projection. It can
	 * map in both directions, either following the projection towards the base
	 * entity, or the opposite direction. It also handles expanded data by
	 * traversing into the projections defined for these expanded entities.
	 */
	private class DataAliasResolver {

		private final boolean forwardMapping;

		/**
		 * @param forwardMapping true, in case mapping should occur towards the base
		 *                       entity
		 */
		public DataAliasResolver(boolean forwardMapping) {
			this.forwardMapping = forwardMapping;
		}

		/**
		 * Resolves the entries against the given projection.
		 *
		 * @param target     the type of entries
		 * @param entries    the entries
		 * @param projection the projection
		 */
		public void resolve(CdsEntity target, List> entries, Projection projection) {
			if (projection.refs.isEmpty()) {
				// traverse into associations
				target.associations().forEach(a -> resolveAssociation(entries, projection, a));
			} else {
				// traverse into expanded entities
				projection.refs.entrySet().forEach(refEntry -> resolveExpandedEntities(entries, projection, refEntry));
			}

			Map aliasMap = calculateAliasMap(projection.getQuery());
			// add all aliases that are introduced through references with more than one
			// segment
			projection.aliases.entrySet().forEach(
					entry -> aliasMap.putIfAbsent(entry.getKey().lastSegment(), entry.getValue().lastSegment()));

			// resolve this layer
			CdsEntity projectedType = forwardMapping ? analyzer.analyze(projection.getQuery()).targetEntity()
					: projection.getEntity();
			entries.forEach(entry -> {
				Map renamedEntry = new HashMap<>();
				entry.forEach((key, value) -> {
					String mapping = getMapping(key, aliasMap);
					if (projectedType == null
							|| (CdsModelUtils.findElement(target, CQL.get(key)).isPresent() == CdsModelUtils
									.findElement(projectedType, CQL.get(mapping)).isPresent())
							|| (aliasMap.containsKey(mapping) && aliasMap.get(mapping).split("\\.").length > 1)) {
						renamedEntry.put(mapping, value);
					}
				});
				entry.clear();
				entry.putAll(renamedEntry);
			});
		}

		private void resolveExpandedEntities(List> entries, Projection projection,
				Entry> refEntry) {
			CqnReference ref = projection.aliases.get(refEntry.getKey());
			String name = ref.lastSegment();
			List> associationEntries = getAssociationEntries(entries, name);

			if (associationEntries.isEmpty()) {
				return;
			}

			for (Projection childProjection : refEntry.getValue()) {
				CdsEntity childProjectionTarget = CqnAnalyzer.create(model).analyze(childProjection.getQuery())
						.targetEntity();
				resolve(childProjectionTarget, associationEntries, childProjection);
			}
		}

		private void resolveAssociation(List> entries, Projection projection,
				CdsElement a) {
			String associationName = a.getName();
			CdsEntity associationTarget = a.getType().as(CdsAssociationType.class).getTarget();
			List> associationEntries = getAssociationEntries(entries, associationName);

			if (associationEntries.isEmpty()) {
				return;
			}

			String resolvedAssociationName = getMapping(associationName, projection);
			Deque associationProjections = new LinkedList<>();
			CdsEntity resolvedTarget = getResolvedTarget(projection, forwardMapping);

			if (resolvedTarget != null && hasAssociation(resolvedTarget, resolvedAssociationName)) {
				// we need to resolve all projection layers of the association target until it
				// matches the resolvedAssociationTarget
				CdsEntity resolvedAssociationTarget = resolvedTarget.getTargetOf(resolvedAssociationName);
				associationProjections = getProjectionStack(
						forwardMapping ? associationTarget : resolvedAssociationTarget,
						forwardMapping ? resolvedAssociationTarget : associationTarget);
			} else {
				// aliases in expands in projection
				for (CqnSelectListItem item : projection.getQuery().items()) {
					if (item.isExpand() && item.asExpand().displayName().equals(resolvedAssociationName)) {
						associationProjections.add(
								new Projection(null, Select.from(associationTarget).columns(item.asExpand().items())));
						break;
					}
				}
			}

			Iterator iter = forwardMapping ? associationProjections.descendingIterator()
					: associationProjections.iterator();
			Projection associationProjection;
			while (iter.hasNext()) {
				associationProjection = iter.next();
				resolve(getResolvedTarget(associationProjection, !forwardMapping), associationEntries,
						associationProjection);
			}
		}

		private boolean hasAssociation(CdsEntity target, String associationName) {
			return !UNKNOWN.equals(associationName)
					&& target.findElement(associationName).filter(e -> e.getType().isAssociation()).isPresent();
		}

		@SuppressWarnings("unchecked")
		private List> getAssociationEntries(List> entries,
				String name) {
			List> associationEntries = new ArrayList<>();
			for (Map entry : entries) {
				Object value = entry.get(name);
				if (value instanceof Map) {
					associationEntries.add((Map) value);
				} else if (value instanceof List) {
					associationEntries.addAll((List>) value);
				}
			}
			return associationEntries;
		}

		private String getMapping(String key, Projection projection) {
			return getMapping(key, calculateAliasMap(projection.getQuery()));
		}

		private String getMapping(String key, Map aliasMap) {
			if (forwardMapping) {
				return aliasMap.getOrDefault(key, key);
			}
			return aliasMap.entrySet().stream().filter(e -> e.getValue().equals(key)).map(e -> e.getKey()).findFirst()
					.orElse(aliasMap.entrySet().stream().filter(e -> getLastSegment(e.getValue()).equals(key))
							.map(e -> e.getKey()).findFirst().orElse(key));
		}

		private CdsEntity getResolvedTarget(Projection projection, boolean forward) {
			if (forward) {
				return analyzer.analyze(projection.getQuery()).targetEntity();
			}
			return projection.getEntity();
		}

		private String getLastSegment(String segments) {
			if (segments == null || segments.isEmpty()) {
				return segments;
			}
			String[] segmentArray = segments.split("\\.");
			return segmentArray[segmentArray.length - 1];
		}

	}

	/**
	 * Returns the resolved statement.
	 *
	 * @return the resolved statement
	 */
	public T getResolvedStatement() {
		return current;
	}

	/**
	 * Transforms the list of entries to structurally match the original statement.
	 *
	 * @param entries the execution result for the resolved statement to be
	 *                transformed into the original representation
	 * @return the transformed entries
	 */
	public List> transform(List> entries) {
		if (projectionStack.isEmpty()) {
			return entries;
		}

		List> copy = DataUtils.copyGenericList(entries);
		DataAliasResolver dataAliasResolver = new DataAliasResolver(false);
		CdsEntity target = analyzer.analyze(current.ref()).targetEntity();
		for (Projection projection : projectionStack) {
			dataAliasResolver.resolve(target, copy, projection);
			target = projection.getEntity();
		}
		return copy;
	}

	/**
	 * Transforms the {@link Result} to structurally match the original statement.
	 * It also redetermines the row type of the {@link Result}
	 *
	 * @param result the execution result for the resolved statement to be
	 *               transformed into the original representation
	 * @return the transformed {@link Result}
	 */
	public Result transform(Result result) {
		if (projectionStack.isEmpty()) {
			return result;
		}

		List> transformedResult = transform(result.list());
		ResultImpl builder = ResultImpl.from(result).rows(transformedResult);
		if (current.isSelect()) {
			builder.rowType(CqnStatementUtils.rowType(model, projectionStack.getLast().getQuery()));
		}
		return builder.result();
	}

	private interface Condition {
		default boolean testNext() {
			return false;
		}
	}

	@FunctionalInterface
	public interface BiPredicate extends Condition, java.util.function.BiPredicate {
	}

	@FunctionalInterface
	public interface TriPredicate extends Condition {
		boolean test(CqnStatement previous, CqnStatement resolved, CqnStatement next);

		@Override
		default boolean testNext() {
			return true;
		}
	}

	/**
	 * Represents a projection definition. For example: entity Foo as projection on
	 * Bar { b as f }
	 *
	 * It consists of the projection entity (the entity created / defined by the
	 * projection, here: Foo) and the projection query (here: SELECT Bar { b as f
	 * }), which is based on the projected entity (here: Bar)
	 */
	private static class Projection {

		private final CdsEntity entity;
		private final CqnSelect query;

		/**
		 * The references resolved in this projection layer and their projection stack
		 */
		private Map> refs = new HashMap<>();

		/**
		 * The aliases of {@link CqnReference}s resolved on this projection layer
		 */
		private Map aliases = new HashMap<>();

		/**
		 * @param entity the projection entity, which is created / defined by the
		 *               projection. May be null, if there is no such entity definition
		 *               in the model. This is the case when aliases are used in a
		 *               Select statement, creating a "virtual" projection entity.
		 *
		 * @param query  the projection query, defining the projection entity based on
		 *               the projected entity.
		 */
		public Projection(CdsEntity entity, CqnSelect query) {
			this.entity = entity;
			this.query = query;
		}

		public CdsEntity getEntity() {
			return entity;
		}

		public CqnSelect getQuery() {
			return query;
		}

	}

	/**
	 * Only visible internally. Used to ease aborting of projection resolvement.
	 * Can't use a checked Exception however due to Modifier interface constraints.
	 */
	private static class UnsupportedProjectionException extends RuntimeException {
		private static final long serialVersionUID = 1L;
	}

	private static List skipFirst(CqnReference ref) {
		List segments = ref.segments();
		return segments.subList(1, segments.size());
	}

	private static List skipLast(CqnReference ref) {
		List segments = ref.segments();
		return segments.subList(0, segments.size() - 1);
	}

	/**
	 * @param top    the high projection layer
	 * @param bottom the low projection layer
	 * @return the projection stack to get from {@code top} to {@bottom}
	 */
	private Deque getProjectionStack(CdsEntity top, CdsEntity bottom) {
		Deque stack = new LinkedList<>();
		CdsEntity currentEntity = top;
		while (!currentEntity.getQualifiedName().equals(bottom.getQualifiedName())) {
			Optional optQuery = currentEntity.query();
			if (optQuery.isPresent()) {
				if (!isSupportedProjection(currentEntity, optQuery.get())) {
					throw new UnsupportedProjectionException();
				}
				stack.push(new Projection(currentEntity, optQuery.get()));
				currentEntity = analyzer.analyze(optQuery.get()).targetEntity();
			} else {
				// entity is not projected
				break;
			}
		}
		return stack;
	}

}