QueryExpressionRenderer.java

/*
 *    Copyright 2016-2024 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.dynamic.sql.select.render;

import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import org.mybatis.dynamic.sql.BasicColumn;
import org.mybatis.dynamic.sql.TableExpression;
import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator;
import org.mybatis.dynamic.sql.render.GuaranteedTableAliasCalculator;
import org.mybatis.dynamic.sql.render.RenderingContext;
import org.mybatis.dynamic.sql.render.TableAliasCalculator;
import org.mybatis.dynamic.sql.select.GroupByModel;
import org.mybatis.dynamic.sql.select.HavingModel;
import org.mybatis.dynamic.sql.select.QueryExpressionModel;
import org.mybatis.dynamic.sql.select.join.JoinModel;
import org.mybatis.dynamic.sql.util.FragmentAndParameters;
import org.mybatis.dynamic.sql.util.FragmentCollector;
import org.mybatis.dynamic.sql.util.StringUtilities;
import org.mybatis.dynamic.sql.where.EmbeddedWhereModel;

public class QueryExpressionRenderer {
    private final QueryExpressionModel queryExpression;
    private final TableExpressionRenderer tableExpressionRenderer;
    private final RenderingContext renderingContext;

    private QueryExpressionRenderer(Builder builder) {
        queryExpression = Objects.requireNonNull(builder.queryExpression);
        TableAliasCalculator childTableAliasCalculator = calculateChildTableAliasCalculator(queryExpression);

        renderingContext = builder.renderingContext.withChildTableAliasCalculator(childTableAliasCalculator);

        tableExpressionRenderer = new TableExpressionRenderer.Builder()
                .withRenderingContext(renderingContext)
                .build();
    }

    /**
     * This function calculates a table alias calculator to use in the current context. There are several
     * possibilities: this could be a renderer for a top level select statement, or it could be a renderer for a table
     * expression in a join, or a column to sub query where condition, or it could be a renderer for a select
     * statement in an "exists" condition in a where clause.
     *
     * <p>In the case of conditions in a where clause, we will have a parent table alias calculator. This will give
     * visibility to the aliases in the outer select statement to this renderer so columns in aliased tables can be
     * used in where clause sub query conditions without having to re-specify the alias.
     *
     * <p>Another complication is that we calculate aliases differently if there are joins and sub queries. The
     * cases are as follows:
     *
     * <ol>
     *     <li>If there are no joins, then we will only use aliases that are explicitly set by the user</li>
     *     <lI>If there are joins and sub queries, we will also only use explicit aliases</lI>
     *     <li>If there are joins, but no sub queries, then we will automatically use the table name
     *     as an alias if no explicit alias has been specified</li>
     * </ol>
     *
     * @param queryExpression the model to render
     * @return a table alias calculator appropriate for this context
     */
    private TableAliasCalculator calculateChildTableAliasCalculator(QueryExpressionModel queryExpression) {
        return queryExpression.joinModel()
                .map(JoinModel::containsSubQueries)
                .map(this::calculateTableAliasCalculatorWithJoins)
                .orElseGet(this::explicitTableAliasCalculator);
    }

    private TableAliasCalculator calculateTableAliasCalculatorWithJoins(boolean hasSubQueries) {
        if (hasSubQueries) {
            // if there are subqueries, we cannot use the table name automatically
            // so all aliases must be specified
            return explicitTableAliasCalculator();
        } else {
            // without subqueries, we can automatically use table names as aliases
            return guaranteedTableAliasCalculator();
        }
    }

    private TableAliasCalculator explicitTableAliasCalculator() {
        return ExplicitTableAliasCalculator.of(queryExpression.tableAliases());
    }

    private TableAliasCalculator guaranteedTableAliasCalculator() {
        return GuaranteedTableAliasCalculator.of(queryExpression.tableAliases());
    }

    public FragmentAndParameters render() {
        FragmentCollector fragmentCollector = new FragmentCollector();

        fragmentCollector.add(calculateQueryExpressionStart());
        calculateJoinClause().ifPresent(fragmentCollector::add);
        calculateWhereClause().ifPresent(fragmentCollector::add);
        calculateGroupByClause().ifPresent(fragmentCollector::add);
        calculateHavingClause().ifPresent(fragmentCollector::add);

        return fragmentCollector.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$
    }

    private FragmentAndParameters calculateQueryExpressionStart() {
        FragmentAndParameters columnList = calculateColumnList();

        String start = queryExpression.connector().map(StringUtilities::spaceAfter).orElse("") //$NON-NLS-1$
                + "select " //$NON-NLS-1$
                + (queryExpression.isDistinct() ? "distinct " : "") //$NON-NLS-1$ //$NON-NLS-2$
                + columnList.fragment()
                + " from "; //$NON-NLS-1$

        FragmentAndParameters renderedTable = renderTableExpression(queryExpression.table());
        start += renderedTable.fragment();

        return FragmentAndParameters.withFragment(start)
                .withParameters(renderedTable.parameters())
                .withParameters(columnList.parameters())
                .build();
    }

    private FragmentAndParameters calculateColumnList() {
        return queryExpression.columns()
                .map(this::renderColumnAndAlias)
                .collect(FragmentCollector.collect())
                .toFragmentAndParameters(Collectors.joining(", ")); //$NON-NLS-1$
    }

    private FragmentAndParameters renderColumnAndAlias(BasicColumn selectListItem) {
        FragmentAndParameters renderedColumn = selectListItem.render(renderingContext);

        String nameAndTableAlias = selectListItem.alias().map(a -> renderedColumn.fragment() + " as " + a) //$NON-NLS-1$
                .orElse(renderedColumn.fragment());

        return FragmentAndParameters.withFragment(nameAndTableAlias)
                .withParameters(renderedColumn.parameters())
                .build();
    }

    private FragmentAndParameters renderTableExpression(TableExpression table) {
        return table.accept(tableExpressionRenderer);
    }

    private Optional<FragmentAndParameters> calculateJoinClause() {
        return queryExpression.joinModel().map(this::renderJoin);
    }

    private FragmentAndParameters renderJoin(JoinModel joinModel) {
        return JoinRenderer.withJoinModel(joinModel)
                .withTableExpressionRenderer(tableExpressionRenderer)
                .withRenderingContext(renderingContext)
                .build()
                .render();
    }

    private Optional<FragmentAndParameters> calculateWhereClause() {
        return queryExpression.whereModel().flatMap(this::renderWhereClause);
    }

    private Optional<FragmentAndParameters> renderWhereClause(EmbeddedWhereModel whereModel) {
        return whereModel.render(renderingContext);
    }

    private Optional<FragmentAndParameters> calculateGroupByClause() {
        return queryExpression.groupByModel().map(this::renderGroupBy);
    }

    private FragmentAndParameters renderGroupBy(GroupByModel groupByModel) {
        return groupByModel.columns()
                .map(this::renderColumn)
                .collect(FragmentCollector.collect())
                .toFragmentAndParameters(
                        Collectors.joining(", ", "group by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$)
    }

    private FragmentAndParameters renderColumn(BasicColumn column) {
        return column.render(renderingContext);
    }

    private Optional<FragmentAndParameters> calculateHavingClause() {
        return queryExpression.havingModel().flatMap(this::renderHavingClause);
    }

    private Optional<FragmentAndParameters> renderHavingClause(HavingModel havingModel) {
        return HavingRenderer.withHavingModel(havingModel)
                .withRenderingContext(renderingContext)
                .build()
                .render();
    }

    public static Builder withQueryExpression(QueryExpressionModel model) {
        return new Builder().withQueryExpression(model);
    }

    public static class Builder {
        private QueryExpressionModel queryExpression;
        private RenderingContext renderingContext;

        public Builder withRenderingContext(RenderingContext renderingContext) {
            this.renderingContext = renderingContext;
            return this;
        }

        public Builder withQueryExpression(QueryExpressionModel queryExpression) {
            this.queryExpression = queryExpression;
            return this;
        }

        public QueryExpressionRenderer build() {
            return new QueryExpressionRenderer(this);
        }
    }
}