com.sap.cds.impl.qat.FromClauseBuilder Maven / Gradle / Ivy
The newest version!
/**************************************************************************
* (C) 2019-2024 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 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<>(), true, 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();
}
}
}