diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB297Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB297Dialect.java index fd80bc4eeb2b..314f1eba2d54 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB297Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB297Dialect.java @@ -6,11 +6,14 @@ */ package org.hibernate.dialect; +import org.hibernate.dialect.function.DB2SubstringFunction; +import org.hibernate.dialect.function.StandardSQLFunction; import org.hibernate.hql.spi.id.IdTableSupportStandardImpl; import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; import org.hibernate.hql.spi.id.global.GlobalTemporaryTableBulkIdStrategy; import org.hibernate.hql.spi.id.local.AfterUseAction; import org.hibernate.hql.spi.id.local.LocalTemporaryTableBulkIdStrategy; +import org.hibernate.type.StandardBasicTypes; /** * An SQL dialect for DB2 9.7. @@ -19,6 +22,11 @@ */ public class DB297Dialect extends DB2Dialect { + public DB297Dialect() { + super(); + registerFunction( "substring", new DB2SubstringFunction() ); + } + @Override public String getCrossJoinSeparator() { // DB2 9.7 and later support "cross join" diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/DB2SubstringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/DB2SubstringFunction.java new file mode 100644 index 000000000000..787ad14a6c4d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/DB2SubstringFunction.java @@ -0,0 +1,51 @@ +/* + * 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 . + */ +package org.hibernate.dialect.function; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.hibernate.type.StandardBasicTypes; + +/** + * When "substring" function is used for DB2, this implementation of {@link StandardSQLFunction} + * will render "substr" or "substring", depending on the last argument being used. If the last + * argument is a string unit ("CODEUNITS16", "CODEUNITS32", or "OCTETS"), then the function + * will be rendered as "substring"; otherwise, it will be rendered as "substr". + *

+ * ANSI SQL-92 standard defines "substring" without string units, which is more similar to DB2's "substr", + * so it makes sense to use DB2's "substr" function when string units are not provided. + *

+ * Background: DB2 has both "substr" and "substring", which are different functions that are not + * interchangeable. Prior to DB2 11.1, DB2's "substring" function requires an argument for string + * units; without this argument, DB2 throws an exception. DB2's "substr" function throws an exception + * if string unit is provided as an argument. + * + * @author Gail Badner + */ +public class DB2SubstringFunction extends StandardSQLFunction { + private static final Set possibleStringUnits = new HashSet( + Arrays.asList( "CODEUNITS16", "CODEUNITS32", "OCTETS" ) + ); + + public DB2SubstringFunction() { + super( "substring", StandardBasicTypes.STRING ); + } + + @Override + protected String getRenderedName(List arguments) { + final String lastArgument = (String) arguments.get( arguments.size() - 1 ); + if ( lastArgument != null && possibleStringUnits.contains( lastArgument.toUpperCase() ) ) { + return getName(); + } + else{ + return "substr"; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/StandardSQLFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/StandardSQLFunction.java index d3fd714b9d51..bdd98d3a39b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/StandardSQLFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/StandardSQLFunction.java @@ -86,7 +86,7 @@ public Type getReturnType(Type firstArgumentType, Mapping mapping) { @Override public String render(Type firstArgumentType, List arguments, SessionFactoryImplementor sessionFactory) { final StringBuilder buf = new StringBuilder(); - buf.append( name ).append( '(' ); + buf.append( getRenderedName( arguments) ).append( '(' ); for ( int i = 0; i < arguments.size(); i++ ) { buf.append( arguments.get( i ) ); if ( i < arguments.size() - 1 ) { @@ -96,6 +96,10 @@ public String render(Type firstArgumentType, List arguments, SessionFactoryImple return buf.append( ')' ).toString(); } + protected String getRenderedName(List arguments) { + return getName(); + } + @Override public String toString() { return name; diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/DB297SubStringFunctionsTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/DB297SubStringFunctionsTest.java new file mode 100644 index 000000000000..6a83bf8f1848 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/DB297SubStringFunctionsTest.java @@ -0,0 +1,188 @@ +/* + * 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 . + */ +package org.hibernate.test.hql; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.PersistenceException; + +import org.hibernate.Session; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; +import org.hibernate.dialect.DB297Dialect; +import org.hibernate.exception.SQLGrammarException; +import org.hibernate.resource.jdbc.spi.StatementInspector; + +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * DB2 has 2 functions for getting a substring: "substr" and "substring" + * + * @author Gail Badner + */ +@RequiresDialect(DB297Dialect.class) +public class DB297SubStringFunctionsTest extends BaseCoreFunctionalTestCase { + private static final MostRecentStatementInspector mostRecentStatementInspector = new MostRecentStatementInspector(); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + AnEntity.class + }; + } + + @Override + protected void configure(Configuration configuration) { + super.configure( configuration ); + configuration.getProperties().put( Environment.STATEMENT_INSPECTOR, mostRecentStatementInspector ); + } + + @Before + public void setup() { + AnEntity anEntity = new AnEntity(); + anEntity.description = "A very long, boring description."; + + Session session = openSession(); + session.beginTransaction(); + { + session.persist( anEntity ); + } + session.getTransaction().commit(); + session.close(); + } + + @After + public void cleanup() { + Session session = openSession(); + session.beginTransaction(); + { + session.createQuery( "delete from AnEntity" ).executeUpdate(); + } + session.getTransaction().commit(); + session.close(); + } + + @Test + @TestForIssue( jiraKey = "HHH-11957") + public void testSubstringWithStringUnits() { + + mostRecentStatementInspector.clear(); + + Session session = openSession(); + session.beginTransaction(); + { + String value = (String) session.createQuery( + "select substring( e.description, 21, 11, octets ) from AnEntity e" + ).uniqueResult(); + assertEquals( "description", value ); + } + session.getTransaction().commit(); + session.close(); + + assertTrue( mostRecentStatementInspector.mostRecentSql.contains( "substring(" ) ); + assertTrue( mostRecentStatementInspector.mostRecentSql.contains( "octets" ) ); + } + + @Test + @TestForIssue( jiraKey = "HHH-11957") + public void testSubstringWithoutStringUnits() { + + mostRecentStatementInspector.clear(); + + Session session = openSession(); + session.beginTransaction(); + { + String value = (String) session.createQuery( + "select substring( e.description, 21, 11 ) from AnEntity e" + ).uniqueResult(); + assertEquals( "description", value ); + } + session.getTransaction().commit(); + session.close(); + + assertTrue( mostRecentStatementInspector.mostRecentSql.contains( "substr(" ) ); + } + + @Test + @TestForIssue( jiraKey = "HHH-11957") + public void testSubstrWithStringUnits() { + + mostRecentStatementInspector.clear(); + + Session session = openSession(); + session.beginTransaction(); + + try { + String value = (String) session.createQuery( + "select substr( e.description, 21, 11, octets ) from AnEntity e" + ).uniqueResult(); + assertEquals( "description", value ); + fail( "Should have failed because substr cannot be used with string units." ); + } + catch (SQLGrammarException expected) { + // expected + } + finally { + session.getTransaction().rollback(); + session.close(); + } + + assertTrue( mostRecentStatementInspector.mostRecentSql.contains( "substr(" ) ); + assertTrue( mostRecentStatementInspector.mostRecentSql.contains( "octets" ) ); + } + + @Test + @TestForIssue( jiraKey = "HHH-11957") + public void testSubstrWithoutStringUnits() { + + mostRecentStatementInspector.clear(); + + Session session = openSession(); + session.beginTransaction(); + { + String value = (String) session.createQuery( + "select substr( e.description, 21, 11 ) from AnEntity e" + ).uniqueResult(); + assertEquals( "description", value ); + } + session.getTransaction().commit(); + session.close(); + + assertTrue( mostRecentStatementInspector.mostRecentSql.contains( "substr(" ) ); + } + + @Entity(name="AnEntity") + public static class AnEntity { + @Id + @GeneratedValue + private long id; + private String description; + } + + private static class MostRecentStatementInspector implements StatementInspector { + private String mostRecentSql; + + public String inspect(String sql) { + mostRecentSql = sql; + return sql; + } + private void clear() { + mostRecentSql = null; + } + } +} \ No newline at end of file