All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.sap.cds.impl.qat.FromClauseBuilder Maven / Gradle / Ivy
/**************************************************************************
* (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 extends CqnReference.Segment> 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();
}
}
}