/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2013, Red Hat Inc. or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.dialect.pagination;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.hibernate.engine.spi.RowSelection;
import org.hibernate.internal.util.StringHelper;

LIMIT clause handler compatible with SQL Server 2005 and later.
Author:Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
/** * LIMIT clause handler compatible with SQL Server 2005 and later. * * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) */
public class SQLServer2005LimitHandler extends AbstractLimitHandler { private static final String SELECT = "select"; private static final String SELECT_WITH_SPACE = SELECT + ' '; private static final String FROM = "from"; private static final String DISTINCT = "distinct"; private static final String ORDER_BY = "order by"; private static final Pattern ALIAS_PATTERN = Pattern.compile( "(?i)\\sas\\s(.)+$" ); // Flag indicating whether TOP(?) expression has been added to the original query. private boolean topAdded; // True if offset greater than 0. private boolean hasOffset = true;
Constructs a SQLServer2005LimitHandler
Params:
  • sql – The SQL
  • selection – The row selection options
/** * Constructs a SQLServer2005LimitHandler * * @param sql The SQL * @param selection The row selection options */
public SQLServer2005LimitHandler(String sql, RowSelection selection) { super( sql, selection ); } @Override public boolean supportsLimit() { return true; } @Override public boolean useMaxForLimit() { return true; } @Override public boolean supportsLimitOffset() { return true; } @Override public boolean supportsVariableLimit() { return true; } @Override public int convertToFirstRowValue(int zeroBasedFirstResult) { // Our dialect paginated results aren't zero based. The first row should get the number 1 and so on return zeroBasedFirstResult + 1; }
Add a LIMIT clause to the given SQL SELECT (HHH-2655: ROW_NUMBER for Paging) The LIMIT SQL will look like:
WITH query AS (
  SELECT inner_query.*
       , ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__
    FROM ( original_query_with_top_if_order_by_present_and_all_aliased_columns ) inner_query
)
SELECT alias_list FROM query WHERE __hibernate_row_nr__ >= offset AND __hibernate_row_nr__ < offset + last
When offset equals 0, only TOP(?) expression is added to the original query.
Returns:A new SQL statement with the LIMIT clause applied.
/** * Add a LIMIT clause to the given SQL SELECT (HHH-2655: ROW_NUMBER for Paging) * * The LIMIT SQL will look like: * * <pre> * WITH query AS ( * SELECT inner_query.* * , ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ * FROM ( original_query_with_top_if_order_by_present_and_all_aliased_columns ) inner_query * ) * SELECT alias_list FROM query WHERE __hibernate_row_nr__ >= offset AND __hibernate_row_nr__ < offset + last * </pre> * * When offset equals {@literal 0}, only <code>TOP(?)</code> expression is added to the original query. * * @return A new SQL statement with the LIMIT clause applied. */
@Override public String getProcessedSql() { final StringBuilder sb = new StringBuilder( sql ); if ( sb.charAt( sb.length() - 1 ) == ';' ) { sb.setLength( sb.length() - 1 ); } if ( LimitHelper.hasFirstRow( selection ) ) { final String selectClause = fillAliasInSelectClause( sb ); final int orderByIndex = shallowIndexOfWord( sb, ORDER_BY, 0 ); if ( orderByIndex > 0 ) { // ORDER BY requires using TOP. addTopExpression( sb ); } encloseWithOuterQuery( sb ); // Wrap the query within a with statement: sb.insert( 0, "WITH query AS (" ).append( ") SELECT " ).append( selectClause ).append( " FROM query " ); sb.append( "WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?" ); } else { hasOffset = false; addTopExpression( sb ); } return sb.toString(); } @Override public int bindLimitParametersAtStartOfQuery(PreparedStatement statement, int index) throws SQLException { if ( topAdded ) { // Binding TOP(?) statement.setInt( index, getMaxOrLimit() - 1 ); return 1; } return 0; } @Override public int bindLimitParametersAtEndOfQuery(PreparedStatement statement, int index) throws SQLException { return hasOffset ? super.bindLimitParametersAtEndOfQuery( statement, index ) : 0; }
Adds missing aliases in provided SELECT clause and returns coma-separated list of them. If query takes advantage of expressions like * or {table}.* inside SELECT clause, method returns *.
Params:
  • sb – SQL query.
Returns:List of aliases separated with comas or *.
/** * Adds missing aliases in provided SELECT clause and returns coma-separated list of them. * If query takes advantage of expressions like {@literal *} or {@literal {table}.*} inside SELECT clause, * method returns {@literal *}. * * @param sb SQL query. * * @return List of aliases separated with comas or {@literal *}. */
protected String fillAliasInSelectClause(StringBuilder sb) { final List<String> aliases = new LinkedList<String>(); final int startPos = shallowIndexOf( sb, SELECT_WITH_SPACE, 0 ); int endPos = shallowIndexOfWord( sb, FROM, startPos ); int nextComa = startPos; int prevComa = startPos; int unique = 0; boolean selectsMultipleColumns = false; while ( nextComa != -1 ) { prevComa = nextComa; nextComa = shallowIndexOf( sb, ",", nextComa ); if ( nextComa > endPos ) { break; } if ( nextComa != -1 ) { final String expression = sb.substring( prevComa, nextComa ); if ( selectsMultipleColumns( expression ) ) { selectsMultipleColumns = true; } else { String alias = getAlias( expression ); if ( alias == null ) { // Inserting alias. It is unlikely that we would have to add alias, but just in case. alias = StringHelper.generateAlias( "page", unique ); sb.insert( nextComa, " as " + alias ); int aliasExprLength = ( " as " + alias ).length(); ++unique; nextComa += aliasExprLength; endPos += aliasExprLength; } aliases.add( alias ); } ++nextComa; } } // Processing last column. // Refreshing end position, because we might have inserted new alias. endPos = shallowIndexOfWord( sb, FROM, startPos ); final String expression = sb.substring( prevComa, endPos ); if ( selectsMultipleColumns( expression ) ) { selectsMultipleColumns = true; } else { String alias = getAlias( expression ); if ( alias == null ) { // Inserting alias. It is unlikely that we would have to add alias, but just in case. alias = StringHelper.generateAlias( "page", unique ); sb.insert( endPos - 1, " as " + alias ); } aliases.add( alias ); } // In case of '*' or '{table}.*' expressions adding an alias breaks SQL syntax, returning '*'. return selectsMultipleColumns ? "*" : StringHelper.join( ", ", aliases.iterator() ); }
Params:
  • expression – Select expression.
Returns:true when expression selects multiple columns, false otherwise.
/** * @param expression Select expression. * * @return {@code true} when expression selects multiple columns, {@code false} otherwise. */
private boolean selectsMultipleColumns(String expression) { final String lastExpr = expression.trim().replaceFirst( "(?i)(.)*\\s", "" ); return "*".equals( lastExpr ) || lastExpr.endsWith( ".*" ); }
Returns alias of provided single column selection or null if not found. Alias should be preceded with AS keyword.
Params:
  • expression – Single column select expression.
Returns:Column alias.
/** * Returns alias of provided single column selection or {@code null} if not found. * Alias should be preceded with {@code AS} keyword. * * @param expression Single column select expression. * * @return Column alias. */
private String getAlias(String expression) { final Matcher matcher = ALIAS_PATTERN.matcher( expression ); if ( matcher.find() ) { // Taking advantage of Java regular expressions greedy behavior while extracting the last AS keyword. // Note that AS keyword can appear in CAST operator, e.g. 'cast(tab1.col1 as varchar(255)) as col1'. return matcher.group( 0 ).replaceFirst( "(?i)(.)*\\sas\\s", "" ).trim(); } return null; }
Encloses original SQL statement with outer query that provides __hibernate_row_nr__ column.
Params:
  • sql – SQL query.
/** * Encloses original SQL statement with outer query that provides {@literal __hibernate_row_nr__} column. * * @param sql SQL query. */
protected void encloseWithOuterQuery(StringBuilder sql) { sql.insert( 0, "SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " ); sql.append( " ) inner_query " ); }
Adds TOP expression. Parameter value is bind in bindLimitParametersAtStartOfQuery(PreparedStatement, int) method.
Params:
  • sql – SQL query.
/** * Adds {@code TOP} expression. Parameter value is bind in * {@link #bindLimitParametersAtStartOfQuery(PreparedStatement, int)} method. * * @param sql SQL query. */
protected void addTopExpression(StringBuilder sql) { final int distinctStartPos = shallowIndexOfWord( sql, DISTINCT, 0 ); if ( distinctStartPos > 0 ) { // Place TOP after DISTINCT. sql.insert( distinctStartPos + DISTINCT.length(), " TOP(?)" ); } else { final int selectStartPos = shallowIndexOf( sql, SELECT_WITH_SPACE, 0 ); // Place TOP after SELECT. sql.insert( selectStartPos + SELECT.length(), " TOP(?)" ); } topAdded = true; }
Returns index of the first case-insensitive match of search term surrounded by spaces that is not enclosed in parentheses.
Params:
  • sb – String to search.
  • search – Search term.
  • fromIndex – The index from which to start the search.
Returns:Position of the first match, or -1 if not found.
/** * Returns index of the first case-insensitive match of search term surrounded by spaces * that is not enclosed in parentheses. * * @param sb String to search. * @param search Search term. * @param fromIndex The index from which to start the search. * * @return Position of the first match, or {@literal -1} if not found. */
private static int shallowIndexOfWord(final StringBuilder sb, final String search, int fromIndex) { final int index = shallowIndexOf( sb, ' ' + search + ' ', fromIndex ); // In case of match adding one because of space placed in front of search term. return index != -1 ? ( index + 1 ) : -1; }
Returns index of the first case-insensitive match of search term that is not enclosed in parentheses.
Params:
  • sb – String to search.
  • search – Search term.
  • fromIndex – The index from which to start the search.
Returns:Position of the first match, or -1 if not found.
/** * Returns index of the first case-insensitive match of search term that is not enclosed in parentheses. * * @param sb String to search. * @param search Search term. * @param fromIndex The index from which to start the search. * * @return Position of the first match, or {@literal -1} if not found. */
private static int shallowIndexOf(StringBuilder sb, String search, int fromIndex) { // case-insensitive match final String lowercase = sb.toString().toLowerCase(Locale.ROOT); final int len = lowercase.length(); final int searchlen = search.length(); int pos = -1; int depth = 0; int cur = fromIndex; do { pos = lowercase.indexOf( search, cur ); if ( pos != -1 ) { for ( int iter = cur; iter < pos; iter++ ) { final char c = sb.charAt( iter ); if ( c == '(' ) { depth = depth + 1; } else if ( c == ')' ) { depth = depth - 1; } } cur = pos + searchlen; } } while ( cur < len && depth != 0 && pos != -1 ); return depth == 0 ? pos : -1; } }