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

com.sap.cds.impl.qat.FromClauseBuilder Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/**************************************************************************
 * (C) 2019-2023 SAP SE or an SAP affiliate company. All rights reserved. *
 **************************************************************************/
package com.sap.cds.impl.qat;

import static com.sap.cds.impl.docstore.DocStoreUtils.targetsDocStore;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.sap.cds.impl.Context;
import com.sap.cds.impl.DraftUtils;
import com.sap.cds.impl.DraftUtils.Element;
import com.sap.cds.impl.PreparedCqnStmt.CqnParam;
import com.sap.cds.impl.PreparedCqnStmt.Parameter;
import com.sap.cds.impl.sql.SQLHelper;
import com.sap.cds.impl.sql.SQLStatementBuilder.SQLStatement;
import com.sap.cds.impl.sql.SelectStatementBuilder;
import com.sap.cds.impl.sql.TokenToSQLTransformer;
import com.sap.cds.jdbc.spi.SqlMapping;
import com.sap.cds.jdbc.spi.TableNameResolver;
import com.sap.cds.ql.cqn.CqnElementRef;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnReference;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnVisitor;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.reflect.CdsElement;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.reflect.CdsSimpleType;
import com.sap.cds.util.CdsModelUtils;
import com.sap.cds.util.CqnStatementUtils;

public class FromClauseBuilder {
	private static final String $JSON_ALIAS = "\"$json\"";
	private static final String INNER = "INNER";
	private static final String LEFT = "LEFT";
	private static final String OUTER = "OUTER";
	private static final String JOIN = "JOIN";
	private static final String ON = "ON";
	private static final String LPAREN = "(";
	private static final String RPAREN = ")";
	private static final String AND = "and";

	private final Context context;
	private final List params;
	private final Map hints;

	public FromClauseBuilder(Context context, List params) {
		this(context, params, Collections.emptyMap());
	}

	public FromClauseBuilder(Context context, List params, Map hints) {
		this.context = context;
		this.params = params;
		this.hints = hints;
	}

	public Stream with(Deque outer) {
		List ctes = collectCTEs(outer);
		if (ctes.isEmpty()) {
			return Stream.empty();
		}

		return Stream.of("WITH", renderCTEs(ctes));
	}

	public Stream sql(Deque outer) {
		ToSQLVisitor v = new ToSQLVisitor(outer);
		QatTraverser.take(v).traverse(outer.getLast());

		return v.sql();
	}

	private String renderCTEs(List ctes) {

		return ctes.stream().map(this::renderCTE).collect(Collectors.joining(", "));
	}

	private String renderCTE(CTE cte) {
		return cte.getElements().stream().map(element -> mapElement(element, cte)).collect(
				Collectors.joining(", ", cte.getAlias() + " as (SELECT ", " FROM " + cte.getTableName() + ")"));
	}

	private String mapElement(String element, CTE cte) {
		if (CqnStatementUtils.$JSON.equals(element)) {
			return cte.tableName + " as " + $JSON_ALIAS;
		}
		return SQLHelper.delimited(context.getDbContext().casing().apply(element));
	}

	private List collectCTEs(Deque outer) {

		List collectedCTEs = new ArrayList<>();
		QatVisitor cteVisitor = new QatVisitor() {

			@Override
			public void visit(QatAssociationNode assoc) {

				CdsEntity targetEntity = assoc.association().targetEntity();
				if (targetsDocStore(targetEntity)) {
					SqlMapping sqlMapping = getSqlMapping(targetEntity);
					Set elements = new HashSet<>();
					assoc.children().stream().map(QatNode::name).forEach(elements::add);
					targetEntity.keyElements().map(CdsElement::getName).forEach(elements::add);

					collectedCTEs.add(new CTE(sqlMapping.tableName(), assoc.alias(), elements));
				}
			}
		};

		QatTraverser.take(cteVisitor).traverse(outer.getLast());

		return collectedCTEs;
	}

	private SqlMapping getSqlMapping(CdsEntity targetEntity) {
		return context.getDbContext().getSqlMapping(targetEntity);
	}

	private class ToSQLVisitor implements QatVisitor {

		final Deque outer;
		final Stream.Builder sql = Stream.builder();

		public ToSQLVisitor(Deque outer) {
			this.outer = outer;
		}

		@Override
		public void visit(QatEntityRootNode root) {
			assertFilterHasNoPaths(root);

			table(root);
		}

		@Override
		public void visit(QatSelectRootNode root) {
			subSelect(root.select(), root.alias());
		}

		@Override
		public void visit(QatAssociationNode assoc) {
			assertFilterHasNoPaths(assoc);

			if (assoc.inSource()) {
				add(INNER);
			} else {
				add(LEFT);
				add(OUTER);
			}
			add(JOIN);
			table(assoc);
			add(ON);
			if (assoc.inSource()) {
				onConditionFilterOnParent(assoc);
			} else {
				onConditionFilterOnTarget(assoc);
			}
		}

		private void assertFilterHasNoPaths(QatEntityNode node) {
			node.filter().ifPresent(f -> f.accept(new CqnVisitor() {
				@Override
				public void visit(CqnElementRef ref) {
					if (ref.size() > 1) {
						throw new UnsupportedOperationException("Path " + ref + " in infix filter is not supported");
					}
				}
			}));
		}

		private void onConditionFilterOnParent(QatAssociationNode assoc) {
			onCondition(assoc, (QatEntityNode) assoc.parent());
		}

		private void onConditionFilterOnTarget(QatAssociationNode assoc) {
			onCondition(assoc, assoc);
		}

		private void onCondition(QatAssociationNode assoc, QatEntityNode filtered) {
			CqnPredicate on = assoc.association().on();
			TokenToSQLTransformer onToSQL = new TokenToSQLTransformer(context, params, refInOnToAlias(assoc), outer);
			Optional filter = filtered.filter();
			if (filter.isPresent()) {
				add(LPAREN);
				add(onToSQL.toSQL(on)); // toSQL
				add(RPAREN);
				TokenToSQLTransformer filterToSQL = new TokenToSQLTransformer(context, params,
						refInFilterToAlias(filtered), outer);

				filter.map(filterToSQL::toSQL).ifPresent(f -> {
					// not simplified to TRUE
					add(AND);
					add(LPAREN);
					add(f);
					add(RPAREN);
				});
			} else {
				add(onToSQL.toSQL(on)); // toSQL
			}
		}

		private void add(String txt) {
			sql.add(txt);
		}

		private Function refInFilterToAlias(QatEntityNode entityNode) {
			// we assume that filter cannot be applied to subqueries
			String alias = entityNode.alias();
			CdsEntity entity = entityNode.rowType();
			SqlMapping toSql = getSqlMapping(entity);

			return ref -> {
				checkRef(ref);

				String elementName = ref.lastSegment();
				return alias + "." + toSql.columnName(elementName);
			};

		}

		private Function refInOnToAlias(QatAssociationNode assoc) {
			String lhs = parentAlias(assoc);
			String rhs = assoc.alias();
			CdsEntity lhsType = ((QatEntityNode) assoc.parent()).rowType();
			CdsEntity rhsType = assoc.rowType();
			SqlMapping lhsToSql = getSqlMapping(lhsType);
			SqlMapping rhsToSql = getSqlMapping(rhsType);

			return ref -> {
				checkRef(ref);

				String alias;
				String columnName;
				Stream streamOfSegments = ref.stream();
				if (ref.size() > 1) {
					streamOfSegments = streamOfSegments.skip(1);
				}
				if (ref.firstSegment().equals(assoc.name())) {
					alias = rhs;
					columnName = rhsToSql.columnName(streamOfSegments);
				} else {
					alias = lhs;
					columnName = lhsToSql.columnName(streamOfSegments);
				}

				return alias + "." + columnName;
			};
		}

		private String parentAlias(QatAssociationNode assoc) {
			QatNode parent = assoc.parent();
			if (parent instanceof QatEntityNode) {
				return ((QatEntityNode) assoc.parent()).alias();
			}

			// TODO parent could be structured type
			throw new IllegalStateException("parent is no structured type");
		}

		private void subSelect(CqnSelect select, String alias) {
			add(LPAREN);
			SQLStatement stmt = new SelectStatementBuilder(context, params, select, new ArrayDeque<>()).build();
			add(stmt.sql());
			add(RPAREN);
			add(alias);
		}

		private String viewName(CdsEntity cdsEntity, EnumSet draftElements) {
			String tableName = tableName(cdsEntity, draftElements);
			String localParams = cdsEntity.params().map(p -> {
				CqnParam param = new CqnParam(p.getName(), p.getDefaultValue().orElse(null));
				param.type(p.getType().as(CdsSimpleType.class).getType());
				FromClauseBuilder.this.params.add(param);
				return "?";
			}).collect(Collectors.joining(","));
			if (!localParams.isEmpty()) {
				tableName += "(" + localParams + ")";
			}

			return tableName;
		}

		private String tableName(CdsEntity cdsEntity, EnumSet draftElements) {
			TableNameResolver resolver;
			// TODO: actually, we should use the table name resolver from the DbContext and
			// not switch this
			// at runtime. This is, however, needed for the SearchResolver logic. For most
			// of the cases its
			// ok to skip localized views and just use the plain table name. At the moment
			// this runtime dynamic
			// aspect is achieved with a not completely ready DB execution hints logic. This
			// needs some concept work.
			if (ignoreLocalizedViews()) {
				resolver = new PlainTableNameResolver();
			} else {
				resolver = context.getTableNameResolver();
			}

			if (!ignoreDraftSubquery() && DraftUtils.isDraftEnabled(cdsEntity) && !DraftUtils.isDraftView(cdsEntity)) {
				// TODO: consider search without localized views?
				return DraftUtils.activeEntity(context, resolver, cdsEntity, draftElements);
			}
			return resolver.tableName(cdsEntity);
		}

		private boolean ignoreLocalizedViews() {
			return (Boolean) hints.getOrDefault(SelectBuilder.IGNORE_LOCALIZED_VIEWS, false);
		}

		private boolean ignoreDraftSubquery() {
			return (Boolean) hints.getOrDefault(SelectBuilder.IGNORE_DRAFT_SUBQUERIES, false);
		}

		private void table(QatEntityNode node) {

			// view/table name is only needed if the type targets the column store
			// (otherwise it's covered by a CTE)
			if (!targetsDocStore(node.rowType())) {
				String viewName = viewName(node.rowType(), draftElements(node));
				add(viewName);
			}
			add(node.alias());
		}

		private Stream sql() {
			return sql.build();
		}
	}

	private static EnumSet draftElements(QatEntityNode node) {
		EnumSet draftElements = EnumSet.noneOf(DraftUtils.Element.class);
		for (QatNode child : node.children()) {
			child.accept(new QatVisitor() {
				@Override
				public void visit(QatElementNode elementNode) {
					DraftUtils.draftElement(elementNode.element()).ifPresent(draftElements::add);
				}

				@Override
				public void visit(QatAssociationNode association) {
					QatAssociation assoc = association.association();
					if (assoc.on() != null) {
						assoc.on().accept(new CqnVisitor() {
							@Override
							public void visit(CqnElementRef ref) {
								if (ref.size() == 1) {
									DraftUtils.draftElement(ref.lastSegment()).ifPresent(draftElements::add);
								}
							}
						});
					}
				}
			});
		}

		return draftElements;
	}

	private static void checkRef(CqnElementRef ref) {
		if (CdsModelUtils.isContextElementRef(ref)) {
			throw new IllegalStateException("Can't convert context element ref to column name");
		}
	}

	private static final class CTE {
		private final String alias;
		private final Set elements;
		private final String tableName;

		public CTE(String tableName, String alias, Set elements) {
			this.tableName = tableName;
			this.elements = elements;
			this.alias = alias;
		}

		public String getAlias() {
			return alias;
		}

		public Set getElements() {
			return elements;
		}

		public String getTableName() {
			return tableName;
		}
	}

	/**
	 * A table name resolver that the plain {@link SqlMapping} table name for the
	 * given entity.
	 *
	 * This is only used if a hint has been set that requires plain rendering
	 * without e.g. localized views.
	 */
	private class PlainTableNameResolver implements TableNameResolver {

		@Override
		public String tableName(CdsEntity entity) {
			return getSqlMapping(entity).tableName();
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy