com.sap.cds.jdbc.hana.HanaStatementResolver Maven / Gradle / Ivy
/************************************************************************
* © 2020-2024 SAP SE or an SAP affiliate company. All rights reserved. *
************************************************************************/
package com.sap.cds.jdbc.hana;
import static com.sap.cds.DataStoreConfiguration.IGNORE_LOCALE_ON_HANA;
import static com.sap.cds.impl.sql.SQLHelper.commaSeparated;
import static com.sap.cds.jdbc.hana.HanaDbContext.NO_USE_HEX_PLAN;
import static com.sap.cds.jdbc.hana.HanaDbContext.USE_HEX_PLAN;
import static com.sap.cds.ql.impl.SelectBuilder.COLLATING;
import static com.sap.cds.ql.impl.SelectBuilder.COLLATING_OFF;
import static com.sap.cds.util.CqnStatementUtils.hasLimit;
import static java.util.stream.Collectors.joining;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sap.cds.CdsException;
import com.sap.cds.DataStoreConfiguration;
import com.sap.cds.impl.localized.LocaleUtils;
import com.sap.cds.impl.sql.SQLHelper;
import com.sap.cds.jdbc.generic.GenericStatementResolver;
import com.sap.cds.jdbc.hana.hierarchies.HanaHierarchyResolver;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.StructuredType;
import com.sap.cds.ql.cqn.CqnLock.Mode;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSortSpecification;
public class HanaStatementResolver extends GenericStatementResolver {
private static final Logger logger = LoggerFactory.getLogger(HanaStatementResolver.class);
private static final Properties COLLATIONS = loadCollations();
private final boolean ignoreLocale;
private final boolean hanaCloud;
private final boolean optimizeForHexEngine;
public HanaStatementResolver(DataStoreConfiguration dataStoreConfiguration, int majorVersion,
boolean optimizeForHexEngine) {
this.optimizeForHexEngine = optimizeForHexEngine;
this.hanaCloud = majorVersion >= 4;
this.ignoreLocale = dataStoreConfiguration.getProperty(IGNORE_LOCALE_ON_HANA, false);
}
private static Properties loadCollations() {
try (InputStream in = HanaStatementResolver.class.getClassLoader()
.getResourceAsStream("hana/collations.properties")) {
Properties collations = new Properties();
collations.load(in);
return collations;
} catch (Exception e) {
throw new CdsException("Failed to load HANA collations");
}
}
@Override
public Optional collate(CqnSortSpecification o, Locale locale) {
if (ignoreLocale || locale == null) {
return Optional.empty();
}
return collation("COLLATE ", locale);
}
private static Optional collation(String prefix, Locale locale) {
String collationName = getCollationName(locale);
if (collationName == null) {
logger.warn("Missing collation name for locale \"{}\"", locale);
return Optional.empty();
}
return Optional.of(prefix + collationName);
}
private static String getCollationName(Locale locale) {
String language = locale.getLanguage();
if ("zh".equals(language) || "hr".equals(language)) {
String collationName = COLLATIONS.getProperty(locale.toLanguageTag());
if (collationName != null) {
return collationName;
}
}
return COLLATIONS.getProperty(language);
}
@Override
public Optional statementWideCollation(CqnSelect select, Locale locale) {
if (ignoreLocale || locale == null) {
return Optional.empty();
}
Map hints = select.hints();
if (COLLATING_OFF.equals(hints.get(COLLATING))) {
return Optional.empty();
}
if (hints.containsKey(NO_USE_HEX_PLAN)) {
return Optional.of(parametersLocale(locale));
}
if (onHex(hints) && hanaCloud) {
return collation("WITH COLLATION ", locale);
}
return Optional.of(parametersLocale(locale));
}
private boolean onHex(Map hints) {
return optimizeForHexEngine || hints.containsKey(USE_HEX_PLAN);
}
@Override
public boolean supportsStatementWideCollation() {
return true;
}
private static String parametersLocale(Locale locale) {
return "with parameters('LOCALE' = " + SQLHelper.literal(LocaleUtils.getLocaleString(locale)) + ")";
}
/**
* HANA Upsert: Updates rows in a table or inserts new rows.
*
* https://help.sap.com/docs/HANA_SERVICE_CF/7c78579ce9b14a669c1f3295b0d8ca16/ea8b6773be584203bcd99da76844c5ed.html
*/
@Override
public String upsert(String table, Stream keyColumns, Stream upsertColumns,
Stream upsertValues) {
String columns = commaSeparated(upsertColumns);
String values = commaSeparated(upsertValues);
return Stream.of("UPSERT", table, columns, "VALUES", values, "WITH PRIMARY KEY").collect(joining(" "));
}
@Override
public String lockMode(Mode mode) {
final String clause;
switch (mode) {
case SHARED:
if (hanaCloud) {
clause = "FOR SHARE LOCK";
break;
}
default:
clause = "FOR UPDATE";
}
return clause;
}
@Override
public Optional timeoutClause(int timeoutSeconds) {
if (timeoutSeconds > 0) {
return Optional.of("WAIT " + timeoutSeconds);
} else {
return Optional.of("NOWAIT");
}
}
@Override
public Optional hints(Map hints) {
List hanaHints = hints.entrySet().stream()
.filter(h -> h.getKey().startsWith("hdb.") && h.getValue().equals(true))
.map(h -> h.getKey().substring(4)).toList();
if (hanaHints.isEmpty()) {
return Optional.empty();
}
return Optional.of(hanaHints.stream().collect(Collectors.joining(", ", "WITH HINT(", ")")));
}
@Override
public CqnSelect preOptimize(CqnSelect select) {
Map hints = select.hints();
if (onHex(hints) && cantRunOnHex(select, hints)) {
Select> copy = Select.copy(select);
hints = new HashMap<>(hints);
hints.remove(USE_HEX_PLAN);
hints.put(NO_USE_HEX_PLAN, true);
copy.hints(hints);
return copy;
}
return select;
}
@Override
public Select> applyTransformations(Select> select) {
HanaHierarchyResolver resolver = new HanaHierarchyResolver(select);
resolver.applyTransformations();
return resolver.get();
}
private boolean cantRunOnHex(CqnSelect select, Map hints) {
if (select.from().isSelect()) {
CqnSelect inner = select.from().asSelect();
if (hasLimit(inner)) { // HDBHEX-5028
if (logger.isDebugEnabled()) {
logger.debug("Executing on legacy engine as statment has nested limit: {}", select.toJson());
}
return true;
}
}
return false;
}
}