HHH-7440, HHH-7368, HHH-7369, HHH-7370 - Redesign dialect-specific LIMIT clause appliance

This commit is contained in:
Lukasz Antoniak 2012-07-10 21:19:32 +02:00
parent 65d1724433
commit c46daa4cf0
14 changed files with 907 additions and 377 deletions

View File

@ -39,6 +39,7 @@ import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import org.hibernate.engine.spi.RowSelection;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
@ -58,6 +59,8 @@ import org.hibernate.dialect.lock.PessimisticForceIncrementLockingStrategy;
import org.hibernate.dialect.lock.PessimisticReadSelectLockingStrategy; import org.hibernate.dialect.lock.PessimisticReadSelectLockingStrategy;
import org.hibernate.dialect.lock.PessimisticWriteSelectLockingStrategy; import org.hibernate.dialect.lock.PessimisticWriteSelectLockingStrategy;
import org.hibernate.dialect.lock.SelectLockingStrategy; import org.hibernate.dialect.lock.SelectLockingStrategy;
import org.hibernate.dialect.pagination.LegacyLimitHandler;
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.engine.jdbc.LobCreator; import org.hibernate.engine.jdbc.LobCreator;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.exception.spi.ConversionContext; import org.hibernate.exception.spi.ConversionContext;
@ -935,7 +938,9 @@ public abstract class Dialect implements ConversionContext {
* via a SQL clause? * via a SQL clause?
* *
* @return True if this dialect supports some form of LIMIT. * @return True if this dialect supports some form of LIMIT.
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public boolean supportsLimit() { public boolean supportsLimit() {
return false; return false;
} }
@ -945,7 +950,9 @@ public abstract class Dialect implements ConversionContext {
* support specifying an offset? * support specifying an offset?
* *
* @return True if the dialect supports an offset within the limit support. * @return True if the dialect supports an offset within the limit support.
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public boolean supportsLimitOffset() { public boolean supportsLimitOffset() {
return supportsLimit(); return supportsLimit();
} }
@ -955,7 +962,9 @@ public abstract class Dialect implements ConversionContext {
* parameters) for its limit/offset? * parameters) for its limit/offset?
* *
* @return True if bind variables can be used; false otherwise. * @return True if bind variables can be used; false otherwise.
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public boolean supportsVariableLimit() { public boolean supportsVariableLimit() {
return supportsLimit(); return supportsLimit();
} }
@ -965,7 +974,9 @@ public abstract class Dialect implements ConversionContext {
* Does this dialect require us to bind the parameters in reverse order? * Does this dialect require us to bind the parameters in reverse order?
* *
* @return true if the correct order is limit, offset * @return true if the correct order is limit, offset
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public boolean bindLimitParametersInReverseOrder() { public boolean bindLimitParametersInReverseOrder() {
return false; return false;
} }
@ -975,7 +986,9 @@ public abstract class Dialect implements ConversionContext {
* <tt>SELECT</tt> statement, rather than at the end? * <tt>SELECT</tt> statement, rather than at the end?
* *
* @return true if limit parameters should come before other parameters * @return true if limit parameters should come before other parameters
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public boolean bindLimitParametersFirst() { public boolean bindLimitParametersFirst() {
return false; return false;
} }
@ -995,7 +1008,9 @@ public abstract class Dialect implements ConversionContext {
* So essentially, is limit relative from offset? Or is limit absolute? * So essentially, is limit relative from offset? Or is limit absolute?
* *
* @return True if limit is relative from offset; false otherwise. * @return True if limit is relative from offset; false otherwise.
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public boolean useMaxForLimit() { public boolean useMaxForLimit() {
return false; return false;
} }
@ -1005,7 +1020,9 @@ public abstract class Dialect implements ConversionContext {
* to the SQL query. This option forces that the limit be written to the SQL query. * to the SQL query. This option forces that the limit be written to the SQL query.
* *
* @return True to force limit into SQL query even if none specified in Hibernate query; false otherwise. * @return True to force limit into SQL query even if none specified in Hibernate query; false otherwise.
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public boolean forceLimitUsage() { public boolean forceLimitUsage() {
return false; return false;
} }
@ -1017,7 +1034,9 @@ public abstract class Dialect implements ConversionContext {
* @param offset The offset of the limit * @param offset The offset of the limit
* @param limit The limit of the limit ;) * @param limit The limit of the limit ;)
* @return The modified query statement with the limit applied. * @return The modified query statement with the limit applied.
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public String getLimitString(String query, int offset, int limit) { public String getLimitString(String query, int offset, int limit) {
return getLimitString( query, ( offset > 0 || forceLimitUsage() ) ); return getLimitString( query, ( offset > 0 || forceLimitUsage() ) );
} }
@ -1038,7 +1057,9 @@ public abstract class Dialect implements ConversionContext {
* @param query The query to which to apply the limit. * @param query The query to which to apply the limit.
* @param hasOffset Is the query requesting an offset? * @param hasOffset Is the query requesting an offset?
* @return the modified SQL * @return the modified SQL
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
protected String getLimitString(String query, boolean hasOffset) { protected String getLimitString(String query, boolean hasOffset) {
throw new UnsupportedOperationException( "Paged queries not supported by " + getClass().getName()); throw new UnsupportedOperationException( "Paged queries not supported by " + getClass().getName());
} }
@ -1052,16 +1073,27 @@ public abstract class Dialect implements ConversionContext {
* to injecting the limit values into the SQL string. * to injecting the limit values into the SQL string.
* *
* @param zeroBasedFirstResult The user-supplied, zero-based first row offset. * @param zeroBasedFirstResult The user-supplied, zero-based first row offset.
*
* @return The corresponding db/dialect specific offset. * @return The corresponding db/dialect specific offset.
*
* @see org.hibernate.Query#setFirstResult * @see org.hibernate.Query#setFirstResult
* @see org.hibernate.Criteria#setFirstResult * @see org.hibernate.Criteria#setFirstResult
* @deprecated {@link #buildLimitHandler(String, RowSelection)} should be overridden instead.
*/ */
@Deprecated
public int convertToFirstRowValue(int zeroBasedFirstResult) { public int convertToFirstRowValue(int zeroBasedFirstResult) {
return zeroBasedFirstResult; return zeroBasedFirstResult;
} }
/**
* Build delegate managing LIMIT clause.
*
* @param sql SQL query.
* @param selection Selection criteria. {@code null} in case of unlimited number of rows.
* @return LIMIT clause delegate.
*/
public LimitHandler buildLimitHandler(String sql, RowSelection selection) {
return new LegacyLimitHandler( this, sql, selection );
}
// lock acquisition support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // lock acquisition support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -25,14 +25,15 @@ package org.hibernate.dialect;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hibernate.JDBCException; import org.hibernate.JDBCException;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.LockOptions; import org.hibernate.LockOptions;
import org.hibernate.QueryTimeoutException; import org.hibernate.QueryTimeoutException;
import org.hibernate.dialect.function.NoArgSQLFunction; import org.hibernate.dialect.function.NoArgSQLFunction;
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.dialect.pagination.SQLServer2005LimitHandler;
import org.hibernate.engine.spi.RowSelection;
import org.hibernate.exception.LockTimeoutException; import org.hibernate.exception.LockTimeoutException;
import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.internal.util.JdbcExceptionHelper;
@ -42,19 +43,11 @@ import org.hibernate.type.StandardBasicTypes;
* A dialect for Microsoft SQL 2005. (HHH-3936 fix) * A dialect for Microsoft SQL 2005. (HHH-3936 fix)
* *
* @author Yoryos Valotasios * @author Yoryos Valotasios
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/ */
public class SQLServer2005Dialect extends SQLServerDialect { public class SQLServer2005Dialect extends SQLServerDialect {
private static final String SELECT = "select";
private static final String FROM = "from";
private static final String DISTINCT = "distinct";
private static final String ORDER_BY = "order by";
private static final int MAX_LENGTH = 8000; private static final int MAX_LENGTH = 8000;
/**
* Regular expression for stripping alias
*/
private static final Pattern ALIAS_PATTERN = Pattern.compile( "\\sas\\s[^,]+(,?)" );
public SQLServer2005Dialect() { public SQLServer2005Dialect() {
// HHH-3965 fix // HHH-3965 fix
// As per http://www.sql-server-helper.com/faq/sql-server-2005-varchar-max-p01.aspx // As per http://www.sql-server-helper.com/faq/sql-server-2005-varchar-max-p01.aspx
@ -78,139 +71,8 @@ public class SQLServer2005Dialect extends SQLServerDialect {
} }
@Override @Override
public boolean supportsLimitOffset() { public LimitHandler buildLimitHandler(String sql, RowSelection selection) {
return true; return new SQLServer2005LimitHandler( sql, selection );
}
@Override
public boolean bindLimitParametersFirst() {
return false;
}
@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;
}
@Override
public String getLimitString(String query, int offset, int limit) {
// We transform the query to one with an offset and limit if we have an offset and limit to bind
if ( offset > 1 || limit > 1 ) {
return getLimitString( query, true );
}
return query;
}
/**
* 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 (
* original_select_clause_without_distinct_and_order_by,
* ROW_NUMBER() OVER ([ORDER BY CURRENT_TIMESTAMP | original_order_by_clause]) as __hibernate_row_nr__
* original_from_clause
* original_where_clause
* group_by_if_originally_select_distinct
* )
* SELECT * FROM query WHERE __hibernate_row_nr__ >= offset AND __hibernate_row_nr__ < offset + last
* </pre>
*
* @param querySqlString The SQL statement to base the limit query off of.
* @param hasOffset Is the query requesting an offset?
*
* @return A new SQL statement with the LIMIT clause applied.
*/
@Override
public String getLimitString(String querySqlString, boolean hasOffset) {
StringBuilder sb = new StringBuilder( querySqlString.trim() );
int orderByIndex = shallowIndexOfWord( sb, ORDER_BY, 0 );
CharSequence orderby = orderByIndex > 0 ? sb.subSequence( orderByIndex, sb.length() )
: "ORDER BY CURRENT_TIMESTAMP";
// Delete the order by clause at the end of the query
if ( orderByIndex > 0 ) {
sb.delete( orderByIndex, orderByIndex + orderby.length() );
}
// HHH-5715 bug fix
replaceDistinctWithGroupBy( sb );
insertRowNumberFunction( sb, orderby );
// Wrap the query within a with statement:
sb.insert( 0, "WITH query AS (" ).append( ") SELECT * FROM query " );
sb.append( "WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?" );
return sb.toString();
}
/**
* Utility method that checks if the given sql query is a select distinct one and if so replaces the distinct select
* with an equivalent simple select with a group by clause.
*
* @param sql an sql query
*/
protected static void replaceDistinctWithGroupBy(StringBuilder sql) {
int distinctIndex = shallowIndexOfWord( sql, DISTINCT, 0 );
int selectEndIndex = shallowIndexOfWord( sql, FROM, 0 );
if (distinctIndex > 0 && distinctIndex < selectEndIndex) {
sql.delete( distinctIndex, distinctIndex + DISTINCT.length() + " ".length());
sql.append( " group by" ).append( getSelectFieldsWithoutAliases( sql ) );
}
}
public static final String SELECT_WITH_SPACE = SELECT + ' ';
/**
* This utility method searches the given sql query for the fields of the select statement and returns them without
* the aliases.
*
* @param sql sql query
*
* @return the fields of the select statement without their alias
*/
protected static CharSequence getSelectFieldsWithoutAliases(StringBuilder sql) {
final int selectStartPos = shallowIndexOf( sql, SELECT_WITH_SPACE, 0 );
final int fromStartPos = shallowIndexOfWord( sql, FROM, selectStartPos );
String select = sql.substring( selectStartPos + SELECT.length(), fromStartPos );
// Strip the as clauses
return stripAliases( select );
}
/**
* Utility method that strips the aliases.
*
* @param str string to replace the as statements
*
* @return a string without the as statements
*/
protected static String stripAliases(String str) {
Matcher matcher = ALIAS_PATTERN.matcher( str );
return matcher.replaceAll( "$1" );
}
/**
* We must place the row_number function at the end of select clause.
*
* @param sql the initial sql query without the order by clause
* @param orderby the order by clause of the query
*/
protected void insertRowNumberFunction(StringBuilder sql, CharSequence orderby) {
// Find the end of the select clause
int selectEndIndex = shallowIndexOfWord( sql, FROM, 0 );
// Insert after the select clause the row_number() function:
sql.insert( selectEndIndex - 1, ", ROW_NUMBER() OVER (" + orderby + ") as __hibernate_row_nr__" );
} }
@Override // since SQLServer2005 the nowait hint is supported @Override // since SQLServer2005 the nowait hint is supported
@ -235,50 +97,6 @@ public class SQLServer2005Dialect extends SQLServerDialect {
} }
} }
/**
* 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 );
return index != -1 ? ( index + 1 ) : -1; // In case of match adding one because of space placed in front of search term.
}
/**
* 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) {
final String lowercase = sb.toString().toLowerCase(); // case-insensitive match
final int len = lowercase.length();
final int searchlen = search.length();
int pos = -1, depth = 0, cur = fromIndex;
do {
pos = lowercase.indexOf( search, cur );
if ( pos != -1 ) {
for ( int iter = cur; iter < pos; iter++ ) {
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;
}
@Override @Override
public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() {
return new SQLExceptionConversionDelegate() { return new SQLExceptionConversionDelegate() {
@ -286,7 +104,7 @@ public class SQLServer2005Dialect extends SQLServerDialect {
public JDBCException convert(SQLException sqlException, String message, String sql) { public JDBCException convert(SQLException sqlException, String message, String sql) {
final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException ); final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException );
final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException );
if( "HY008".equals( sqlState )){ if ( "HY008".equals( sqlState ) ) {
throw new QueryTimeoutException( message, sqlException, sql ); throw new QueryTimeoutException( message, sqlException, sql );
} }
if (1222 == errorCode ) { if (1222 == errorCode ) {
@ -296,7 +114,4 @@ public class SQLServer2005Dialect extends SQLServerDialect {
} }
}; };
} }
} }

View File

@ -0,0 +1,168 @@
package org.hibernate.dialect.pagination;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.hibernate.engine.spi.RowSelection;
/**
* Default implementation of {@link LimitHandler} interface.
*
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public abstract class AbstractLimitHandler implements LimitHandler {
protected final String sql;
protected final RowSelection selection;
/**
* Default constructor. SQL query and selection criteria required to allow LIMIT clause pre-processing.
*
* @param sql SQL query.
* @param selection Selection criteria. {@code null} in case of unlimited number of rows.
*/
public AbstractLimitHandler(String sql, RowSelection selection) {
this.sql = sql;
this.selection = selection;
}
public boolean supportsLimit() {
return false;
}
public boolean supportsLimitOffset() {
return supportsLimit();
}
/**
* Does this handler support bind variables (i.e., prepared statement
* parameters) for its limit/offset?
*
* @return True if bind variables can be used; false otherwise.
*/
public boolean supportsVariableLimit() {
return supportsLimit();
}
/**
* ANSI SQL defines the LIMIT clause to be in the form LIMIT offset, limit.
* Does this dialect require us to bind the parameters in reverse order?
*
* @return true if the correct order is limit, offset
*/
public boolean bindLimitParametersInReverseOrder() {
return false;
}
/**
* Does the <tt>LIMIT</tt> clause come at the start of the
* <tt>SELECT</tt> statement, rather than at the end?
*
* @return true if limit parameters should come before other parameters
*/
public boolean bindLimitParametersFirst() {
return false;
}
/**
* Does the <tt>LIMIT</tt> clause take a "maximum" row number instead
* of a total number of returned rows?
* <p/>
* This is easiest understood via an example. Consider you have a table
* with 20 rows, but you only want to retrieve rows number 11 through 20.
* Generally, a limit with offset would say that the offset = 11 and the
* limit = 10 (we only want 10 rows at a time); this is specifying the
* total number of returned rows. Some dialects require that we instead
* specify offset = 11 and limit = 20, where 20 is the "last" row we want
* relative to offset (i.e. total number of rows = 20 - 11 = 9)
* <p/>
* So essentially, is limit relative from offset? Or is limit absolute?
*
* @return True if limit is relative from offset; false otherwise.
*/
public boolean useMaxForLimit() {
return false;
}
/**
* Generally, if there is no limit applied to a Hibernate query we do not apply any limits
* to the SQL query. This option forces that the limit be written to the SQL query.
*
* @return True to force limit into SQL query even if none specified in Hibernate query; false otherwise.
*/
public boolean forceLimitUsage() {
return false;
}
/**
* Hibernate APIs explicitly state that setFirstResult() should be a zero-based offset. Here we allow the
* Dialect a chance to convert that value based on what the underlying db or driver will expect.
* <p/>
* NOTE: what gets passed into {@link #AbstractLimitHandler(String, RowSelection)} is the zero-based offset.
* Dialects which do not {@link #supportsVariableLimit} should take care to perform any needed first-row-conversion
* calls prior to injecting the limit values into the SQL string.
*
* @param zeroBasedFirstResult The user-supplied, zero-based first row offset.
*
* @return The corresponding db/dialect specific offset.
*
* @see org.hibernate.Query#setFirstResult
* @see org.hibernate.Criteria#setFirstResult
*/
public int convertToFirstRowValue(int zeroBasedFirstResult) {
return zeroBasedFirstResult;
}
public String getProcessedSql() {
throw new UnsupportedOperationException( "Paged queries not supported by " + getClass().getName() );
}
public int bindLimitParametersAtStartOfQuery(PreparedStatement statement, int index)
throws SQLException {
return bindLimitParametersFirst() ? bindLimitParameters( statement, index ) : 0;
}
public int bindLimitParametersAtEndOfQuery(PreparedStatement statement, int index)
throws SQLException {
return !bindLimitParametersFirst() ? bindLimitParameters( statement, index ) : 0;
}
public void setMaxRows(PreparedStatement statement) throws SQLException {
}
/**
* Default implementation of binding parameter values needed by the LIMIT clause.
*
* @param statement Statement to which to bind limit parameter values.
* @param index Index from which to start binding.
* @return The number of parameter values bound.
* @throws SQLException Indicates problems binding parameter values.
*/
protected int bindLimitParameters(PreparedStatement statement, int index)
throws SQLException {
if ( !supportsVariableLimit() || !LimitHelper.hasMaxRows( selection ) ) {
return 0;
}
int firstRow = convertToFirstRowValue( LimitHelper.getFirstRow( selection ) );
int lastRow = getMaxOrLimit();
boolean hasFirstRow = supportsLimitOffset() && ( firstRow > 0 || forceLimitUsage() );
boolean reverse = bindLimitParametersInReverseOrder();
if ( hasFirstRow ) {
statement.setInt( index + ( reverse ? 1 : 0 ), firstRow );
}
statement.setInt( index + ( reverse || !hasFirstRow ? 0 : 1 ), lastRow );
return hasFirstRow ? 2 : 1;
}
/**
* Some dialect-specific LIMIT clauses require the maximum last row number
* (aka, first_row_number + total_row_count), while others require the maximum
* returned row count (the total maximum number of rows to return).
*
* @return The appropriate value to bind into the limit clause.
*/
protected int getMaxOrLimit() {
final int firstRow = convertToFirstRowValue( LimitHelper.getFirstRow( selection ) );
final int lastRow = selection.getMaxRows();
return useMaxForLimit() ? lastRow + firstRow : lastRow;
}
}

View File

@ -0,0 +1,58 @@
package org.hibernate.dialect.pagination;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.RowSelection;
/**
* Limit handler that delegates all operations to the underlying dialect.
*
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class LegacyLimitHandler extends AbstractLimitHandler {
private final Dialect dialect;
public LegacyLimitHandler(Dialect dialect, String sql, RowSelection selection) {
super( sql, selection );
this.dialect = dialect;
}
public boolean supportsLimit() {
return dialect.supportsLimit();
}
public boolean supportsLimitOffset() {
return dialect.supportsLimitOffset();
}
public boolean supportsVariableLimit() {
return dialect.supportsVariableLimit();
}
public boolean bindLimitParametersInReverseOrder() {
return dialect.bindLimitParametersInReverseOrder();
}
public boolean bindLimitParametersFirst() {
return dialect.bindLimitParametersFirst();
}
public boolean useMaxForLimit() {
return dialect.useMaxForLimit();
}
public boolean forceLimitUsage() {
return dialect.forceLimitUsage();
}
public int convertToFirstRowValue(int zeroBasedFirstResult) {
return dialect.convertToFirstRowValue( zeroBasedFirstResult );
}
public String getProcessedSql() {
boolean useLimitOffset = supportsLimit() && supportsLimitOffset()
&& LimitHelper.hasFirstRow( selection ) && LimitHelper.hasMaxRows( selection );
return dialect.getLimitString(
sql, useLimitOffset ? LimitHelper.getFirstRow( selection ) : 0, getMaxOrLimit()
);
}
}

View File

@ -0,0 +1,66 @@
package org.hibernate.dialect.pagination;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.hibernate.engine.spi.RowSelection;
/**
* Contract defining dialect-specific LIMIT clause handling. Typically implementers might consider extending
* {@link AbstractLimitHandler} class.
*
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public interface LimitHandler {
/**
* Does this handler support some form of limiting query results
* via a SQL clause?
*
* @return True if this handler supports some form of LIMIT.
*/
public boolean supportsLimit();
/**
* Does this handler's LIMIT support (if any) additionally
* support specifying an offset?
*
* @return True if the handler supports an offset within the limit support.
*/
public boolean supportsLimitOffset();
/**
* Return processed SQL query.
*
* @return Query statement with LIMIT clause applied.
*/
public String getProcessedSql();
/**
* Bind parameter values needed by the LIMIT clause before original SELECT statement.
*
* @param statement Statement to which to bind limit parameter values.
* @param index Index from which to start binding.
* @return The number of parameter values bound.
* @throws SQLException Indicates problems binding parameter values.
*/
public int bindLimitParametersAtStartOfQuery(PreparedStatement statement, int index) throws SQLException;
/**
* Bind parameter values needed by the LIMIT clause after original SELECT statement.
*
* @param statement Statement to which to bind limit parameter values.
* @param index Index from which to start binding.
* @return The number of parameter values bound.
* @throws SQLException Indicates problems binding parameter values.
*/
public int bindLimitParametersAtEndOfQuery(PreparedStatement statement, int index) throws SQLException;
/**
* Use JDBC API to limit the number of rows returned by the SQL query. Typically handlers that do not
* support LIMIT clause should implement this method.
*
* @param statement Statement which number of returned rows shall be limited.
* @throws SQLException Indicates problems while limiting maximum rows returned.
*/
public void setMaxRows(PreparedStatement statement) throws SQLException;
}

View File

@ -0,0 +1,24 @@
package org.hibernate.dialect.pagination;
import org.hibernate.engine.spi.RowSelection;
/**
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class LimitHelper {
public static boolean useLimit(LimitHandler limitHandler, RowSelection selection) {
return limitHandler.supportsLimit() && hasMaxRows( selection );
}
public static boolean hasFirstRow(RowSelection selection) {
return getFirstRow( selection ) > 0;
}
public static int getFirstRow(RowSelection selection) {
return ( selection == null || selection.getFirstRow() == null ) ? 0 : selection.getFirstRow();
}
public static boolean hasMaxRows(RowSelection selection) {
return selection != null && selection.getMaxRows() != null && selection.getMaxRows() > 0;
}
}

View File

@ -0,0 +1,35 @@
package org.hibernate.dialect.pagination;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.hibernate.engine.spi.RowSelection;
/**
* Handler not supporting query LIMIT clause. JDBC API is used to set maximum number of returned rows.
*
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
*/
public class NoopLimitHandler extends AbstractLimitHandler {
public NoopLimitHandler(String sql, RowSelection selection) {
super( sql, selection );
}
public String getProcessedSql() {
return sql;
}
public int bindLimitParametersAtStartOfQuery(PreparedStatement statement, int index) {
return 0;
}
public int bindLimitParametersAtEndOfQuery(PreparedStatement statement, int index) {
return 0;
}
public void setMaxRows(PreparedStatement statement) throws SQLException {
if ( LimitHelper.hasMaxRows( selection ) ) {
statement.setMaxRows( selection.getMaxRows() + convertToFirstRowValue( LimitHelper.getFirstRow( selection ) ) );
}
}
}

View File

@ -0,0 +1,263 @@
package org.hibernate.dialect.pagination;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;
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)
*/
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(.)+$" );
private boolean topAdded = false; // Flag indicating whether TOP(?) expression has been added to the original query.
private boolean hasOffset = true; // True if offset greater than 0.
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:
*
* <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 {@literal TOP(?)} expression is added to the original query.
*
* @return A new SQL statement with the LIMIT clause applied.
*/
@Override
public String getProcessedSql() {
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 );
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 ) {
statement.setInt( index, getMaxOrLimit() - 1 ); // Binding TOP(?).
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.
*
* @param sb SQL query.
*
* @return List of aliases separated with comas.
*/
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;
while ( nextComa != -1 ) {
prevComa = nextComa;
nextComa = shallowIndexOf( sb, ",", nextComa );
if ( nextComa > endPos ) {
break;
}
if ( nextComa != -1 ) {
String expression = sb.substring( prevComa, nextComa );
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 );
++unique;
nextComa += ( " as " + alias ).length();
}
aliases.add( alias );
++nextComa;
}
}
// Processing last column.
endPos = shallowIndexOfWord( sb, FROM, startPos ); // Refreshing end position, because we might have inserted new alias.
String expression = sb.substring( prevComa, endPos );
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 );
return StringHelper.join( ", ", aliases.iterator() );
}
/**
* 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) {
Matcher matcher = ALIAS_PATTERN.matcher( expression );
if ( matcher.find() ) {
return matcher.group( 0 ).replaceFirst( "(?i)\\sas\\s", "" ).trim();
}
return null;
}
/**
* 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 {@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.
*
* @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 );
return index != -1 ? ( index + 1 ) : -1; // In case of match adding one because of space placed in front of search term.
}
/**
* 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) {
final String lowercase = sb.toString().toLowerCase(); // case-insensitive match
final int len = lowercase.length();
final int searchlen = search.length();
int pos = -1, depth = 0, cur = fromIndex;
do {
pos = lowercase.indexOf( search, cur );
if ( pos != -1 ) {
for ( int iter = cur; iter < pos; iter++ ) {
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;
}
}

View File

@ -953,9 +953,8 @@ public class QueryTranslatorImpl extends BasicLoader implements FilterTranslator
if ( stats ) startTime = System.currentTimeMillis(); if ( stats ) startTime = System.currentTimeMillis();
try { try {
final ResultSet rs = executeQueryStatement( queryParameters, false, session );
PreparedStatement st = prepareQueryStatement( queryParameters, false, session ); final PreparedStatement st = (PreparedStatement) rs.getStatement();
ResultSet rs = getResultSet( st, queryParameters.hasAutoDiscoverScalarTypes(), false, queryParameters.getRowSelection(), session );
HolderInstantiator hi = HolderInstantiator.createClassicHolderInstantiator(holderConstructor, queryParameters.getResultTransformer()); HolderInstantiator hi = HolderInstantiator.createClassicHolderInstantiator(holderConstructor, queryParameters.getResultTransformer());
Iterator result = new IteratorImpl( rs, st, session, queryParameters.isReadOnly( session ), returnTypes, getColumnNames(), hi ); Iterator result = new IteratorImpl( rs, st, session, queryParameters.isReadOnly( session ), returnTypes, getColumnNames(), hi );

View File

@ -53,6 +53,9 @@ import org.hibernate.cache.spi.QueryCache;
import org.hibernate.cache.spi.QueryKey; import org.hibernate.cache.spi.QueryKey;
import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.dialect.Dialect; import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.dialect.pagination.LimitHelper;
import org.hibernate.dialect.pagination.NoopLimitHandler;
import org.hibernate.engine.internal.TwoPhaseLoad; import org.hibernate.engine.internal.TwoPhaseLoad;
import org.hibernate.engine.jdbc.ColumnNameCache; import org.hibernate.engine.jdbc.ColumnNameCache;
import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.EntityKey;
@ -818,15 +821,15 @@ public abstract class Loader {
final ResultTransformer forcedResultTransformer) throws SQLException, HibernateException { final ResultTransformer forcedResultTransformer) throws SQLException, HibernateException {
final RowSelection selection = queryParameters.getRowSelection(); final RowSelection selection = queryParameters.getRowSelection();
final int maxRows = hasMaxRows( selection ) ? final int maxRows = LimitHelper.hasMaxRows( selection ) ?
selection.getMaxRows() : selection.getMaxRows() :
Integer.MAX_VALUE; Integer.MAX_VALUE;
final int entitySpan = getEntityPersisters().length; final int entitySpan = getEntityPersisters().length;
final ArrayList hydratedObjects = entitySpan == 0 ? null : new ArrayList( entitySpan * 10 ); final ArrayList hydratedObjects = entitySpan == 0 ? null : new ArrayList( entitySpan * 10 );
final PreparedStatement st = prepareQueryStatement( queryParameters, false, session ); final ResultSet rs = executeQueryStatement( queryParameters, false, session );
final ResultSet rs = getResultSet( st, queryParameters.hasAutoDiscoverScalarTypes(), queryParameters.isCallable(), selection, session ); final PreparedStatement st = (PreparedStatement) rs.getStatement();
// would be great to move all this below here into another method that could also be used // would be great to move all this below here into another method that could also be used
// from the new scrolling stuff. // from the new scrolling stuff.
@ -1633,7 +1636,7 @@ public abstract class Loader {
private void advance(final ResultSet rs, final RowSelection selection) private void advance(final ResultSet rs, final RowSelection selection)
throws SQLException { throws SQLException {
final int firstRow = getFirstRow( selection ); final int firstRow = LimitHelper.getFirstRow( selection );
if ( firstRow != 0 ) { if ( firstRow != 0 ) {
if ( getFactory().getSettings().isScrollableResultSetsEnabled() ) { if ( getFactory().getSettings().isScrollableResultSetsEnabled() ) {
// we can go straight to the first required row // we can go straight to the first required row
@ -1646,24 +1649,17 @@ public abstract class Loader {
} }
} }
private static boolean hasMaxRows(RowSelection selection) {
return selection != null && selection.getMaxRows() != null && selection.getMaxRows() > 0;
}
private static int getFirstRow(RowSelection selection) {
return ( selection == null || selection.getFirstRow() == null ) ? 0 : selection.getFirstRow();
}
private int interpretFirstRow(int zeroBasedFirstResult) {
return getFactory().getDialect().convertToFirstRowValue( zeroBasedFirstResult );
}
/** /**
* Should we pre-process the SQL string, adding a dialect-specific * Build LIMIT clause handler applicable for given selection criteria. Returns {@link NoopLimitHandler} delegate
* LIMIT clause. * if dialect does not support LIMIT expression or processed query does not use pagination.
*
* @param sql Query string.
* @param selection Selection criteria.
* @return LIMIT clause delegate.
*/ */
private static boolean useLimit(final RowSelection selection, final Dialect dialect) { protected LimitHandler getLimitHandler(String sql, RowSelection selection) {
return dialect.supportsLimit() && hasMaxRows( selection ); final LimitHandler limitHandler = getFactory().getDialect().buildLimitHandler( sql, selection );
return LimitHelper.useLimit( limitHandler, selection ) ? limitHandler : new NoopLimitHandler( sql, selection );
} }
private ScrollMode getScrollMode(boolean scroll, boolean hasFirstRow, boolean useLimitOffSet, QueryParameters queryParameters) { private ScrollMode getScrollMode(boolean scroll, boolean hasFirstRow, boolean useLimitOffSet, QueryParameters queryParameters) {
@ -1678,36 +1674,48 @@ public abstract class Loader {
} }
return null; return null;
} }
/**
* Process query string by applying filters, LIMIT clause, locks and comments if necessary.
* Finally execute SQL statement and advance to the first row.
*/
protected ResultSet executeQueryStatement(
final QueryParameters queryParameters,
final boolean scroll,
final SessionImplementor session) throws SQLException {
// Processing query filters.
queryParameters.processFilters( getSQLString(), session );
// Applying LIMIT clause.
final LimitHandler limitHandler = getLimitHandler( queryParameters.getFilteredSQL(), queryParameters.getRowSelection() );
String sql = limitHandler.getProcessedSql();
// Adding locks and comments.
sql = preprocessSQL( sql, queryParameters, getFactory().getDialect() );
final PreparedStatement st = prepareQueryStatement( sql, queryParameters, limitHandler, scroll, session );
return getResultSet( st, queryParameters.getRowSelection(), limitHandler, queryParameters.hasAutoDiscoverScalarTypes(), session );
}
/** /**
* Obtain a <tt>PreparedStatement</tt> with all parameters pre-bound. * Obtain a <tt>PreparedStatement</tt> with all parameters pre-bound.
* Bind JDBC-style <tt>?</tt> parameters, named parameters, and * Bind JDBC-style <tt>?</tt> parameters, named parameters, and
* limit parameters. * limit parameters.
*/ */
protected final PreparedStatement prepareQueryStatement( protected final PreparedStatement prepareQueryStatement(
final String sql,
final QueryParameters queryParameters, final QueryParameters queryParameters,
final LimitHandler limitHandler,
final boolean scroll, final boolean scroll,
final SessionImplementor session) throws SQLException, HibernateException { final SessionImplementor session) throws SQLException, HibernateException {
queryParameters.processFilters( getSQLString(), session );
String sql = queryParameters.getFilteredSQL();
final Dialect dialect = getFactory().getDialect(); final Dialect dialect = getFactory().getDialect();
final RowSelection selection = queryParameters.getRowSelection(); final RowSelection selection = queryParameters.getRowSelection();
boolean useLimit = useLimit( selection, dialect ); boolean useLimit = LimitHelper.useLimit( limitHandler, selection );
boolean hasFirstRow = getFirstRow( selection ) > 0; boolean hasFirstRow = LimitHelper.hasFirstRow( selection );
boolean useLimitOffset = hasFirstRow && useLimit && dialect.supportsLimitOffset(); boolean useLimitOffset = hasFirstRow && useLimit && limitHandler.supportsLimitOffset();
boolean callable = queryParameters.isCallable(); boolean callable = queryParameters.isCallable();
final ScrollMode scrollMode = getScrollMode( scroll, hasFirstRow, useLimitOffset, queryParameters ); final ScrollMode scrollMode = getScrollMode( scroll, hasFirstRow, useLimitOffset, queryParameters );
if ( useLimit ) {
sql = dialect.getLimitString(
sql.trim(), //use of trim() here is ugly?
useLimitOffset ? getFirstRow(selection) : 0,
getMaxOrLimit(selection, dialect)
);
}
sql = preprocessSQL( sql, queryParameters, dialect );
PreparedStatement st = session.getTransactionCoordinator().getJdbcCoordinator().getStatementPreparer().prepareQueryStatement( PreparedStatement st = session.getTransactionCoordinator().getJdbcCoordinator().getStatementPreparer().prepareQueryStatement(
sql, sql,
callable, callable,
@ -1718,22 +1726,17 @@ public abstract class Loader {
int col = 1; int col = 1;
//TODO: can we limit stored procedures ?! //TODO: can we limit stored procedures ?!
if ( useLimit && dialect.bindLimitParametersFirst() ) { col += limitHandler.bindLimitParametersAtStartOfQuery( st, col );
col += bindLimitParameters( st, col, selection );
}
if (callable) { if (callable) {
col = dialect.registerResultSetOutParameter( (CallableStatement)st, col ); col = dialect.registerResultSetOutParameter( (CallableStatement)st, col );
} }
col += bindParameterValues( st, queryParameters, col, session ); col += bindParameterValues( st, queryParameters, col, session );
if ( useLimit && !dialect.bindLimitParametersFirst() ) { col += limitHandler.bindLimitParametersAtEndOfQuery( st, col );
col += bindLimitParameters( st, col, selection );
}
if ( !useLimit ) { limitHandler.setMaxRows( st );
setMaxRows( st, selection );
}
if ( selection != null ) { if ( selection != null ) {
if ( selection.getTimeout() != null ) { if ( selection.getTimeout() != null ) {
@ -1776,63 +1779,6 @@ public abstract class Loader {
return st; return st;
} }
/**
* Some dialect-specific LIMIT clauses require the maximum last row number
* (aka, first_row_number + total_row_count), while others require the maximum
* returned row count (the total maximum number of rows to return).
*
* @param selection The selection criteria
* @param dialect The dialect
* @return The appropriate value to bind into the limit clause.
*/
private static int getMaxOrLimit(final RowSelection selection, final Dialect dialect) {
final int firstRow = dialect.convertToFirstRowValue( getFirstRow( selection ) );
final int lastRow = selection.getMaxRows();
return dialect.useMaxForLimit() ? lastRow + firstRow : lastRow;
}
/**
* Bind parameter values needed by the dialect-specific LIMIT clause.
*
* @param statement The statement to which to bind limit param values.
* @param index The bind position from which to start binding
* @param selection The selection object containing the limit information.
* @return The number of parameter values bound.
* @throws java.sql.SQLException Indicates problems binding parameter values.
*/
private int bindLimitParameters(
final PreparedStatement statement,
final int index,
final RowSelection selection) throws SQLException {
Dialect dialect = getFactory().getDialect();
if ( !dialect.supportsVariableLimit() ) {
return 0;
}
if ( !hasMaxRows( selection ) ) {
throw new AssertionFailure( "no max results set" );
}
int firstRow = interpretFirstRow( getFirstRow( selection ) );
int lastRow = getMaxOrLimit( selection, dialect );
boolean hasFirstRow = dialect.supportsLimitOffset() && ( firstRow > 0 || dialect.forceLimitUsage() );
boolean reverse = dialect.bindLimitParametersInReverseOrder();
if ( hasFirstRow ) {
statement.setInt( index + ( reverse ? 1 : 0 ), firstRow );
}
statement.setInt( index + ( reverse || !hasFirstRow ? 0 : 1 ), lastRow );
return hasFirstRow ? 2 : 1;
}
/**
* Use JDBC API to limit the number of rows returned by the SQL query if necessary
*/
private void setMaxRows(
final PreparedStatement st,
final RowSelection selection) throws SQLException {
if ( hasMaxRows( selection ) ) {
st.setMaxRows( selection.getMaxRows() + interpretFirstRow( getFirstRow( selection ) ) );
}
}
/** /**
* Bind all parameter values into the prepared statement in preparation * Bind all parameter values into the prepared statement in preparation
* for execution. * for execution.
@ -1936,24 +1882,21 @@ public abstract class Loader {
} }
/** /**
* Fetch a <tt>PreparedStatement</tt>, call <tt>setMaxRows</tt> and then execute it, * Execute given <tt>PreparedStatement</tt>, advance to the first result and return SQL <tt>ResultSet</tt>.
* advance to the first result and return an SQL <tt>ResultSet</tt>
*/ */
protected final ResultSet getResultSet( protected final ResultSet getResultSet(
final PreparedStatement st, final PreparedStatement st,
final boolean autodiscovertypes,
final boolean callable,
final RowSelection selection, final RowSelection selection,
final LimitHandler limitHandler,
final boolean autodiscovertypes,
final SessionImplementor session) final SessionImplementor session)
throws SQLException, HibernateException { throws SQLException, HibernateException {
ResultSet rs = null;
try { try {
Dialect dialect = getFactory().getDialect(); ResultSet rs = st.executeQuery();
rs = st.executeQuery();
rs = wrapResultSetIfEnabled( rs , session ); rs = wrapResultSetIfEnabled( rs , session );
if ( !dialect.supportsLimitOffset() || !useLimit( selection, dialect ) ) { if ( !limitHandler.supportsLimitOffset() || !LimitHelper.useLimit( limitHandler, selection ) ) {
advance( rs, selection ); advance( rs, selection );
} }
@ -2505,9 +2448,8 @@ public abstract class Loader {
if ( stats ) startTime = System.currentTimeMillis(); if ( stats ) startTime = System.currentTimeMillis();
try { try {
final ResultSet rs = executeQueryStatement( queryParameters, true, session );
PreparedStatement st = prepareQueryStatement( queryParameters, true, session ); final PreparedStatement st = (PreparedStatement) rs.getStatement();
ResultSet rs = getResultSet(st, queryParameters.hasAutoDiscoverScalarTypes(), queryParameters.isCallable(), queryParameters.getRowSelection(), session);
if ( stats ) { if ( stats ) {
getFactory().getStatisticsImplementor().queryExecuted( getFactory().getStatisticsImplementor().queryExecuted(

View File

@ -487,11 +487,11 @@ public class QueryLoader extends BasicLoader {
} }
try { try {
final PreparedStatement st = prepareQueryStatement( queryParameters, false, session );
if ( queryParameters.isCallable() ) { if ( queryParameters.isCallable() ) {
throw new QueryException("iterate() not supported for callable statements"); throw new QueryException("iterate() not supported for callable statements");
} }
final ResultSet rs = getResultSet(st, queryParameters.hasAutoDiscoverScalarTypes(), false, queryParameters.getRowSelection(), session); final ResultSet rs = executeQueryStatement( queryParameters, false, session );
final PreparedStatement st = (PreparedStatement) rs.getStatement();
final Iterator result = new IteratorImpl( final Iterator result = new IteratorImpl(
rs, rs,
st, st,

View File

@ -27,6 +27,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.hibernate.engine.spi.RowSelection;
import org.hibernate.testing.TestForIssue; import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseUnitTestCase; import org.hibernate.testing.junit4.BaseUnitTestCase;
@ -51,34 +52,16 @@ public class SQLServer2005DialectTestCase extends BaseUnitTestCase {
dialect = null; dialect = null;
} }
@Test
public void testStripAliases() {
String input = "some_field1 as f1, some_fild2 as f2, _field3 as f3 ";
assertEquals( "some_field1, some_fild2, _field3", SQLServer2005Dialect.stripAliases(input) );
}
@Test
public void testGetSelectFieldsWithoutAliases() {
StringBuilder input = new StringBuilder( "select some_field1 as f12, some_fild2 as f879, _field3 as _f24674_3 from ...." );
String output = SQLServer2005Dialect.getSelectFieldsWithoutAliases( input ).toString();
assertEquals( " some_field1, some_fild2, _field3", output );
}
@Test
public void testReplaceDistinctWithGroupBy() {
StringBuilder input = new StringBuilder( "select distinct f1, f2 as ff, f3 from table where f1 = 5" );
SQLServer2005Dialect.replaceDistinctWithGroupBy( input );
assertEquals( "select f1, f2 as ff, f3 from table where f1 = 5 group by f1, f2, f3 ", input.toString() );
}
@Test @Test
public void testGetLimitString() { public void testGetLimitString() {
String input = "select distinct f1 as f53245 from table849752 order by f234, f67 desc"; String input = "select distinct f1 as f53245 from table849752 order by f234, f67 desc";
assertEquals( "with query as (select f1 as f53245, row_number() over (order by f234, f67 desc) as __hibernate_row_nr__ from table849752 group by f1) select * from query where __hibernate_row_nr__ >= ? and __hibernate_row_nr__ < ?", dialect.getLimitString(input, 10, 15).toLowerCase() ); assertEquals(
"with query as (select inner_query.*, row_number() over (order by current_timestamp) as __hibernate_row_nr__ from ( " +
"select distinct top(?) f1 as f53245 from table849752 order by f234, f67 desc ) inner_query )" +
" select f53245 from query where __hibernate_row_nr__ >= ? and __hibernate_row_nr__ < ?",
dialect.buildLimitHandler( input, toRowSelection( 10, 15 ) ).getProcessedSql().toLowerCase()
);
} }
@Test @Test
@ -91,14 +74,10 @@ public class SQLServer2005DialectTestCase extends BaseUnitTestCase {
"where persistent0_.customerid=?"; "where persistent0_.customerid=?";
assertEquals( assertEquals(
"WITH query AS (select persistent0_.rid as rid1688_, " + "WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " +
"persistent0_.deviationfromtarget as deviati16_1688_, " + fromColumnNameSQL + " ) inner_query ) " +
"persistent0_.sortindex as sortindex1688_, " + "SELECT rid1688_, deviati16_1688_, sortindex1688_ FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
"ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ " + dialect.buildLimitHandler( fromColumnNameSQL, toRowSelection( 1, 10 ) ).getProcessedSql()
"from m_evalstate persistent0_ " +
"where persistent0_.customerid=?) " +
"SELECT * FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
dialect.getLimitString( fromColumnNameSQL, 1, 10 )
); );
} }
@ -113,35 +92,29 @@ public class SQLServer2005DialectTestCase extends BaseUnitTestCase {
"where persistent0_.type='v'"; "where persistent0_.type='v'";
assertEquals( assertEquals(
"WITH query AS (select persistent0_.id as col_0_0_, " + "WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " +
"(select max(persistent1_.acceptancedate) " + subselectInSelectClauseSQL + " ) inner_query ) " +
"from av_advisoryvariant persistent1_ " + "SELECT col_0_0_, col_1_0_ FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
"where persistent1_.clientid=persistent0_.id) as col_1_0_, " + dialect.buildLimitHandler( subselectInSelectClauseSQL, toRowSelection( 2, 5 ) ).getProcessedSql()
"ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ " +
"from c_customer persistent0_ " +
"where persistent0_.type='v') " +
"SELECT * FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
dialect.getLimitString( subselectInSelectClauseSQL, 2, 5 )
); );
} }
@Test @Test
@TestForIssue(jiraKey = "HHH-6728") @TestForIssue(jiraKey = "HHH-6728")
public void testGetLimitStringCaseSensitive() { public void testGetLimitStringCaseSensitive() {
final String caseSensitiveSQL = "select persistent0_.id as col_0_0_, " + final String caseSensitiveSQL = "select persistent0_.id, persistent0_.uid AS tmp1, " +
"(select case when persistent0_.name = 'Smith' then 'Neo' else persistent0_.id end) as col_1_0_ " + "(select case when persistent0_.name = 'Smith' then 'Neo' else persistent0_.id end) " +
"from C_Customer persistent0_ " + "from C_Customer persistent0_ " +
"where persistent0_.type='Va' " + "where persistent0_.type='Va' " +
"order by persistent0_.Order"; "order by persistent0_.Order";
assertEquals( assertEquals(
"WITH query AS (select persistent0_.id as col_0_0_, " + "WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " +
"(select case when persistent0_.name = 'Smith' then 'Neo' else persistent0_.id end) as col_1_0_, " + "select TOP(?) persistent0_.id as page0_, persistent0_.uid AS tmp1, " +
"ROW_NUMBER() OVER (order by persistent0_.Order) as __hibernate_row_nr__ " + "(select case when persistent0_.name = 'Smith' then 'Neo' else persistent0_.id end) as page1_ " +
"from C_Customer persistent0_ " + "from C_Customer persistent0_ where persistent0_.type='Va' order by persistent0_.Order ) " +
"where persistent0_.type='Va' ) " + "inner_query ) SELECT page0_, tmp1, page1_ FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
"SELECT * FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?", dialect.buildLimitHandler( caseSensitiveSQL, toRowSelection( 1, 2 ) ).getProcessedSql()
dialect.getLimitString( caseSensitiveSQL, 1, 2 )
); );
} }
@ -151,10 +124,39 @@ public class SQLServer2005DialectTestCase extends BaseUnitTestCase {
final String distinctInAggregateSQL = "select aggregate_function(distinct p.n) as f1 from table849752 p order by f1"; final String distinctInAggregateSQL = "select aggregate_function(distinct p.n) as f1 from table849752 p order by f1";
assertEquals( assertEquals(
"WITH query AS (select aggregate_function(distinct p.n) as f1, " + "WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " +
"ROW_NUMBER() OVER (order by f1) as __hibernate_row_nr__ from table849752 p ) " + "select TOP(?) aggregate_function(distinct p.n) as f1 from table849752 p order by f1 ) inner_query ) " +
"SELECT * FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?", "SELECT f1 FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
dialect.getLimitString( distinctInAggregateSQL, 2, 5 ) dialect.buildLimitHandler( distinctInAggregateSQL, toRowSelection( 2, 5 ) ).getProcessedSql()
); );
} }
@Test
@TestForIssue(jiraKey = "HHH-7370")
public void testGetLimitStringWithMaxOnly() {
final String query = "select product2x0_.id as id0_, product2x0_.description as descript2_0_ " +
"from Product2 product2x0_ order by product2x0_.id";
assertEquals(
"select TOP(?) product2x0_.id as id0_, product2x0_.description as descript2_0_ " +
"from Product2 product2x0_ order by product2x0_.id",
dialect.buildLimitHandler( query, toRowSelection( 0, 1 ) ).getProcessedSql()
);
final String distinctQuery = "select distinct product2x0_.id as id0_, product2x0_.description as descript2_0_ " +
"from Product2 product2x0_ order by product2x0_.id";
assertEquals(
"select distinct TOP(?) product2x0_.id as id0_, product2x0_.description as descript2_0_ " +
"from Product2 product2x0_ order by product2x0_.id",
dialect.buildLimitHandler( distinctQuery, toRowSelection( 0, 5 ) ).getProcessedSql()
);
}
private RowSelection toRowSelection(int firstRow, int maxRows) {
RowSelection selection = new RowSelection();
selection.setFirstRow( firstRow );
selection.setMaxRows( maxRows );
return selection;
}
} }

View File

@ -45,5 +45,36 @@ public class Product2 implements Serializable {
@Column(name = "description", nullable = false) @Column(name = "description", nullable = false)
public String description; public String description;
public Product2() {
}
public Product2(Integer id, String description) {
this.id = id;
this.description = description;
}
@Override
public boolean equals(Object o) {
if ( this == o ) return true;
if ( !(o instanceof Product2) ) return false;
Product2 product2 = (Product2) o;
if ( description != null ? !description.equals( product2.description ) : product2.description != null ) return false;
if ( id != null ? !id.equals( product2.id ) : product2.id != null ) return false;
return true;
}
@Override
public int hashCode() {
int result = id != null ? id.hashCode() : 0;
result = 31 * result + ( description != null ? description.hashCode() : 0 );
return result;
}
@Override
public String toString() {
return "Product2(id = " + id + ", description = " + description + ")";
}
} }

View File

@ -27,14 +27,15 @@
package org.hibernate.test.dialect.functional; package org.hibernate.test.dialect.functional;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import java.sql.Connection; import java.sql.Connection;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays;
import java.util.List; import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.LockOptions; import org.hibernate.LockOptions;
import org.hibernate.Session; import org.hibernate.Session;
@ -42,7 +43,6 @@ import org.hibernate.Transaction;
import org.hibernate.dialect.SQLServer2005Dialect; import org.hibernate.dialect.SQLServer2005Dialect;
import org.hibernate.exception.LockTimeoutException; import org.hibernate.exception.LockTimeoutException;
import org.hibernate.exception.SQLGrammarException; import org.hibernate.exception.SQLGrammarException;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jdbc.ReturningWork; import org.hibernate.jdbc.ReturningWork;
import org.hibernate.testing.RequiresDialect; import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.TestForIssue; import org.hibernate.testing.TestForIssue;
@ -57,9 +57,8 @@ import org.junit.Test;
*/ */
@RequiresDialect(value = { SQLServer2005Dialect.class }) @RequiresDialect(value = { SQLServer2005Dialect.class })
public class SQLServerDialectTest extends BaseCoreFunctionalTestCase { public class SQLServerDialectTest extends BaseCoreFunctionalTestCase {
@TestForIssue(jiraKey = "HHH-7198")
@Test @Test
@TestForIssue(jiraKey = "HHH-7198")
public void testMaxResultsSqlServerWithCaseSensitiveCollation() throws Exception { public void testMaxResultsSqlServerWithCaseSensitiveCollation() throws Exception {
Session s = openSession(); Session s = openSession();
@ -94,10 +93,7 @@ public class SQLServerDialectTest extends BaseCoreFunctionalTestCase {
Transaction tx = s.beginTransaction(); Transaction tx = s.beginTransaction();
for ( int i = 1; i <= 20; i++ ) { for ( int i = 1; i <= 20; i++ ) {
Product2 kit = new Product2(); s.persist( new Product2( i, "Kit" + i ) );
kit.id = i;
kit.description = "Kit" + i;
s.persist( kit );
} }
s.flush(); s.flush();
s.clear(); s.clear();
@ -118,9 +114,109 @@ public class SQLServerDialectTest extends BaseCoreFunctionalTestCase {
s.close(); s.close();
} }
@TestForIssue(jiraKey = "HHH-3961")
@Test @Test
@TestForIssue(jiraKey = "HHH-7369")
public void testPaginationWithScalarQuery() throws Exception {
Session s = openSession();
Transaction tx = s.beginTransaction();
for ( int i = 0; i < 10; i++ ) {
s.persist( new Product2( i, "Kit" + i ) );
}
s.flush();
s.clear();
List list = s.createSQLQuery( "select id from Product2 where description like 'Kit%' order by id" ).list();
assertEquals(Integer.class, list.get(0).getClass()); // scalar result is an Integer
list = s.createSQLQuery( "select id from Product2 where description like 'Kit%' order by id" ).setFirstResult( 2 ).setMaxResults( 2 ).list();
assertEquals(Integer.class, list.get(0).getClass()); // this fails without patch, as result suddenly has become an array
// same once again with alias
list = s.createSQLQuery( "select id as myint from Product2 where description like 'Kit%' order by id asc" ).setFirstResult( 2 ).setMaxResults( 2 ).list();
assertEquals(Integer.class, list.get(0).getClass());
tx.rollback();
s.close();
}
@Test
@TestForIssue(jiraKey = "HHH-7368")
public void testPaginationWithTrailingSemicolon() throws Exception {
Session s = openSession();
s.createSQLQuery( "select id from Product2 where description like 'Kit%' order by id;" )
.setFirstResult( 2 ).setMaxResults( 2 ).list();
s.close();
}
@Test
public void testPaginationWithHQLProjection() {
Session session = openSession();
Transaction tx = session.beginTransaction();
for ( int i = 10; i < 20; i++ ) {
session.persist( new Product2( i, "Kit" + i ) );
}
session.flush();
session.clear();
List list = session.createQuery(
"select id, description as descr, (select max(id) from Product2) as maximum from Product2"
).setFirstResult( 2 ).setMaxResults( 2 ).list();
assertEquals( 19, ( (Object[]) list.get( 1 ) )[2] );
list = session.createQuery( "select id, description, (select max(id) from Product2) from Product2 order by id" )
.setFirstResult( 2 ).setMaxResults( 2 ).list();
assertEquals( 2, list.size() );
assertArrayEquals( new Object[] {12, "Kit12", 19}, (Object[]) list.get( 0 ));
assertArrayEquals( new Object[] {13, "Kit13", 19}, (Object[]) list.get( 1 ));
tx.rollback();
session.close();
}
@Test
public void testPaginationWithHQL() {
Session session = openSession();
Transaction tx = session.beginTransaction();
for ( int i = 20; i < 30; i++ ) {
session.persist( new Product2( i, "Kit" + i ) );
}
session.flush();
session.clear();
List list = session.createQuery( "from Product2 order by id" ).setFirstResult( 3 ).setMaxResults( 2 ).list();
assertEquals( Arrays.asList( new Product2( 23, "Kit23" ), new Product2( 24, "Kit24" ) ), list );
tx.rollback();
session.close();
}
@Test
@TestForIssue(jiraKey = "HHH-7370")
public void testPaginationWithMaxOnly() {
Session session = openSession();
Transaction tx = session.beginTransaction();
for ( int i = 30; i < 40; i++ ) {
session.persist( new Product2( i, "Kit" + i ) );
}
session.flush();
session.clear();
List list = session.createQuery( "from Product2 order by id" ).setFirstResult( 0 ).setMaxResults( 2 ).list();
assertEquals( Arrays.asList( new Product2( 30, "Kit30" ), new Product2( 31, "Kit31" ) ), list );
list = session.createQuery( "select distinct p from Product2 p order by p.id" ).setMaxResults( 1 ).list();
assertEquals( Arrays.asList( new Product2( 30, "Kit30" ) ), list );
tx.rollback();
session.close();
}
@Test
@TestForIssue(jiraKey = "HHH-3961")
public void testLockNowaitSqlServer() throws Exception { public void testLockNowaitSqlServer() throws Exception {
Session s = openSession(); Session s = openSession();
s.beginTransaction(); s.beginTransaction();
@ -189,5 +285,4 @@ public class SQLServerDialectTest extends BaseCoreFunctionalTestCase {
Product2.class Product2.class
}; };
} }
} }