HHH-11816 - JoinProcessor considers table names with colons dynamic filter parameters

This commit is contained in:
Jonathan Bregler 2017-09-18 15:02:35 +02:00 committed by Vlad Mihalcea
parent 00492a3707
commit 62e691c38a
3 changed files with 327 additions and 7 deletions

View File

@ -19,8 +19,10 @@ import java.util.Map;
import java.util.Set;
import org.hibernate.QueryException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.internal.JoinSequence;
import org.hibernate.engine.internal.ParameterBinder;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.hql.internal.CollectionProperties;
import org.hibernate.hql.internal.antlr.HqlSqlBaseWalker;
@ -1420,6 +1422,10 @@ public class HqlSqlWalker extends HqlSqlBaseWalker implements ErrorReporter, Par
return hqlParser.getTreatMap().get( path );
}
public Dialect getDialect() {
return sessionFactoryHelper.getFactory().getServiceRegistry().getService( JdbcServices.class ).getDialect();
}
public static void panic() {
throw new QueryException( "TreeWalker: panic" );
}

View File

@ -12,6 +12,8 @@ import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hibernate.AssertionFailure;
import org.hibernate.dialect.Dialect;
@ -46,6 +48,9 @@ import org.hibernate.type.Type;
public class JoinProcessor implements SqlTokenTypes {
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( JoinProcessor.class );
private static final Pattern DYNAMIC_FILTER_PATTERN = Pattern.compile(":(\\w+\\S*)\\s");
private static final String LITERAL_DELIMITER = "'";
private final HqlSqlWalker walker;
private final SyntheticAndFactory syntheticAndFactory;
@ -98,7 +103,7 @@ public class JoinProcessor implements SqlTokenTypes {
// found it easiest to simply reorder the FromElements here into ascending order
// in terms of injecting them into the resulting sql ast in orders relative to those
// expected by the old parser; this is definitely another of those "only needed
// for regression purposes". The SyntheticAndFactory, then, simply injects them as it
// for regression purposes". The SyntheticAndFactory, then, simply injects them as it
// encounters them.
fromElements = new ArrayList();
ListIterator liter = fromClause.getFromElements().listIterator( fromClause.getFromElements().size() );
@ -118,7 +123,7 @@ public class JoinProcessor implements SqlTokenTypes {
&& fromElement.getOrigin().getWithClauseFragment().contains( fromElement.getTableAlias() ) ) {
fromElement.getOrigin().getJoinSequence().addJoin( (ImpliedFromElement) fromElement );
// This from element will be rendered as part of the origins join sequence
fromElement.setText("");
fromElement.setText( "" );
}
else {
fromElements.add( fromElement );
@ -203,7 +208,7 @@ public class JoinProcessor implements SqlTokenTypes {
private String processFromFragment(String frag, JoinSequence join) {
String fromFragment = frag.trim();
// The FROM fragment will probably begin with ', '. Remove this if it is present.
// The FROM fragment will probably begin with ', '. Remove this if it is present.
if ( fromFragment.startsWith( ", " ) ) {
fromFragment = fromFragment.substring( 2 );
}
@ -215,12 +220,12 @@ public class JoinProcessor implements SqlTokenTypes {
final ParameterContainer container,
final HqlSqlWalker walker) {
if ( walker.getEnabledFilters().isEmpty()
&& ( !hasDynamicFilterParam( sqlFragment ) )
&& ( !hasDynamicFilterParam( walker, sqlFragment ) )
&& ( !( hasCollectionFilterParam( sqlFragment ) ) ) ) {
return;
}
Dialect dialect = walker.getSessionFactoryHelper().getFactory().getDialect();
Dialect dialect = walker.getDialect();
String symbols = ParserHelper.HQL_SEPARATORS + dialect.openQuote() + dialect.closeQuote();
StringTokenizer tokens = new StringTokenizer( sqlFragment, symbols, true );
StringBuilder result = new StringBuilder();
@ -261,8 +266,15 @@ public class JoinProcessor implements SqlTokenTypes {
container.setText( result.toString() );
}
private static boolean hasDynamicFilterParam(String sqlFragment) {
return !sqlFragment.contains( ParserHelper.HQL_VARIABLE_PREFIX );
private static boolean hasDynamicFilterParam(HqlSqlWalker walker, String sqlFragment) {
String closeQuote = String.valueOf( walker.getDialect().closeQuote() );
Matcher matcher = DYNAMIC_FILTER_PATTERN.matcher( sqlFragment );
if ( matcher.find() && matcher.groupCount() > 0 ) {
String match = matcher.group( 1 );
return match.endsWith( closeQuote ) || match.endsWith( LITERAL_DELIMITER );
}
return true;
}
private static boolean hasCollectionFilterParam(String sqlFragment) {

View File

@ -0,0 +1,302 @@
/*
* 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.test.hql;
import static org.hamcrest.core.Is.is;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertThat;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.hibernate.annotations.NaturalId;
import org.hibernate.testing.Skip;
import org.hibernate.testing.TestForIssue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* @author Jonathan Bregler
*/
public class EntityWithUnusualTableNameJoinTest extends EntityJoinTest {
@Override
protected Class[] getAnnotatedClasses() {
return new Class[]{ FinancialRecord.class, User.class, Customer.class, Account.class };
}
@Override
@Before
public void prepare() {
createTestData();
}
@Override
@After
public void cleanup() {
deleteTestData();
}
@Test
@TestForIssue(jiraKey = "HHH-11816")
public void testInnerEntityJoinsWithVariable() {
doInHibernate( this::sessionFactory, session -> {
// this should get financial records which have a lastUpdateBy user set
List<Object[]> result = session.createQuery(
"select r.id, c.name, u.id, u.username " +
"from FinancialRecord r " +
" inner join r.customer c " +
" inner join User u on r.lastUpdateBy = u.username and u.username=:username" )
.setParameter( "username", "steve" ).list();
assertThat( Integer.valueOf( result.size() ), is( Integer.valueOf( 1 ) ) );
Object[] steveAndAcme = result.get( 0 );
assertThat( steveAndAcme[0], is( Integer.valueOf( 1 ) ) );
assertThat( steveAndAcme[1], is( "Acme" ) );
assertThat( steveAndAcme[3], is( "steve" ) );
} );
}
@Test
@TestForIssue(jiraKey = "HHH-11816")
public void testInnerEntityJoinsWithVariableSingleQuoted() {
doInHibernate( this::sessionFactory, session -> {
// this should get financial records which have a lastUpdateBy user set
List<Object[]> result = session.createQuery(
"select r.id, c.name, a.id, a.accountname, r.lastUpdateBy " +
"from FinancialRecord r " +
" inner join r.customer c " +
" inner join Account a on a.customer = c and a.accountname!='test:account' and a.accountname=:accountname and r.lastUpdateBy != null" )
.setParameter( "accountname", "DEBIT" ).list();
assertThat( Integer.valueOf( result.size() ), is( Integer.valueOf( 1 ) ) );
Object[] steveAndAcmeAndDebit = result.get( 0 );
assertThat( steveAndAcmeAndDebit[0], is( Integer.valueOf( 1 ) ) );
assertThat( steveAndAcmeAndDebit[1], is( "Acme" ) );
assertThat( steveAndAcmeAndDebit[3], is( "DEBIT" ) );
assertThat( steveAndAcmeAndDebit[4], is( "steve" ) );
} );
}
@Override
@Skip(message = "The superclass test checks for the table name which is different in this test case and causes the test to fail", condition = Skip.AlwaysSkip.class)
public void testNoImpliedJoinGeneratedForEqualityComparison() {
}
private void createTestData() {
doInHibernate( this::sessionFactory, session -> {
final Customer customer = new Customer( Integer.valueOf( 1 ), "Acme" );
session.save( customer );
session.save( new User( Integer.valueOf( 1 ), "steve", customer ) );
session.save( new User( Integer.valueOf( 2 ), "jane" ) );
session.save( new FinancialRecord( Integer.valueOf( 1 ), customer, "steve" ) );
session.save( new FinancialRecord( Integer.valueOf( 2 ), customer, null ) );
session.save( new Account( Integer.valueOf( 1 ), "DEBIT", customer ) );
session.save( new Account( Integer.valueOf( 2 ), "CREDIT" ) );
} );
}
private void deleteTestData() {
doInHibernate( this::sessionFactory, session -> {
session.createQuery( "delete FinancialRecord" ).executeUpdate();
session.createQuery( "delete User" ).executeUpdate();
session.createQuery( "delete Account" ).executeUpdate();
session.createQuery( "delete Customer" ).executeUpdate();
} );
}
@Entity(name = "Customer")
@Table(name = "`my::customer`")
public static class Customer {
private Integer id;
private String name;
public Customer() {
}
public Customer(Integer id, String name) {
this.id = id;
this.name = name;
}
@Id
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
@Entity(name = "FinancialRecord")
@Table(name = "`financial?record`")
public static class FinancialRecord {
private Integer id;
private Customer customer;
private String lastUpdateBy;
public FinancialRecord() {
}
public FinancialRecord(Integer id, Customer customer, String lastUpdateBy) {
this.id = id;
this.customer = customer;
this.lastUpdateBy = lastUpdateBy;
}
@Id
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@ManyToOne
@JoinColumn
public Customer getCustomer() {
return this.customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public String getLastUpdateBy() {
return this.lastUpdateBy;
}
public void setLastUpdateBy(String lastUpdateBy) {
this.lastUpdateBy = lastUpdateBy;
}
}
@Entity(name = "User")
@Table(name = "`my::user`")
public static class User {
private Integer id;
private String username;
private Customer customer;
public User() {
}
public User(Integer id, String username) {
this.id = id;
this.username = username;
}
public User(Integer id, String username, Customer customer) {
this.id = id;
this.username = username;
this.customer = customer;
}
@Id
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@NaturalId
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
@ManyToOne(fetch = FetchType.LAZY)
public Customer getCustomer() {
return this.customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}
@Entity(name = "Account")
@Table(name = "`account`")
public static class Account {
private Integer id;
private String accountname;
private Customer customer;
public Account() {
}
public Account(Integer id, String accountname) {
this.id = id;
this.accountname = accountname;
}
public Account(Integer id, String accountname, Customer customer) {
this.id = id;
this.accountname = accountname;
this.customer = customer;
}
@Id
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
@NaturalId
public String getAccountname() {
return this.accountname;
}
public void setAccountname(String accountname) {
this.accountname = accountname;
}
@ManyToOne(fetch = FetchType.LAZY)
public Customer getCustomer() {
return this.customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}
}