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.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.CqnTableFunction;
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;
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.TableFunctionMapper;
import com.sap.cds.jdbc.spi.TableNameResolver;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Select;
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.CqnSelectListItem;
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.CqnCalculatedElementsSubstitutor;
import com.sap.cds.util.CqnStatementUtils;

public class FromClauseBuilder {
	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;

	private final Map entityToCte = new HashMap<>();

	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) {
		Map ctes = collectCTEs(outer);
		if (ctes.isEmpty()) {
			return Stream.empty();
		}

		return Stream.of(ctes.entrySet().stream().map(e -> {
			entityToCte.put(e.getKey(), e.getValue().name());
			return e.getValue().render();
		}).collect(Collectors.joining(", ", "WITH ", "")));
	}

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

		return v.sql();
	}

	private Map collectCTEs(Deque outer) {
		Map result = new HashMap<>();

		QatVisitor cteVisitor = new QatVisitor() {

			@Override
			public void visit(QatEntityRootNode root) {
				if (hasCalculatedElements(root)) {
					String name = root.rowType().getQualifiedName();
					result.put(name, StatementCTE.buildForCalculatedElements(context, root, params));
				}
			}

			@Override
			public void visit(QatAssociationNode assoc) {
				CdsEntity targetEntity = assoc.association().targetEntity();
				if (targetsDocStore(targetEntity)) {
					Set elements = new HashSet<>();
					assoc.children().stream().map(QatNode::name).forEach(elements::add);
					targetEntity.keyElements().map(CdsElement::getName).forEach(elements::add);

					String tableName = targetEntity.getQualifiedName().replace(".", "_");
					result.put(assoc.alias(), new HanaJsonDocStoreCTE(context, tableName, assoc.alias(), elements));

				} else if (!result.containsKey(assoc.rowType().getQualifiedName()) && hasCalculatedElements(assoc)) {
					result.put(targetEntity.getQualifiedName(),
							StatementCTE.buildForCalculatedElements(context, assoc, params));
				}
			}

			private boolean hasCalculatedElements(QatEntityNode node) {
				CdsEntity target = node.rowType();
				// Views and anonymous types do not retain element definition
				return !target.isView() && !target.isAnonymous() && node.children().stream()
						.anyMatch(c -> (c instanceof QatElementNode en && en.element().isCalculated()));
			}
		};

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

		return result;
	}

	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 subquery) {
			subSelect(subquery.select(), subquery.alias());
		}

		@Override
		public void visit(QatTableFunctionRootNode tableFunction) {
			tableFunction(tableFunction.tableFunction(), tableFunction.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 && !CdsModelUtils.isContextElementRef(ref)) {
						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 = TokenToSQLTransformer.notCollating(context, params, refInOnToAlias(assoc),
					outer);
			Optional filter = filtered.filter();
			if (filter.isPresent()) {
				add(LPAREN);
				add(onToSQL.toSQL(assoc.entity(), on)); // toSQL
				add(RPAREN);
				TokenToSQLTransformer filterToSQL = TokenToSQLTransformer.notCollating(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<>(), true).build();
			add(stmt.sql());
			add(RPAREN);
			add(alias);
		}

		private void tableFunction(CqnTableFunction tableFunction, String alias) {
			TableFunctionMapper mapper = context.getDbContext().getTableFunctionMapper(context, params, tableFunction);
			String fromSQL = mapper.toSQL();
			add(fromSQL);
			add(alias);
		}

		private String viewName(CdsEntity cdsEntity, EnumSet draftElements,
				boolean usesLocalizedElements) {
			String tableName = tableName(cdsEntity, draftElements, usesLocalizedElements);
			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,
				boolean usesLocalizedElements) {
			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 (!usesLocalizedElements || 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) {
			// CTE replacement is indexed. Docstore is evaluated first
			if (!entityToCte.containsKey(node.alias())) {
				// If the entity is replaced by CTE, use it for every node
				if (entityToCte.containsKey(node.rowType().getQualifiedName())) {
					add(entityToCte.get(node.rowType().getQualifiedName()));
				} else {
					String viewName = viewName(node.rowType(), draftElements(node), usesLocalizedElements(node));
					add(viewName);
				}
			}
			add(node.alias()); // Covered by CTE for DocStore
		}

		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 boolean usesLocalizedElements(QatEntityNode node) {
		CdsEntity entity = node.rowType();
		boolean[] usesLocalized = new boolean[1];

		CqnVisitor checkRefs = new CqnVisitor() {
			@Override
			public void visit(CqnElementRef ref) {
				if (ref.size() == 1
						&& CdsModelUtils.findElement(entity, ref).map(CdsElement::isLocalized).orElse(false)) {
					usesLocalized[0] = true;
				}
			}
		};

		QatVisitor vElement = new QatVisitor() {
			@Override
			public void visit(QatStructuredElementNode struct) {
				struct.children().forEach(c -> c.accept(this));
			}

			@Override
			public void visit(QatElementNode element) {
				if (element.element().isLocalized()) {
					usesLocalized[0] = true;
				}
			}

			@Override
			public void visit(QatAssociationNode assoc) {
				// check for localized elements on source side
				assoc.association().on().accept(checkRefs);
			}

		};

		QatVisitor vEntity = new QatVisitor() {
			@Override
			public void visit(QatEntityRootNode root) {
				root.filter().ifPresent(f -> f.accept(checkRefs));
				checkChildNodes(root);
			}

			@Override
			public void visit(QatAssociationNode assoc) {
				assoc.filter().ifPresent(f -> f.accept(checkRefs));
				checkChildNodes(assoc);

				// check for localized elements on target side
				CqnVisitor collectRefs = new CqnVisitor() {
					@Override
					public void visit(CqnElementRef ref) {
						if (ref.size() == 2 && ref.firstSegment().equals(assoc.name()) && assoc.rowType()
								.findElement(ref.lastSegment()).map(CdsElement::isLocalized).orElse(false)) {
							usesLocalized[0] = true;
						}
					}
				};
				assoc.association().on().accept(collectRefs);
			}

			private void checkChildNodes(QatEntityNode node) {
				Iterator iter = node.children().iterator();
				while (!usesLocalized[0] && iter.hasNext()) {
					iter.next().accept(vElement);
				}
			}

		};

		node.accept(vEntity);

		return usesLocalized[0];
	}

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

	private interface CTE {

		String render();

		String name();

	}

	private static final class HanaJsonDocStoreCTE implements CTE {
		private static final String $JSON_ALIAS = "\"$JSON\"";

		private final Collection elements;
		private final String tableName;
		private final Context context;
		private final String name;

		public HanaJsonDocStoreCTE(Context context, String tableName, String alias, Collection elements) {
			this.tableName = tableName;
			this.elements = elements;
			this.name = alias;
			this.context = context;
		}

		private String mapElement(String element) {
			if (CqnStatementUtils.$JSON.equals(element)) {
				return tableName + " as " + $JSON_ALIAS;
			}

			String col = SQLHelper.delimited(element);
			String alias = SQLHelper.delimited(context.getDbContext().casing().apply(element));

			return col + " as " + alias;
		}

		@Override
		public String render() {
			return elements.stream().map(this::mapElement)
					.collect(Collectors.joining(", ", name + " as (SELECT ", " FROM " + tableName + ")"));
		}

		@Override
		public String name() {
			return name;
		}
	}

	private static final class StatementCTE implements CTE {
		private final Context context;
		private final String name;
		private final CqnSelect statement;
		private final List parameters;

		public StatementCTE(Context context, CdsEntity target, CqnSelect statement, List parameters) {
			this.context = context;
			this.statement = statement;
			this.parameters = parameters;
			this.name = this.context.getDbContext().getSqlMapping(target).cteName();
		}

		public static StatementCTE buildForCalculatedElements(Context context, QatEntityNode node,
				List parameters) {
			CdsEntity target = node.rowType();
			SqlMapping sqlMapping = context.getDbContext().getSqlMapping(target);

			// All calculated elements selected by this node are replaced
			// with refs aliased as if they are the plain columns.
			List items = target.elements().filter(CdsElement::isCalculated)
					.map(e -> CQL.get(e.getName()).as(sqlMapping.columnLikeAlias(e))).collect(Collectors.toList());

			items.add(CQL.plain(QatBuilder.ROOT_ALIAS + ".*").withoutAlias());

			// Everything that points to calculated elements replaced with expressions
			// defining them
			CqnSelect statement = CQL.copy(Select.from(target).columns(items),
					new CqnCalculatedElementsSubstitutor(target));
			return new StatementCTE(context, target, statement, parameters);
		}

		@Override
		public String render() {
			SQLStatement sqlStatement = new SelectStatementBuilder(context, parameters, statement, new ArrayDeque<>())
					.build();
			return name + " as (" + sqlStatement.sql() + ")";
		}

		@Override
		public String name() {
			return name;
		}
	}

	/**
	 * 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