HHH-8507 - Fix SQL Server 2005 limit handler to be tolerant of carriage returns.

This commit is contained in:
Chris Cranford 2016-05-11 12:16:25 -05:00
parent ef6c9266e6
commit 2b6ab8e8bb
2 changed files with 82 additions and 53 deletions

View File

@ -10,7 +10,6 @@ import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -21,14 +20,20 @@ import org.hibernate.internal.util.StringHelper;
* LIMIT clause handler compatible with SQL Server 2005 and later. * LIMIT clause handler compatible with SQL Server 2005 and later.
* *
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
* @author Chris Cranford
*/ */
public class SQLServer2005LimitHandler extends AbstractLimitHandler { public class SQLServer2005LimitHandler extends AbstractLimitHandler {
private static final String SELECT = "select"; private static final String SELECT = "select";
private static final String SELECT_WITH_SPACE = SELECT + ' ';
private static final String FROM = "from"; private static final String FROM = "from";
private static final String DISTINCT = "distinct"; private static final String DISTINCT = "distinct";
private static final String ORDER_BY = "order by"; private static final String ORDER_BY = "order by";
private static final Pattern SELECT_PATTERN = buildShallowIndexPattern( SELECT + "(.)*" );
private static final Pattern FROM_PATTERN = buildShallowIndexPattern( FROM );
private static final Pattern DISTINCT_PATTERN = buildShallowIndexPattern( DISTINCT );
private static final Pattern ORDER_BY_PATTERN = buildShallowIndexPattern( ORDER_BY );
private static final Pattern COMMA_PATTERN = buildShallowIndexPattern( "," );
private static final Pattern ALIAS_PATTERN = Pattern.compile( "(?i)\\sas\\s(.)+$" ); private static final Pattern ALIAS_PATTERN = Pattern.compile( "(?i)\\sas\\s(.)+$" );
// Flag indicating whether TOP(?) expression has been added to the original query. // Flag indicating whether TOP(?) expression has been added to the original query.
@ -95,7 +100,7 @@ public class SQLServer2005LimitHandler extends AbstractLimitHandler {
if ( LimitHelper.hasFirstRow( selection ) ) { if ( LimitHelper.hasFirstRow( selection ) ) {
final String selectClause = fillAliasInSelectClause( sb ); final String selectClause = fillAliasInSelectClause( sb );
final int orderByIndex = shallowIndexOfWord( sb, ORDER_BY, 0 ); final int orderByIndex = shallowIndexOfPattern( sb, ORDER_BY_PATTERN, 0 );
if ( orderByIndex > 0 ) { if ( orderByIndex > 0 ) {
// ORDER BY requires using TOP. // ORDER BY requires using TOP.
addTopExpression( sb ); addTopExpression( sb );
@ -140,8 +145,9 @@ public class SQLServer2005LimitHandler extends AbstractLimitHandler {
*/ */
protected String fillAliasInSelectClause(StringBuilder sb) { protected String fillAliasInSelectClause(StringBuilder sb) {
final List<String> aliases = new LinkedList<String>(); final List<String> aliases = new LinkedList<String>();
final int startPos = shallowIndexOf( sb, SELECT_WITH_SPACE, 0 ); final int startPos = shallowIndexOfPattern( sb, SELECT_PATTERN, 0 );
int endPos = shallowIndexOfWord( sb, FROM, startPos ); int endPos = shallowIndexOfPattern( sb, FROM_PATTERN, startPos );
int nextComa = startPos; int nextComa = startPos;
int prevComa = startPos; int prevComa = startPos;
int unique = 0; int unique = 0;
@ -149,7 +155,7 @@ public class SQLServer2005LimitHandler extends AbstractLimitHandler {
while ( nextComa != -1 ) { while ( nextComa != -1 ) {
prevComa = nextComa; prevComa = nextComa;
nextComa = shallowIndexOf( sb, ",", nextComa ); nextComa = shallowIndexOfPattern( sb, COMMA_PATTERN, nextComa );
if ( nextComa > endPos ) { if ( nextComa > endPos ) {
break; break;
} }
@ -176,7 +182,7 @@ public class SQLServer2005LimitHandler extends AbstractLimitHandler {
} }
// Processing last column. // Processing last column.
// Refreshing end position, because we might have inserted new alias. // Refreshing end position, because we might have inserted new alias.
endPos = shallowIndexOfWord( sb, FROM, startPos ); endPos = shallowIndexOfPattern( sb, FROM_PATTERN, startPos );
final String expression = sb.substring( prevComa, endPos ); final String expression = sb.substring( prevComa, endPos );
if ( selectsMultipleColumns( expression ) ) { if ( selectsMultipleColumns( expression ) ) {
selectsMultipleColumns = true; selectsMultipleColumns = true;
@ -240,13 +246,13 @@ public class SQLServer2005LimitHandler extends AbstractLimitHandler {
* @param sql SQL query. * @param sql SQL query.
*/ */
protected void addTopExpression(StringBuilder sql) { protected void addTopExpression(StringBuilder sql) {
final int distinctStartPos = shallowIndexOfWord( sql, DISTINCT, 0 ); final int distinctStartPos = shallowIndexOfPattern( sql, DISTINCT_PATTERN, 0 );
if ( distinctStartPos > 0 ) { if ( distinctStartPos > 0 ) {
// Place TOP after DISTINCT. // Place TOP after DISTINCT.
sql.insert( distinctStartPos + DISTINCT.length(), " TOP(?)" ); sql.insert( distinctStartPos + DISTINCT.length(), " TOP(?)" );
} }
else { else {
final int selectStartPos = shallowIndexOf( sql, SELECT_WITH_SPACE, 0 ); final int selectStartPos = shallowIndexOfPattern( sql, SELECT_PATTERN, 0 );
// Place TOP after SELECT. // Place TOP after SELECT.
sql.insert( selectStartPos + SELECT.length(), " TOP(?)" ); sql.insert( selectStartPos + SELECT.length(), " TOP(?)" );
} }
@ -254,53 +260,41 @@ public class SQLServer2005LimitHandler extends AbstractLimitHandler {
} }
/** /**
* Returns index of the first case-insensitive match of search term surrounded by spaces * Returns index of the first case-insensitive match of search pattern that is not
* that is not enclosed in parentheses. * enclosed in parenthesis.
* *
* @param sb String to search. * @param sb String to search.
* @param search Search term. * @param pattern Compiled search pattern.
* @param fromIndex The index from which to start the search. * @param fromIndex The index from which to start the search.
* *
* @return Position of the first match, or {@literal -1} if not found. * @return Position of the first match, or {@literal -1} if not found.
*/ */
private static int shallowIndexOfWord(final StringBuilder sb, final String search, int fromIndex) { private static int shallowIndexOfPattern(final StringBuilder sb, final Pattern pattern, int fromIndex) {
final int index = shallowIndexOf( sb, ' ' + search + ' ', fromIndex ); int index = -1;
// In case of match adding one because of space placed in front of search term. final String matchString = sb.toString();
return index != -1 ? ( index + 1 ) : -1;
// quick exit, save performance and avoid exceptions
if ( matchString.length() < fromIndex || fromIndex < 0 ) {
return -1;
}
Matcher matcher = pattern.matcher( matchString );
matcher.region( fromIndex, matchString.length() );
if ( matcher.find() && matcher.groupCount() > 0 ) {
index = matcher.start();
}
return index;
} }
/** /**
* Returns index of the first case-insensitive match of search term that is not enclosed in parentheses. * Builds a pattern that can be used to find matches of case-insensitive matches
* based on the search pattern that is not enclosed in parenthesis.
* *
* @param sb String to search. * @param pattern String search pattern.
* @param search Search term. * @return Compiled {@link Pattern}.
* @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) { private static Pattern buildShallowIndexPattern(String pattern) {
// case-insensitive match return Pattern.compile( "(\\b" + pattern + ")(?![^\\(]*\\))", Pattern.CASE_INSENSITIVE );
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;
} }
} }

View File

@ -43,10 +43,45 @@ public class SQLServer2005DialectTestCase extends BaseUnitTestCase {
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( assertEquals(
"with query as (select inner_query.*, row_number() over (order by current_timestamp) as __hibernate_row_nr__ from ( " + "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 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__ < ?", " select f53245 from query where __hibernate_row_nr__ >= ? and __hibernate_row_nr__ < ?",
dialect.getLimitHandler().processSql( input, toRowSelection( 10, 15 ) ).toLowerCase(Locale.ROOT) ); dialect.getLimitHandler().processSql( input, toRowSelection( 10, 15 ) ).toLowerCase(Locale.ROOT) );
}
@Test
@TestForIssue(jiraKey = "HHH-10736")
public void testGetLimitStringWithNewlineAfterSelect() {
final String query = "select" + System.lineSeparator() + "* FROM Employee E WHERE E.firstName = :firstName";
assertEquals(
"WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " +
query + " ) inner_query ) SELECT * FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
dialect.getLimitHandler().processSql( query, toRowSelection( 1, 25 ) )
);
}
@Test
@TestForIssue(jiraKey = "HHH-10736")
public void testGetLimitStringWithNewlineAfterSelectWithMultipleSpaces() {
final String query = "select " + System.lineSeparator() + "* FROM Employee E WHERE E.firstName = :firstName";
assertEquals(
"WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " +
query + " ) inner_query ) SELECT * FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
dialect.getLimitHandler().processSql( query, toRowSelection( 1, 25 ) )
);
}
@Test
@TestForIssue(jiraKey = "HHH-8507")
public void testGetLimitStringWithNewlineAfterColumnList() {
final String query = "select E.fieldA,E.fieldB" + System.lineSeparator() + "FROM Employee E WHERE E.firstName = :firstName";
assertEquals(
"WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " +
"select E.fieldA as page0_,E.fieldB as page1_" + System.lineSeparator() +
"FROM Employee E WHERE E.firstName = :firstName ) inner_query ) SELECT page0_, page1_ FROM query " +
"WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
dialect.getLimitHandler().processSql( query, toRowSelection( 1, 25 ) )
);
} }
@Test @Test
@ -159,9 +194,9 @@ public class SQLServer2005DialectTestCase extends BaseUnitTestCase {
assertEquals( assertEquals(
"WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " + "WITH query AS (SELECT inner_query.*, ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as __hibernate_row_nr__ FROM ( " +
"select TOP(?) cast(lc302_doku6_.redniBrojStavke as varchar(255)) as col_0_0_, lc302_doku6_.dokumentiID as col_1_0_ " + "select TOP(?) cast(lc302_doku6_.redniBrojStavke as varchar(255)) as col_0_0_, lc302_doku6_.dokumentiID as col_1_0_ " +
"from LC302_Dokumenti lc302_doku6_ order by lc302_doku6_.dokumentiID DESC ) inner_query ) " + "from LC302_Dokumenti lc302_doku6_ order by lc302_doku6_.dokumentiID DESC ) inner_query ) " +
"SELECT col_0_0_, col_1_0_ FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?", "SELECT col_0_0_, col_1_0_ FROM query WHERE __hibernate_row_nr__ >= ? AND __hibernate_row_nr__ < ?",
dialect.getLimitHandler().processSql( query, toRowSelection( 1, 3 ) ) dialect.getLimitHandler().processSql( query, toRowSelection( 1, 3 ) )
); );
} }