HHH-18033 Fix LimitHandler detect wrong statement end if sql contains quoted semicolon

This commit is contained in:
Yanming Zhou 2024-04-30 12:25:50 +08:00 committed by Christian Beikov
parent 7b0d66782d
commit df7f104689
15 changed files with 314 additions and 23 deletions

View File

@ -66,7 +66,7 @@ public class IngresLimitHandler extends OffsetFetchLimitHandler {
}; };
private static final Pattern WITH_OPTION_PATTERN = private static final Pattern WITH_OPTION_PATTERN =
Pattern.compile("\\s+with\\s+(" + String.join("|", WITH_OPTIONS) + ")\\b|\\s*(;|$)"); Pattern.compile("\\s+with\\s+(" + String.join("|", WITH_OPTIONS) + ")\\b|\\s*;?\\s*$");
/** /**
* The offset/fetch clauses must come before * The offset/fetch clauses must come before

View File

@ -47,7 +47,7 @@ public class RowsLimitHandler extends AbstractSimpleLimitHandler {
} }
private static final Pattern FOR_UPDATE_PATTERN = private static final Pattern FOR_UPDATE_PATTERN =
compile("\\s+for\\s+update\\b|\\s+with\\s+lock\\b|\\s*(;|$)", CASE_INSENSITIVE); compile("\\s+for\\s+update\\b|\\s+with\\s+lock\\b|\\s*;?\\s*$", CASE_INSENSITIVE);
@Override @Override
protected Pattern getForUpdatePattern() { protected Pattern getForUpdatePattern() {

View File

@ -0,0 +1,22 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.community.dialect;
import org.hibernate.community.dialect.pagination.IngresLimitHandler;
import org.hibernate.dialect.pagination.AbstractLimitHandler;
import org.hibernate.orm.test.dialect.AbstractLimitHandlerTest;
/**
* @author Yanming Zhou
*/
public class IngresLimitHandlerTest extends AbstractLimitHandlerTest {
@Override
protected AbstractLimitHandler getLimitHandler() {
return IngresLimitHandler.INSTANCE;
}
}

View File

@ -0,0 +1,27 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.community.dialect;
import org.hibernate.community.dialect.pagination.RowsLimitHandler;
import org.hibernate.dialect.pagination.AbstractLimitHandler;
import org.hibernate.orm.test.dialect.AbstractLimitHandlerTest;
/**
* @author Yanming Zhou
*/
public class RowsLimitHandlerTest extends AbstractLimitHandlerTest {
@Override
protected AbstractLimitHandler getLimitHandler() {
return RowsLimitHandler.INSTANCE;
}
@Override
protected String getLimitClause() {
return " rows ?";
}
}

View File

@ -33,10 +33,10 @@ public abstract class AbstractLimitHandler implements LimitHandler {
compile( "^\\s*select(\\s+(distinct|all))?\\b", CASE_INSENSITIVE ); compile( "^\\s*select(\\s+(distinct|all))?\\b", CASE_INSENSITIVE );
private static final Pattern END_PATTERN = private static final Pattern END_PATTERN =
compile("\\s*(;|$)", CASE_INSENSITIVE); compile("\\s*;?\\s*$", CASE_INSENSITIVE);
private static final Pattern FOR_UPDATE_PATTERN = private static final Pattern FOR_UPDATE_PATTERN =
compile("\\s+for\\s+update\\b|\\s*(;|$)", CASE_INSENSITIVE); compile("\\s+for\\s+update\\b|\\s*;?\\s*$", CASE_INSENSITIVE);
@Override @Override

View File

@ -29,7 +29,7 @@ public class DerbyLimitHandler extends OffsetFetchLimitHandler {
} }
private static final Pattern FOR_UPDATE_WITH_LOCK_PATTERN = private static final Pattern FOR_UPDATE_WITH_LOCK_PATTERN =
Pattern.compile("\\s+for\\s+(update|read|fetch)\\b|\\s+with\\s+(rr|rs|cs|ur)\\b|\\s*(;|$)"); Pattern.compile("\\s+for\\s+(update|read|fetch)\\b|\\s+with\\s+(rr|rs|cs|ur)\\b|\\s*;?\\s*$");
/** /**
* The offset/fetch clauses must come before the * The offset/fetch clauses must come before the

View File

@ -34,7 +34,7 @@ public class LimitLimitHandler extends AbstractSimpleLimitHandler {
} }
private static final Pattern FOR_UPDATE_PATTERN = private static final Pattern FOR_UPDATE_PATTERN =
compile("\\s+for\\s+update\\b|\\s+lock\\s+in\\s+shared\\s+mode\\b|\\s*(;|$)", CASE_INSENSITIVE); compile("\\s+for\\s+update\\b|\\s+lock\\s+in\\s+shared\\s+mode\\b|\\s*;?\\s*$", CASE_INSENSITIVE);
@Override @Override
protected Pattern getForUpdatePattern() { protected Pattern getForUpdatePattern() {

View File

@ -99,7 +99,7 @@ public class Oracle12LimitHandler extends AbstractLimitHandler {
offsetFetchString = " fetch first ? rows only"; offsetFetchString = " fetch first ? rows only";
} }
return sql + offsetFetchString; return insertAtEnd(offsetFetchString, sql);
} }
/** /**

View File

@ -0,0 +1,73 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.dialect;
import org.hibernate.dialect.pagination.LimitHandler;
import org.hibernate.dialect.pagination.OffsetFetchLimitHandler;
import org.hibernate.query.spi.Limit;
import org.hibernate.query.spi.QueryOptions;
import org.junit.jupiter.api.Test;
import static org.hibernate.dialect.pagination.AbstractLimitHandler.hasFirstRow;
import static org.hibernate.dialect.pagination.AbstractLimitHandler.hasMaxRows;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @author Yanming Zhou
*/
public abstract class AbstractLimitHandlerTest {
@Test
public void testSqlWithSemicolonInsideQuotedString() {
String sql = "select * from Person p where p.name like ';'";
String expected = "select * from Person p where p.name like ';'" + getLimitClause();
assertGenerateExpectedSql(expected, sql);
sql = "select * from Person p where p.name like ';' ";
expected = "select * from Person p where p.name like ';'" + getLimitClause() + " ";
assertGenerateExpectedSql(expected, sql);
}
@Test
public void testSqlWithSemicolonInsideQuotedStringAndEndsWithSemicolon() {
String sql = "select * from Person p where p.name like ';';";
String expected = "select * from Person p where p.name like ';'" + getLimitClause() + ";";
assertGenerateExpectedSql(expected, sql);
sql = "select * from Person p where p.name like ';' ; ";
expected = "select * from Person p where p.name like ';'" + getLimitClause() + " ; ";
assertGenerateExpectedSql(expected, sql);
}
protected void assertGenerateExpectedSql(String expected, String sql) {
assertEquals(expected, getLimitHandler().processSql(sql, getLimit(), QueryOptions.NONE));
}
protected abstract LimitHandler getLimitHandler();
protected Limit getLimit() {
return new Limit(0, 10);
}
protected String getLimitClause() {
LimitHandler handler = getLimitHandler();
if (handler instanceof OffsetFetchLimitHandler) {
OffsetFetchLimitHandler oflh = (OffsetFetchLimitHandler) handler;
Limit limit = getLimit();
if (hasFirstRow(limit) && hasMaxRows(limit)) {
return " offset " + (oflh.supportsVariableLimit() ? "?" : String.valueOf(limit.getFirstRow()))
+ " rows fetch next " + (oflh.supportsVariableLimit() ? "?" : String.valueOf(limit.getMaxRows())) + " rows only";
}
else if (hasFirstRow(limit)) {
return " offset " + (oflh.supportsVariableLimit() ? "?" : String.valueOf(limit.getFirstRow())) + " rows";
} else {
return " fetch first " + (oflh.supportsVariableLimit() ? "?" : String.valueOf(limit.getMaxRows())) + " rows only";
}
}
return " limit ?";
}
}

View File

@ -0,0 +1,21 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.dialect;
import org.hibernate.dialect.pagination.AbstractLimitHandler;
import org.hibernate.dialect.pagination.DB2LimitHandler;
/**
* @author Yanming Zhou
*/
public class DB2LimitHandlerTest extends AbstractLimitHandlerTest {
@Override
protected AbstractLimitHandler getLimitHandler() {
return DB2LimitHandler.INSTANCE;
}
}

View File

@ -0,0 +1,21 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.dialect;
import org.hibernate.dialect.pagination.AbstractLimitHandler;
import org.hibernate.dialect.pagination.DerbyLimitHandler;
/**
* @author Yanming Zhou
*/
public class DerbyLimitHandlerTest extends AbstractLimitHandlerTest {
@Override
protected AbstractLimitHandler getLimitHandler() {
return DerbyLimitHandler.INSTANCE;
}
}

View File

@ -0,0 +1,21 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.dialect;
import org.hibernate.dialect.pagination.AbstractLimitHandler;
import org.hibernate.dialect.pagination.LimitLimitHandler;
/**
* @author Yanming Zhou
*/
public class LimitLimitHandlerTest extends AbstractLimitHandlerTest {
@Override
protected AbstractLimitHandler getLimitHandler() {
return LimitLimitHandler.INSTANCE;
}
}

View File

@ -0,0 +1,21 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.orm.test.dialect;
import org.hibernate.dialect.pagination.AbstractLimitHandler;
import org.hibernate.dialect.pagination.OffsetFetchLimitHandler;
/**
* @author Yanming Zhou
*/
public class OffsetFetchLimitHandlerTest extends AbstractLimitHandlerTest {
@Override
protected AbstractLimitHandler getLimitHandler() {
return OffsetFetchLimitHandler.INSTANCE;
}
}

View File

@ -6,27 +6,45 @@
*/ */
package org.hibernate.orm.test.dialect; package org.hibernate.orm.test.dialect;
import org.hibernate.dialect.pagination.AbstractLimitHandler;
import org.hibernate.dialect.pagination.Oracle12LimitHandler; import org.hibernate.dialect.pagination.Oracle12LimitHandler;
import org.hibernate.query.spi.Limit; import org.hibernate.query.spi.Limit;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.testing.TestForIssue; import org.hibernate.testing.TestForIssue;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.Assert.assertEquals; import static org.hibernate.dialect.pagination.AbstractLimitHandler.hasFirstRow;
import static org.hibernate.dialect.pagination.AbstractLimitHandler.hasMaxRows;
@TestForIssue( jiraKey = "HHH-14649") @TestForIssue( jiraKey = "HHH-14649")
public class Oracle12LimitHandlerTest { public class Oracle12LimitHandlerTest extends AbstractLimitHandlerTest {
@Override
protected AbstractLimitHandler getLimitHandler() {
return Oracle12LimitHandler.INSTANCE;
}
@Override
protected String getLimitClause() {
Limit limit = getLimit();
if ( hasFirstRow(limit) && hasMaxRows(limit) ) {
return " offset ? rows fetch next ? rows only";
}
else if ( hasFirstRow(limit) ) {
return " offset ? rows";
}
else {
return " fetch first ? rows only";
}
}
@Test @Test
public void testSqlWithSpace() { public void testSqlWithSpace() {
final String sql = "select p.name from Person p where p.id = 1 for update"; final String sql = "select p.name from Person p where p.id = 1 for update";
final String expected = "select * from (select p.name from Person p where p.id = 1) where rownum<=? for update"; final String expected = "select * from (select p.name from Person p where p.id = 1) where rownum<=? for update";
final String processedSql = Oracle12LimitHandler.INSTANCE.processSql( sql, new Limit( 0, 5 ), QueryOptions.NONE ); assertGenerateExpectedSql(expected, sql);
assertEquals( expected, processedSql );
} }
@Test @Test
@ -34,9 +52,7 @@ public class Oracle12LimitHandlerTest {
final String sql = "select p.name from Person p where p.name = ' this is a string with spaces ' for update"; final String sql = "select p.name from Person p where p.name = ' this is a string with spaces ' for update";
final String expected = "select * from (select p.name from Person p where p.name = ' this is a string with spaces ') where rownum<=? for update"; final String expected = "select * from (select p.name from Person p where p.name = ' this is a string with spaces ') where rownum<=? for update";
final String processedSql = Oracle12LimitHandler.INSTANCE.processSql( sql, new Limit( 0, 5 ), QueryOptions.NONE ); assertGenerateExpectedSql(expected, sql);
assertEquals( expected, processedSql );
} }
@Test @Test
@ -44,9 +60,7 @@ public class Oracle12LimitHandlerTest {
final String sql = "select a.prop from A a where a.name = 'this is for update '"; final String sql = "select a.prop from A a where a.name = 'this is for update '";
final String expected = "select a.prop from A a where a.name = 'this is for update ' fetch first ? rows only"; final String expected = "select a.prop from A a where a.name = 'this is for update ' fetch first ? rows only";
final String processedSql = Oracle12LimitHandler.INSTANCE.processSql( sql, new Limit( 0, 5 ), QueryOptions.NONE ); assertGenerateExpectedSql(expected, sql);
assertEquals( expected, processedSql );
} }
@Test @Test
@ -54,9 +68,7 @@ public class Oracle12LimitHandlerTest {
final String sql = "select a.prop from A a where a.name = 'this is for update ' for update"; final String sql = "select a.prop from A a where a.name = 'this is for update ' for update";
final String expected = "select * from (select a.prop from A a where a.name = 'this is for update ') where rownum<=? for update"; final String expected = "select * from (select a.prop from A a where a.name = 'this is for update ') where rownum<=? for update";
final String processedSql = Oracle12LimitHandler.INSTANCE.processSql( sql, new Limit( 0, 5 ), QueryOptions.NONE ); assertGenerateExpectedSql(expected, sql);
assertEquals( expected, processedSql );
} }
} }

View File

@ -28,8 +28,11 @@ import org.hibernate.Hibernate;
import org.hibernate.QueryException; import org.hibernate.QueryException;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.CockroachDialect;
import org.hibernate.dialect.DerbyDialect;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.PostgreSQLDialect;
import org.hibernate.dialect.PostgresPlusDialect; import org.hibernate.dialect.PostgresPlusDialect;
import org.hibernate.dialect.SybaseDialect;
import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase;
import org.hibernate.orm.test.jpa.Distributor; import org.hibernate.orm.test.jpa.Distributor;
@ -40,6 +43,7 @@ import org.hibernate.stat.Statistics;
import org.hibernate.testing.SkipForDialect; import org.hibernate.testing.SkipForDialect;
import org.hibernate.testing.TestForIssue; import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.JiraKey;
import org.junit.Test; import org.junit.Test;
import junit.framework.Assert; import junit.framework.Assert;
@ -55,6 +59,7 @@ import static org.junit.Assert.fail;
* @author Emmanuel Bernard * @author Emmanuel Bernard
* @author Steve Ebersole * @author Steve Ebersole
* @author Chris Cranford * @author Chris Cranford
* @author Yanming Zhou
*/ */
public class QueryTest extends BaseEntityManagerFunctionalTestCase { public class QueryTest extends BaseEntityManagerFunctionalTestCase {
@Override @Override
@ -546,6 +551,74 @@ public class QueryTest extends BaseEntityManagerFunctionalTestCase {
} }
} }
@Test
@JiraKey("HHH-18033")
public void testQueryContainsQuotedSemicolonWithLimit() {
EntityManager em = getOrCreateEntityManager();
em.getTransaction().begin();
try {
em.persist( new Item( "Mouse;", "Micro$oft mouse" ) );
Query q = em.createQuery( "from Item where name like '%;%'" ).setMaxResults(10);
assertEquals( 1, q.getResultList().size() );
q = em.createQuery( "from Item where name like '%;%' " ).setMaxResults(10);
assertEquals( 1, q.getResultList().size() );
}
finally {
if ( em.getTransaction() != null && em.getTransaction().isActive() ) {
em.getTransaction().rollback();
}
em.close();
}
}
@Test
@JiraKey("HHH-18033")
public void testNativeQueryContainsQuotedSemicolonWithLimit() {
EntityManager em = getOrCreateEntityManager();
em.getTransaction().begin();
try {
em.persist( new Item( "Mouse;", "Micro$oft mouse" ) );
Query q = em.createNativeQuery( "select * from Item where name like '%;%'" ).setMaxResults(10);
assertEquals( 1, q.getResultList().size() );
q = em.createNativeQuery( "select * from Item where name like '%;%' " ).setMaxResults(10);
assertEquals( 1, q.getResultList().size() );
}
finally {
if ( em.getTransaction() != null && em.getTransaction().isActive() ) {
em.getTransaction().rollback();
}
em.close();
}
}
@Test
@SkipForDialect(value = OracleDialect.class, jiraKey = "HHH-18033", comment = "Doesn't support semicolon as ending of statement")
@SkipForDialect(value = SybaseDialect.class, jiraKey = "HHH-18033", comment = "Doesn't support semicolon as ending of statement")
@SkipForDialect(value = DerbyDialect.class, jiraKey = "HHH-18033", comment = "Doesn't support semicolon as ending of statement")
public void testNativeQueryContainsQuotedSemicolonAndEndsWithSemicolonWithLimit() {
EntityManager em = getOrCreateEntityManager();
em.getTransaction().begin();
try {
em.persist( new Item( "Mouse;", "Micro$oft mouse" ) );
Query q = em.createNativeQuery( "select * from Item where name like '%;%';" ).setMaxResults(10);
assertEquals( 1, q.getResultList().size() );
q = em.createNativeQuery( "select * from Item where name like '%;%' ; " ).setMaxResults(10);
assertEquals( 1, q.getResultList().size() );
}
finally {
if ( em.getTransaction() != null && em.getTransaction().isActive() ) {
em.getTransaction().rollback();
}
em.close();
}
}
@Test @Test
public void testAggregationReturnType() throws Exception { public void testAggregationReturnType() throws Exception {
EntityManager em = getOrCreateEntityManager(); EntityManager em = getOrCreateEntityManager();