All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.impl.sql.SelectStatementBuilder Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
/**************************************************************************
 * (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 **************************************************************************/
package com.sap.cds.impl.sql;

import static com.sap.cds.impl.sql.SQLStatementBuilder.commaSeparated;
import static com.sap.cds.impl.sql.SpaceSeparatedCollector.joining;
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.CdsModelUtils.entity;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Stream;

import com.google.common.collect.Streams;
import com.sap.cds.SessionContext;
import com.sap.cds.impl.Context;
import com.sap.cds.impl.PreparedCqnStmt.Parameter;
import com.sap.cds.impl.builder.model.Conjunction;
import com.sap.cds.impl.localized.LocaleUtils;
import com.sap.cds.impl.qat.FromClauseBuilder;
import com.sap.cds.impl.qat.QatBuilder;
import com.sap.cds.impl.qat.QatSelectableNode;
import com.sap.cds.impl.qat.Ref2QualifiedColumn;
import com.sap.cds.jdbc.spi.StatementResolver;
import com.sap.cds.ql.CQL;
import com.sap.cds.ql.cqn.CqnLock;
import com.sap.cds.ql.cqn.CqnPredicate;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnSelectListItem;
import com.sap.cds.ql.cqn.CqnSortSpecification;
import com.sap.cds.ql.cqn.CqnSource;
import com.sap.cds.ql.cqn.CqnStructuredTypeRef;
import com.sap.cds.ql.cqn.CqnValue;
import com.sap.cds.reflect.CdsBaseType;
import com.sap.cds.reflect.CdsEntity;
import com.sap.cds.util.CqnStatementUtils;

public class SelectStatementBuilder implements SQLStatementBuilder {

	private final Context context;
	private final CqnSelect select;
	private final Deque outer;
	private final StatementResolver statementResolver;
	private final SessionContext sessionContext;
	private final List params;

	private Ref2QualifiedColumn aliasResolver;
	private TokenToSQLTransformer toSQL;
	private final FromClauseBuilder fromClauseBuilder;
	private final LocaleUtils localeUtils;
	private final CdsEntity targetEntity;
	private final Collating collating;

	public SelectStatementBuilder(Context context, List params, CqnSelect select,
			Deque outer) {
		this(context, params, select, outer, false);
	}

	public SelectStatementBuilder(Context context, List params, CqnSelect select,
			Deque outer, boolean noCollating) {
		this.context = context;
		this.params = params;
		this.select = select;
		this.outer = append(outer, new QatBuilder(context, select, outer.size()).create());
		this.statementResolver = context.getDbContext().getStatementResolver();
		this.sessionContext = context.getSessionContext();
		this.fromClauseBuilder = new FromClauseBuilder(context, params, select.hints());
		this.localeUtils = new LocaleUtils(context.getCdsModel(), context.getDataStoreConfiguration());
		this.targetEntity = select.from().isRef() ? entity(context.getCdsModel(), select.from().asRef()) : null;
		this.collating = determineCollating(select, localeUtils, statementResolver,
				context.getSessionContext().getLocale(), noCollating);
	}

	@Override
	public SQLStatement build() {
		aliasResolver = new Ref2QualifiedColumn(context.getDbContext()::getSqlMapping, outer, localeUtils);
		// TODO propagate statement-level collation from inner to outer query
		toSQL = new TokenToSQLTransformer(context, params, aliasResolver, outer, true);
		List snippets = new ArrayList<>();
		with().forEach(snippets::add);
		snippets.add("SELECT");
		distinct().forEach(snippets::add);
		columns().forEach(snippets::add);
		from().forEach(snippets::add);
		where().forEach(snippets::add);
		groupBy().forEach(snippets::add);
		having().forEach(snippets::add);
		orderBy().forEach(snippets::add);
		limit().forEach(snippets::add);
		lock().forEach(snippets::add);
		withCollation().ifPresent(snippets::add);
		hints().forEach(snippets::add);
		String sql = snippets.stream().collect(joining());

		return new SQLStatement(sql, params);
	}

	private static boolean hasCollatingOffHint(CqnSelect select) {
		return COLLATING_OFF.equals(select.hints().get(COLLATING));
	}

	private Optional withCollation() {
		if (collating == Collating.STATEMENT) {
			Locale locale = context.getSessionContext().getLocale();
			return statementResolver.statementWideCollation(select, locale);
		}
		return Optional.empty();
	}

	private Stream distinct() {
		if (select.isDistinct()) {
			return Stream.of("DISTINCT");
		}

		return Stream.empty();
	}

	private Stream columns() {
		List items = select.items();
		if (items.isEmpty()) {
			throw new IllegalStateException("select * not expected");
		}

		Stream.Builder sql = Stream.builder();
		items.stream().flatMap(CqnSelectListItem::ofValue).forEach(slv -> {
			sql.add(",");
			sql.add(toSQL.apply(slv.value()));
			slv.alias().ifPresent(a -> {
				sql.add("as");
				sql.add(SQLHelper.delimited(a));
			});
		});

		return sql.build().skip(1);
	}

	private Stream with() {
		return fromClauseBuilder.with(outer);
	}

	private Stream from() {
		return Streams.concat(Stream.of("FROM"), fromClauseBuilder.sql(outer));
	}

	private Stream where() {
		Optional where = Conjunction.and(select.where(), select.search());
		CqnSource source = select.from();
		if (source.isRef()) {
			CqnStructuredTypeRef ref = source.asRef();
			where = Conjunction.and(where, ref.targetSegment().filter());
		}

		if (collating == Collating.COLLATE) {
			Locale locale = sessionContext.getLocale();
			statementResolver.collate(locale).ifPresent(aliasResolver::startCollate);
		}
		Stream.Builder sql = Stream.builder();
		where.map(w -> toSQL.toSQL(targetEntity, w)).ifPresent(s -> {
			sql.add("WHERE");
			sql.add(s);
		});
		aliasResolver.stopCollate();

		return sql.build();
	}

	private Stream groupBy() {
		List groupByTokens = select.groupBy();
		if (groupByTokens.isEmpty()) {
			return Stream.empty();
		}
		return Streams.concat(Stream.of("GROUP BY"), commaSeparated(groupByTokens.stream(), toSQL::apply));
	}

	private Stream having() {
		Optional having = select.having();

		if (collating == Collating.COLLATE) {
			Locale locale = sessionContext.getLocale();
			statementResolver.collate(locale).ifPresent(aliasResolver::startCollate);
		}
		Stream.Builder sql = Stream.builder();
		having.map(h -> toSQL.toSQL(targetEntity, h)).ifPresent(s -> {
			sql.add("HAVING");
			sql.add(s);
		});
		aliasResolver.stopCollate();

		return sql.build();
	}

	private Stream orderBy() {
		List orderBy = select.orderBy();
		if (orderBy.isEmpty()) {
			return Stream.empty();
		}
		return Streams.concat(Stream.of("ORDER BY"), commaSeparated(orderBy.stream(), this::sort));
	}

	private String sort(CqnSortSpecification o) {
		StringBuilder sort = new StringBuilder(toSQL.apply(o.value()));
		Optional collateClause = statementResolver.collate(o, sessionContext.getLocale());
		if (collating == Collating.COLLATE && collateClause.isPresent() && requiresCollate(o.value())) {
			sort.append(" " + collateClause.get());
		}
		switch (o.order()) {
			case DESC:
				sort.append(" DESC NULLS LAST");
				break;
			case DESC_NULLS_FIRST:
				sort.append(" DESC NULLS FIRST");
				break;
			case ASC_NULLS_LAST:
				sort.append(" NULLS LAST");
				break;
			default: // ASC
				sort.append(" NULLS FIRST");
		}
		return sort.toString();
	}

	private boolean requiresCollate(CqnValue value) {
		if (targetEntity == null) {
			return false;
		}
		if (value.isRef()) {
			return localeUtils.requiresCollate(targetEntity, value.asRef());
		}
		return CqnStatementUtils.getCdsType(targetEntity, value).filter(t -> CdsBaseType.STRING == t).isPresent();
	}

	private Stream limit() {
		long top = select.top();
		long skip = select.skip();

		Stream.Builder sql = Stream.builder();
		if (top < 0 && skip > 0) {
			top = Integer.MAX_VALUE;
		}

		if (top >= 0) {
			sql.add("LIMIT");
			sql.add(toSQL.apply(CQL.constant(top)));
		}

		if (skip > 0) {
			sql.add("OFFSET");
			sql.add(toSQL.apply(CQL.val(skip)));
		}

		return sql.build();
	}

	private Stream lock() {
		Stream stream = select.getLock().stream();

		return stream.flatMap(statementResolver::lockClause);
	}

	private Stream hints() {
		Stream.Builder sql = Stream.builder();
		statementResolver.hints(select.hints()).ifPresent(sql::add);

		return sql.build();
	}

	private static Deque append(Deque prefix, QatSelectableNode newRoot) {
		ArrayDeque roots = new ArrayDeque<>(prefix);
		roots.add(newRoot);

		return roots;
	}

	private static Collating determineCollating(CqnSelect select, LocaleUtils localeUtils,
			StatementResolver statementResolver, Locale locale, boolean noCollating) {
		if (locale == null || noCollating || hasCollatingOffHint(select)) {
			return Collating.OFF;
		}

		return statementResolver.supportsStatementWideCollation() && localeUtils.requiresCollationClause(select, locale)
				? Collating.STATEMENT
				: Collating.COLLATE;
	}

	private enum Collating {
		STATEMENT, COLLATE, OFF
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy