diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2012Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2012Dialect.java index 79467066f1..e25b004a03 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2012Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2012Dialect.java @@ -23,7 +23,10 @@ */ package org.hibernate.dialect; +import java.util.ArrayList; +import java.util.List; +import org.hibernate.internal.util.StringHelper; /** * Microsoft SQL Server 2012 Dialect * @@ -65,4 +68,29 @@ public class SQLServer2012Dialect extends SQLServer2008Dialect { public String getQuerySequencesString() { return "select name from sys.sequences"; } + + @Override + public String getQueryHintString(String sql, List hints) { + final String hint = StringHelper.join(", ", hints.iterator()); + + if (StringHelper.isEmpty(hint)) { + return sql; + } + + final StringBuilder buffer = new StringBuilder(sql.length() + + hint.length() + 12); + final int pos = sql.indexOf(";"); + if (pos > -1) { + buffer.append(sql.substring(0, pos)); + } else { + buffer.append(sql); + } + buffer.append(" OPTION (").append(hint).append(")"); + if (pos > -1) { + buffer.append(";"); + } + sql = buffer.toString(); + + return sql; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/test/queryhint/QueryHintSQLServer2012Test.java b/hibernate-core/src/test/java/org/hibernate/test/queryhint/QueryHintSQLServer2012Test.java new file mode 100644 index 0000000000..47aa0234a5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/queryhint/QueryHintSQLServer2012Test.java @@ -0,0 +1,177 @@ + +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * JBoss, Home of Professional Open Source + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @authors tag. All rights reserved. + * See the copyright.txt in the distribution for a + * full listing of individual contributors. + * + * This copyrighted material is made available to anyone wishing to use, + * modify, copy, or redistribute it subject to the terms and conditions + * of the GNU Lesser General Public License, v. 2.1. + * This program is distributed in the hope that it will be useful, but WITHOUT A + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * You should have received a copy of the GNU Lesser General Public License, + * v.2.1 along with this distribution; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ +package org.hibernate.test.queryhint; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToOne; + +import org.hibernate.Criteria; +import org.hibernate.Query; +import org.hibernate.Session; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.criterion.Restrictions; +import org.hibernate.dialect.SQLServer2012Dialect; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; + +/** + * @author Brett Meyer + */ + +@RequiresDialect(SQLServer2012Dialect.class) +public class QueryHintSQLServer2012Test extends BaseCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Employee.class, Department.class }; + } + + @Override + protected void configure(Configuration configuration) { + configuration.setProperty( AvailableSettings.DIALECT, QueryHintTestSQLServer2012Dialect.class.getName() ); + configuration.setProperty( AvailableSettings.USE_SQL_COMMENTS, "true" ); + } + + @Test + public void testQueryHint() { + Department department = new Department(); + department.name = "Sales"; + Employee employee1 = new Employee(); + employee1.department = department; + Employee employee2 = new Employee(); + employee2.department = department; + + Session s = openSession(); + s.getTransaction().begin(); + s.persist( department ); + s.persist( employee1 ); + s.persist( employee2 ); + s.getTransaction().commit(); + s.clear(); + + // test Query w/ a simple SQLServer2012 optimizer hint + s.getTransaction().begin(); + Query query = s.createQuery( "FROM QueryHintTest$Employee e WHERE e.department.name = :departmentName" ).addQueryHint( "MAXDOP 2" ) + .setParameter( "departmentName", "Sales" ); + List results = query.list(); + s.getTransaction().commit(); + s.clear(); + + assertEquals( results.size(), 2 ); + assertTrue( QueryHintTestSQLServer2012Dialect.getProcessedSql().contains( "OPTION (MAXDOP 2)" ) ); + + QueryHintTestSQLServer2012Dialect.resetProcessedSql(); + + // test multiple hints + s.getTransaction().begin(); + query = s.createQuery( "FROM QueryHintTest$Employee e WHERE e.department.name = :departmentName" ).addQueryHint( "MAXDOP 2" ) + .addQueryHint( "USE_CONCAT" ).setParameter( "departmentName", "Sales" ); + results = query.list(); + s.getTransaction().commit(); + s.clear(); + + assertEquals( results.size(), 2 ); + assertTrue( QueryHintTestSQLServer2012Dialect.getProcessedSql().contains( "OPTION (MAXDOP 2)" ) ); + + QueryHintTestSQLServer2012Dialect.resetProcessedSql(); + + // ensure the insertion logic can handle a comment appended to the front + s.getTransaction().begin(); + query = s.createQuery( "FROM QueryHintTest$Employee e WHERE e.department.name = :departmentName" ).setComment( "this is a test" ) + .addQueryHint( "MAXDOP 2" ).setParameter( "departmentName", "Sales" ); + results = query.list(); + s.getTransaction().commit(); + s.clear(); + + assertEquals( results.size(), 2 ); + assertTrue( QueryHintTestSQLServer2012Dialect.getProcessedSql().contains( "OPTION (MAXDOP 2)" ) ); + + QueryHintTestSQLServer2012Dialect.resetProcessedSql(); + + // test Criteria + s.getTransaction().begin(); + Criteria criteria = s.createCriteria( Employee.class ).addQueryHint( "MAXDOP 2" ).createCriteria( "department" ) + .add( Restrictions.eq( "name", "Sales" ) ); + results = criteria.list(); + s.getTransaction().commit(); + s.close(); + + assertEquals( results.size(), 2 ); + assertTrue( QueryHintTestSQLServer2012Dialect.getProcessedSql().contains( "OPTION (MAXDOP 2)" ) ); + assertEquals( false, true ); + } + + /** + * Since the query hint is added to the SQL during Loader's executeQueryStatement -> preprocessSQL, rather than + * early on during the QueryTranslator or QueryLoader initialization, there's not an easy way to check the full SQL + * after completely processing it. Instead, use this ridiculous hack to ensure Loader actually calls Dialect. TODO: + * This is terrible. Better ideas? + */ + public static class QueryHintTestSQLServer2012Dialect extends SQLServer2012Dialect { + + private static String processedSql; + + @Override + public String getQueryHintString(String sql, List hints) { + processedSql = super.getQueryHintString( sql, hints ); + return processedSql; + } + + public static String getProcessedSql() { + return processedSql; + } + + public static void resetProcessedSql() { + processedSql = ""; + } + } + + @Entity + public static class Employee { + + @Id + @GeneratedValue + public long id; + + @ManyToOne + public Department department; + } + + @Entity + public static class Department { + + @Id + @GeneratedValue + public long id; + + public String name; + } +}