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

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

The newest version!
/************************************************************************
 * © 2020-2024 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.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 java.util.Map;

import com.sap.cds.CqnTableFunction;
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.impl.builder.model.ExistsSubquery;
import com.sap.cds.jdbc.hana.HanaDbContext;
import com.sap.cds.jdbc.spi.SearchResolver;
import com.sap.cds.jdbc.spi.StatementResolver;
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.Source;
import com.sap.cds.ql.TableFunction;
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.CqnExistsSubquery;
import com.sap.cds.ql.cqn.CqnExpand;
import com.sap.cds.ql.cqn.CqnFilterableStatement;
import com.sap.cds.ql.cqn.CqnInsert;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnSearchTermPredicate;
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.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.CqnUpsert;
import com.sap.cds.ql.cqn.Modifier;
import com.sap.cds.ql.hana.HierarchySubset;
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.CqnCalculatedElementsSubstitutor;
import com.sap.cds.util.CqnStatementUtils;
import com.sap.cds.util.EtagPredicateNormalizer;
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 final StatementResolver statementResolver;
	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();
		this.statementResolver = context.getDbContext().getStatementResolver();
	}

	private SearchResolver getSearchResolver(Map hints) {
		// Lazy load SearchResolver, as HanaSearchResolver is stateful
		// TODO: make Hana search resolver is stateless, by removing 'inSubquery'

		if (null == searchResolver || hints.get(HanaDbContext.NO_USE_HEX_PLAN) == Boolean.TRUE
				|| hints.get(HanaDbContext.USE_HEX_PLAN) == Boolean.TRUE) {
			this.searchResolver = context.getSearchResolver(hints);
		}
		return searchResolver;
	}

	public CqnSelect resolveTransformations(CqnSelect select) {
		if (!select.transformations().isEmpty()) {
			Select copy = statementResolver.applyTransformations((Select) select);
			copy = normalizeSelect(copy, select.hints());
			return copy;
		}
		return select;
	}

	public CqnSelect normalize(CqnSelect select) {
		select = statementResolver.preOptimize(select);
		Map hints = select.hints();

		return normalizeSelect(select, hints);
	}

	private Select normalizeSelect(CqnSelect select, Map hints) {
		Select copy = normalizeSource(select, hints);
		CdsStructuredType target = CqnStatementUtils.targetType(cdsModel, copy);
		boolean includeAssocs = includeManagedAssocs(copy);
		resolveStar(copy, target, includeAssocs);
		resolveNestedStructs(copy, target, includeAssocs);
		resolveKeyPlaceholder(target, copy);
		// TODO don't copy
		if (copy.from().isRef()) {
			copy = resolveStructureComparison(cdsModel, target, copy);
		}
		copy = resolveExpands(copy, target, includeAssocs);
		unfoldInline(copy, target);
		substituteAliasesInOderingSpec(copy);
		removeVirtualElements(copy, target);

		copy = resolveEtag(target, copy);
		copy = (Select) getSearchResolver(hints).resolve(copy);
		copy = resolveMatchPredicates(target, copy);
		copy = (Select) DraftUtils.resolveConstantElements(cdsModel, target, copy);
		simplify(target, copy);

		return copy;
	}

	private Select normalizeSource(CqnSelect select, Map hints) {
		Select copy;
		CqnSource source = select.from();
		if (source.isRef()) {
			// SB.copy copies the ref
			copy = SelectBuilder.copy(select);
		} else if (source.isSelect()) {
			CqnSelect subquery = source.asSelect();

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

			final CqnSelect inner = normalizeSelect(subquery, hints);
			copy = SelectBuilder.from(inner);

			moveAllQueryPartsFromOldSelectToNewOuterSelect(select, copy);
		} else if (source.isTableFunction()) {
			CqnTableFunction tableFunction = select.from().asTableFunction();
			resolveSearch(tableFunction, hints);
			final TableFunction inner = normalizeTableFunction((TableFunction) tableFunction, hints);
			copy = SelectBuilder.from(inner);

			moveAllQueryPartsFromOldSelectToNewOuterSelect(select, copy);
		} else {
			throw new IllegalStateException("unexpected source for select: " + source.getClass().getName());
		}
		return copy;
	}

	private void resolveSearch(CqnTableFunction tableFunction, Map hints) {
		if (tableFunction instanceof HierarchySubset hierarchy) {
			hierarchy.startWhere().ifPresent(s -> {
				CdsStructuredType rowType = CqnStatementUtils.rowType(cdsModel, tableFunction.source());
				CqnPredicate startWhere = CQL.copy(s, new Modifier() {
					@Override
					public CqnPredicate searchTerm(CqnSearchTermPredicate search) {
						return getSearchResolver(hints).resolve(search, rowType, true);
					}
				});
				hierarchy.startWhere(startWhere);
			});
		}
	}

	private TableFunction normalizeTableFunction(TableFunction tableFunction, Map hints) {
		CqnSource source = tableFunction.source();

		Source normalizedSource;
		if (source == null) {
			normalizedSource = null;
		} else if (source.isRef()) {
			CqnStructuredTypeRef ref = source.asRef();
			if (ref.size() == 1 && ref.rootSegment().filter().isEmpty()) {
				normalizedSource = (Source) source;
			} else {
				normalizedSource = normalizeSelect(Select.from(ref).columns(CQL.star()), hints);
			}
		} else if (source.isSelect()) {
			normalizedSource = normalizeSelect(source.asSelect(), hints);
		} else if (source.isTableFunction()) {
			normalizedSource = (Source) normalizeTableFunction((TableFunction) source.asTableFunction(), hints);
		} else {
			throw new IllegalStateException("unexpected source for select: " + source.getClass().getName());
		}

		return tableFunction.copy(normalizedSource);
	}

	private  S resolveEtag(CdsStructuredType target, S statement) {
		return new EtagPredicateNormalizer(cdsModel, target).normalize(statement);
	}

	private void substituteAliasesInOderingSpec(Select source) {
		List sortSpecifications = source.orderBy();
		if (!sortSpecifications.isEmpty()) {
			source.orderBy(sortSpecifications.stream().map(spec -> {
				if (spec.value().isRef()) {
					return source.items().stream().flatMap(CqnSelectListItem::ofValue)
							.filter(v -> v.alias().isPresent())
							.filter(v -> spec.value().asRef().path().equals(v.displayName())).findFirst()
							.map(v -> CQL.sort(v.value(), spec.order())).orElse(spec);
				} else {
					return spec;
				}
			}).toList());
		}
	}

	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 Modifier() {
			@Override
			public List items(List items) {
				List resolved = resolveForwardMappedAssocs(target, items);

				return Modifier.super.items(resolved);
			}

			@Override
			public CqnSelectListItem expand(CqnExpand expand) {
				if (!expand.ref().lastSegment().equals("*")) {
					CdsStructuredType expTarget = CdsModelUtils.target(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 void moveAllQueryPartsFromOldSelectToNewOuterSelect(CqnSelect select, Select 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 = ((SelectBuilder) outer).searchableElements();
			outer.search(e -> searchExpression, searchableElements);
		});

		select.having().ifPresent(outer::having);
		outer.groupBy(select.groupBy());
		outer.orderBy(select.orderBy());
		outer.limit(select.top(), select.skip());
		outer.excluding(select.excluding());

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

		outer.hints(select.hints());
	}

	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);
		CdsEntity target = CdsModelUtils.entity(cdsModel, update.ref());
		update = CQL.copy(update, new CqnCalculatedElementsSubstitutor(target));
		update = resolveEtag(target, update);
		return norm(update);
	}

	public CqnDelete normalize(CqnDelete delete) {
		delete = resolvePath(cdsModel, delete);
		CdsEntity target = CdsModelUtils.entity(cdsModel, delete.ref());
		delete = CQL.copy(delete, new CqnCalculatedElementsSubstitutor(target));
		delete = resolveEtag(target, delete);
		return norm(delete);
	}

	public CqnExistsSubquery normalize(CqnExistsSubquery existsSubquery) {
		CqnSelect select = statementResolver.preOptimize(existsSubquery.subquery());
		Map hints = select.hints();

		Select copy = normalizeSource(select, hints);
		CdsStructuredType target = CqnStatementUtils.targetType(cdsModel, copy);
		copy = (Select) getSearchResolver(hints).resolve(copy);
		copy = resolveMatchPredicates(target, copy);
		resolveKeyPlaceholder(target, copy);
		simplify(target, (Select) copy);

		return new ExistsSubquery(copy);
	}

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

}