com.sap.cds.impl.InlineCountProcessorFactory Maven / Gradle / Ivy
/*******************************************************************
* © 2024 SAP SE or an SAP affiliate company. All rights reserved. *
*******************************************************************/
package com.sap.cds.impl;
import static com.sap.cds.DataStoreConfiguration.INLINE_COUNT;
import static com.sap.cds.DataStoreConfiguration.INLINE_COUNT_AUTO;
import static com.sap.cds.DataStoreConfiguration.INLINE_COUNT_QUERY;
import static com.sap.cds.DataStoreConfiguration.INLINE_COUNT_WINDOW_FUNCTION;
import java.util.List;
import java.util.Map;
import com.google.common.annotations.VisibleForTesting;
import com.sap.cds.DataStoreConfiguration;
import com.sap.cds.Result;
import com.sap.cds.ResultBuilder;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnSelectListValue;
import com.sap.cds.ql.cqn.Modifier;
import com.sap.cds.ql.impl.SelectBuilder;
import com.sap.cds.reflect.CdsBaseType;
import com.sap.cds.util.CqnStatementUtils.Count;
public class InlineCountProcessorFactory {
private final CdsDataStoreImpl dataStore;
private final DataStoreConfiguration cfg;
InlineCountProcessorFactory(CdsDataStoreImpl dataStore, DataStoreConfiguration cfg) {
this.dataStore = dataStore;
this.cfg = cfg;
}
private static final InlineCountProcessor noOp = new InlineCountProcessor() {
};
private static final InlineCountProcessor rowCounter = new InlineCountProcessor() {
@Override
public ResultBuilder execute(ResultBuilder result, Map paramValues) {
result.inlineCount(result.rowCount());
return result;
}
};
public InlineCountProcessor create(CqnSelect query) {
if (!query.hasInlineCount()) {
return noOp;
}
if (!query.hasLimit()) {
return rowCounter;
}
if (query.top() == 0) {
return new QueryProcessor(query);
}
String inlineCountConfig = cfg.getProperty(INLINE_COUNT, INLINE_COUNT_AUTO);
return switch (inlineCountConfig) {
case INLINE_COUNT_QUERY -> new QueryProcessor(query);
case INLINE_COUNT_WINDOW_FUNCTION -> new WindowFunctionProcessor(query);
default -> !hasFilter(query) ? new QueryProcessor(query) // count(*) is very quick if there's no filter
: new WindowFunctionProcessor(query);
};
}
private static boolean hasFilter(CqnSelect query) {
return query.where().isPresent() || query.search().isPresent()
|| query.from().isRef() && query.ref().targetSegment().filter().isPresent();
}
private class QueryProcessor implements InlineCountProcessor {
private CqnSelect select;
QueryProcessor(CqnSelect select) {
this.select = select;
}
@Override
public ResultBuilder execute(ResultBuilder result, Map paramValues) {
long rowCount = result.rowCount();
if (requiresInlineCountQuery(select.top(), select.skip(), rowCount)) {
result.inlineCount(executeInlineCountQuery(select, paramValues));
} else {
result.inlineCount(rowCount);
}
return result;
}
private long executeInlineCountQuery(CqnSelect select, Map paramValues) {
CqnSelect inlineCountQuery = inlineCountQuery(select);
Result result = dataStore.executeResolvedQuery(inlineCountQuery, paramValues).result();
return result.single(Count.class).getCount();
}
private static CqnSelect inlineCountQuery(CqnSelect select) {
Select> countQuery = SelectBuilder.from(select.from());
select.where().ifPresent(countQuery::where);
select.search().ifPresent(countQuery::search);
select.having().ifPresent(countQuery::having);
if (select.isDistinct() || !select.groupBy().isEmpty()) {
countQuery.columns(select.items());
countQuery.groupBy(select.groupBy());
countQuery.distinct();
countQuery = Select.from(countQuery);
}
return countQuery.columns(Count.ALL);
}
}
private class WindowFunctionProcessor implements InlineCountProcessor {
private static final String $COUNT = "_$count";
private static final CqnSelectListValue COUNT_ALL_OVER = CQL.plain("COUNT(*) OVER()").type(CdsBaseType.INT64)
.as($COUNT);
private final CqnSelect select;
WindowFunctionProcessor(CqnSelect select) {
this.select = select;
}
@Override
public CqnSelect prepare(CqnSelect select) {
return CQL.copy(select, new Modifier() {
@Override
public List items(List items) {
items.add(COUNT_ALL_OVER);
return items;
}
});
}
@Override
public ResultBuilder execute(ResultBuilder result, Map paramValues) {
if (result.rowCount() == 0 && select.skip() > 0) {
// all rows skipped. Fallback to query
return new QueryProcessor(select).execute(result, paramValues);
}
Result tmpResult = result.result();
long inlineCount = tmpResult.first().map(r -> (Long) r.get($COUNT)).orElse(0l);
result.inlineCount(inlineCount);
tmpResult.stream().forEach(r -> r.remove($COUNT));
return result;
}
}
@VisibleForTesting
static boolean requiresInlineCountQuery(long top, long skip, long rowCount) {
return skip > 0 || top <= rowCount;
}
}