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

com.sap.cds.ql.impl.CqnNormalizer Maven / Gradle / Ivy

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

import static com.sap.cds.DataStoreConfiguration.SELECT_STAR;
import static com.sap.cds.DataStoreConfiguration.SELECT_STAR_COLUMNS;
import static com.sap.cds.DataStoreConfiguration.UNIVERSAL_CSN;
import static com.sap.cds.util.CqnStatementUtils.isToOnePath;
import static com.sap.cds.util.CqnStatementUtils.removeVirtualElements;
import static com.sap.cds.util.CqnStatementUtils.resolveExpands;
import static com.sap.cds.util.CqnStatementUtils.resolveKeyPlaceholder;
import static com.sap.cds.util.CqnStatementUtils.resolveStar;
import static com.sap.cds.util.CqnStatementUtils.resolveStructureComparison;
import static com.sap.cds.util.CqnStatementUtils.resolveVirtualElements;
import static com.sap.cds.util.CqnStatementUtils.simplify;
import static com.sap.cds.util.CqnStatementUtils.unfoldInline;
import static com.sap.cds.util.NestedStructsResolver.resolveNestedStructs;
import static com.sap.cds.util.PathExpressionResolver.resolvePath;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.sap.cds.DataStoreConfiguration;
import com.sap.cds.SessionContext;
import com.sap.cds.impl.Context;
import com.sap.cds.impl.DraftUtils;
import com.sap.cds.jdbc.spi.SearchResolver;
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.cqn.CqnAnalyzer;
import com.sap.cds.ql.cqn.CqnDelete;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnExpand;
import com.sap.cds.ql.cqn.CqnInsert;
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.CqnSource;
import com.sap.cds.ql.cqn.CqnStatement;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.ql.cqn.CqnUpsert;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsModel;
import com.sap.cds.reflect.CdsStructuredType;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CqnStatementUtils;
import com.sap.cds.util.OnConditionAnalyzer;

public class CqnNormalizer {
	private final CdsModel cdsModel;
	private final DataStoreConfiguration config;
	private final CqnAnalyzer analyzer;
	private final Context context;
	private SessionContext sessionContext;
	private SearchResolver searchResolver;

	public CqnNormalizer(Context context) {
		this.context = context;
		this.cdsModel = context.getCdsModel();
		this.sessionContext = context.getSessionContext();
		this.analyzer = CqnAnalyzer.create(cdsModel);
		this.config = context.getDataStoreConfiguration();
	}

	private SearchResolver getSearchResolver() {
		// Lazy load SearchResolver, as HanaSearchResolver is stateful
		// TODO: make Hana search resolver is stateless, by removing 'inSubquery'
		if (null == searchResolver) {
			this.searchResolver = context.getSearchResolver();
		}
		return searchResolver;
	}

	public CqnSelect normalize(CqnSelect select) {
		Select copy = normalizeSubqueries(select);
		CdsStructuredType target = CqnStatementUtils.targetType(cdsModel, copy);
		boolean includeAssocs = includeManagedAssocs(copy);
		resolveStar(copy, target, includeAssocs);
		resolveNestedStructs(copy, target, includeAssocs);
		if (config.getProperty(DataStoreConfiguration.IGNORE_VIRTUAL_ELEMENTS, true)) {
			removeVirtualElements(copy, target);
		} else {
			resolveVirtualElements(copy, target);
		}
		resolveKeyPlaceholder(target, copy);
		// TODO don't copy
		resolveStructureComparison(target, copy);
		copy = resolveExpands(copy, target, includeAssocs);
		unfoldInline(copy, target);
		copy = (Select) getSearchResolver().resolve(copy);
		copy = resolveMatchPredicates(target, copy);
		copy = (Select) DraftUtils.resolveConstantElements(cdsModel, target, copy);
		simplify(target, copy);

		return copy;
	}

	private boolean includeManagedAssocs(CqnSelect select) {
		return select.from().isRef() && config.getProperty(UNIVERSAL_CSN, false)
				&& SELECT_STAR_COLUMNS.equals(config.getProperty(SELECT_STAR));
	}

	public CqnSelect resolveForwardMappedAssocs(CqnSelect select) {
		CdsStructuredType target = CqnStatementUtils.targetType(cdsModel, select);
		return resolveForwardMappedAssocs(select, target);
	}

	private CqnSelect resolveForwardMappedAssocs(CqnSelect select, CdsStructuredType target) {
		return SelectBuilder.copy(select, new LeanModifier() {
			@Override
			public List items(List items) {
				List resolved = resolveForwardMappedAssocs(target, items);

				return LeanModifier.super.items(resolved);
			}

			@Override
			public CqnSelectListItem expand(CqnExpand expand) {
				if (!expand.ref().lastSegment().equals("*")) {
					CdsEntity expTarget = CdsModelUtils.entity((CdsEntity) target, expand.ref().segments());
					List resolved = resolveForwardMappedAssocs(expTarget, expand.items());

					return CQL.copy(expand).items(resolved);
				}

				return expand;
			}
		}, false);
	}

	/*
	 * Returns a list of slis based on items w/o associations. Forward-mapped
	 * associations are resolved to FK elements with a structuring alias.
	 * Reverse-mapped associations cause a UnsupportedOperationException.
	 */
	private List resolveForwardMappedAssocs(CdsStructuredType target,
			List items) {
		List slis = new ArrayList<>();
		for (CqnSelectListItem item : items) {
			if (item.isRef()) {
				CdsElement element = CdsModelUtils.element(target, item.asRef());
				if (element.getType().isAssociation()) {
					slis.addAll(resolveForwardMappedAssoc(target, item.asValue(), element));
					continue;
				}
			}
			slis.add(item);
		}
		return slis;
	}

	/*
	 * Returns the FK elements of forward-mapped to-one associations. Reverse-mapped
	 * associations cause a UnsupportedOperationException.
	 */
	private List resolveForwardMappedAssoc(CdsStructuredType target, CqnSelectListValue refValue,
			CdsElement element) {
		List slis = new ArrayList<>();
		CqnElementRef ref = refValue.value().asRef();
		if (isToOnePath(target, ref.segments()) && !CdsModelUtils.isReverseAssociation(element)) {
			OnConditionAnalyzer onCondAnalyzer = new OnConditionAnalyzer(element, false);
			onCondAnalyzer.getFkMapping().forEach((fk, r) -> {
				if (!fk.contains(".") && r.isRef() && r.asRef().firstSegment().equals(ref.lastSegment())) {
					ElementRef elementRef = CQL.to(ref.segments().subList(0, ref.segments().size() - 1)).get(fk);
					slis.add(elementRef.as(refValue.displayName() + "." + r.asRef().displayName()));
				}
			});
		} else {
			throw new UnsupportedOperationException("Association " + element.getQualifiedName()
					+ " is not supported on the select list. Only forward-mapped to-one associations are supported on the select list");
		}
		return slis;
	}

	private  S resolveMatchPredicates(CdsStructuredType target, S stmt) {
		return new MatchPredicateNormalizer(cdsModel, target).normalize(stmt);
	}

	private Select normalizeSubqueries(CqnSelect select) {
		CqnSource source = select.from();
		if (!source.isSelect()) {
			return SelectBuilder.copy(select);
		}

		// a SearchResolver may push down a search expression
		select.search().ifPresent(s -> getSearchResolver().pushDownSearchToSubquery(select, select.from().asSelect()));

		final CqnSelect inner = normalize(source.asSelect());
		SelectBuilder outerSelect = (SelectBuilder) Select.from(inner);

		moveAllQueryPartsFromOldSelectToNewOuterSelect(select, outerSelect);

		return outerSelect;
	}

	private void moveAllQueryPartsFromOldSelectToNewOuterSelect(CqnSelect select, SelectBuilder outer) {
		if (select.isDistinct()) {
			outer.distinct();
		}
		if (select.hasInlineCount()) {
			outer.inlineCount();
		}

		outer.columns(select.items());

		select.where().ifPresent(outer::where);
		select.search().ifPresent(s -> {
			Predicate searchExpression = (Predicate) s;
			Collection searchableElements = outer.searchableElements();
			outer.search(e -> searchExpression, searchableElements);
		});

		select.having().ifPresent(outer::having);
		outer.groupBy(select.groupBy());
		outer.orderBy(select.orderBy());

		select.limit().ifPresent(l -> outer.limit(l.top(), l.skip()));
		outer.excluding(select.excluding());

		select.getLock().ifPresent(l -> outer.lock(l.timeout().get()));
	}

	public CqnInsert normalize(CqnInsert insert) {
		insert = resolvePath(cdsModel, insert, sessionContext);

		return insert;
	}

	public CqnUpsert normalize(CqnUpsert upsert) {
		upsert = resolvePath(cdsModel, upsert, sessionContext);

		return upsert;
	}

	public CqnUpdate normalize(CqnUpdate update) {
		update = resolvePath(cdsModel, update);

		return norm(update);
	}

	public CqnDelete normalize(CqnDelete delete) {
		delete = resolvePath(cdsModel, delete);

		return norm(delete);
	}

	private  S norm(S stmt) {
		CdsEntity target = analyzer.analyze(stmt.ref()).targetEntity();
		stmt = resolveKeyPlaceholder(target, stmt);
		stmt = resolveStructureComparison(target, stmt);
		stmt = resolveMatchPredicates(target, stmt);
		return stmt;
	}

}