diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java index 45f3c7cedd..90b293268f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java @@ -17,6 +17,7 @@ import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.RowLockStrategy; +import org.hibernate.dialect.SybaseDriverKind; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.TopLimitHandler; import org.hibernate.engine.jdbc.Size; @@ -123,7 +124,7 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect { // According to Wikipedia bigdatetime and bigtime were added in 15.5 // But with jTDS we can't use them as the driver can't handle the types - if ( getVersion().isSameOrAfter( 15, 5 ) && !jtdsDriver ) { + if ( getVersion().isSameOrAfter( 15, 5 ) && getDriverKind() != SybaseDriverKind.JTDS ) { ddlTypeRegistry.addDescriptor( CapacityDependentDdlType.builder( DATE, "bigdatetime", "bigdatetime", this ) .withTypeCapacity( 3, "datetime" ) @@ -230,7 +231,7 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect { .getJdbcTypeRegistry(); jdbcTypeRegistry.addDescriptor( Types.BOOLEAN, TinyIntJdbcType.INSTANCE ); // At least the jTDS driver does not support this type code - if ( jtdsDriver ) { + if ( getDriverKind() == SybaseDriverKind.JTDS ) { jdbcTypeRegistry.addDescriptor( Types.TIMESTAMP_WITH_TIMEZONE, TimestampJdbcType.INSTANCE ); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java index 9f07ef6d5f..9387f231df 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java @@ -15,6 +15,7 @@ import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.AbstractTransactSQLDialect; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.NationalizationSupport; +import org.hibernate.dialect.SybaseDriverKind; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.function.CountFunction; import org.hibernate.dialect.function.IntegralTimestampaddFunction; @@ -29,6 +30,7 @@ import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.procedure.internal.JTDSCallableStatementSupport; +import org.hibernate.procedure.internal.SybaseCallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; @@ -69,11 +71,13 @@ import jakarta.persistence.TemporalType; */ public class SybaseLegacyDialect extends AbstractTransactSQLDialect { - protected final boolean jtdsDriver; - //All Sybase dialects share an IN list size limit. private static final int PARAM_LIST_SIZE_LIMIT = 250000; private final UniqueDelegate uniqueDelegate = new SkipNullableUniqueDelegate(this); + private final SybaseDriverKind driverKind; + + @Deprecated(forRemoval = true) + protected final boolean jtdsDriver; public SybaseLegacyDialect() { this( DatabaseVersion.make( 11, 0 ) ); @@ -81,13 +85,18 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect { public SybaseLegacyDialect(DatabaseVersion version) { super(version); - jtdsDriver = true; + this.driverKind = SybaseDriverKind.OTHER; + this.jtdsDriver = true; } public SybaseLegacyDialect(DialectResolutionInfo info) { super(info); - jtdsDriver = info.getDriverName() != null - && info.getDriverName().contains( "jTDS" ); + this.driverKind = SybaseDriverKind.determineKind( info ); + this.jtdsDriver = driverKind == SybaseDriverKind.JTDS; + } + + public SybaseDriverKind getDriverKind() { + return driverKind; } @Override @@ -164,7 +173,7 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect { super.contributeTypes(typeContributions, serviceRegistry); final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() .getJdbcTypeRegistry(); - if ( jtdsDriver ) { + if ( driverKind == SybaseDriverKind.JTDS ) { jdbcTypeRegistry.addDescriptor( Types.TINYINT, TinyIntAsSmallIntJdbcType.INSTANCE ); // The jTDS driver doesn't support the JDBC4 signatures using 'long length' for stream bindings @@ -198,7 +207,7 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect { @Override public NationalizationSupport getNationalizationSupport() { // At least the jTDS driver doesn't support this - return jtdsDriver ? NationalizationSupport.IMPLICIT : super.getNationalizationSupport(); + return driverKind == SybaseDriverKind.JTDS ? NationalizationSupport.IMPLICIT : super.getNationalizationSupport(); } @Override @@ -360,6 +369,12 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect { @Override public CallableStatementSupport getCallableStatementSupport() { - return jtdsDriver ? JTDSCallableStatementSupport.INSTANCE : super.getCallableStatementSupport(); + return driverKind == SybaseDriverKind.JTDS ? JTDSCallableStatementSupport.INSTANCE : SybaseCallableStatementSupport.INSTANCE; + } + + @Override + public boolean supportsNamedParameters(DatabaseMetaData databaseMetaData) throws SQLException { + // Only the jTDS driver supports named parameters properly + return driverKind == SybaseDriverKind.JTDS && super.supportsNamedParameters( databaseMetaData ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java index 43c0d2dea9..7261f9bc9e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java @@ -127,7 +127,7 @@ public class SybaseASEDialect extends SybaseDialect { // According to Wikipedia bigdatetime and bigtime were added in 15.5 // But with jTDS we can't use them as the driver can't handle the types - if ( !jtdsDriver ) { + if ( getDriverKind() != SybaseDriverKind.JTDS ) { ddlTypeRegistry.addDescriptor( CapacityDependentDdlType.builder( DATE, "bigdatetime", "bigdatetime", this ) .withTypeCapacity( 3, "datetime" ) @@ -234,7 +234,7 @@ public class SybaseASEDialect extends SybaseDialect { .getJdbcTypeRegistry(); jdbcTypeRegistry.addDescriptor( Types.BOOLEAN, TinyIntJdbcType.INSTANCE ); // At least the jTDS driver does not support this type code - if ( jtdsDriver ) { + if ( getDriverKind() == SybaseDriverKind.JTDS ) { jdbcTypeRegistry.addDescriptor( Types.TIMESTAMP_WITH_TIMEZONE, TimestampJdbcType.INSTANCE ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java index 70a6a079ed..c45442c5b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java @@ -29,6 +29,7 @@ import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.procedure.internal.JTDSCallableStatementSupport; +import org.hibernate.procedure.internal.SybaseCallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.spi.QueryParameterBindings; @@ -69,8 +70,6 @@ import jakarta.persistence.TemporalType; */ public class SybaseDialect extends AbstractTransactSQLDialect { - protected final boolean jtdsDriver; - private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 16, 0 ); //All Sybase dialects share an IN list size limit. @@ -79,6 +78,10 @@ public class SybaseDialect extends AbstractTransactSQLDialect { private static final int PARAM_COUNT_LIMIT = 2000; private final UniqueDelegate uniqueDelegate = new SkipNullableUniqueDelegate(this); + private final SybaseDriverKind driverKind; + + @Deprecated(forRemoval = true) + protected final boolean jtdsDriver; public SybaseDialect() { this( MINIMUM_VERSION ); @@ -86,13 +89,14 @@ public class SybaseDialect extends AbstractTransactSQLDialect { public SybaseDialect(DatabaseVersion version) { super(version); - jtdsDriver = true; + this.driverKind = SybaseDriverKind.OTHER; + this.jtdsDriver = true; } public SybaseDialect(DialectResolutionInfo info) { super(info); - jtdsDriver = info.getDriverName() != null - && info.getDriverName().contains( "jTDS" ); + this.driverKind = SybaseDriverKind.determineKind( info ); + this.jtdsDriver = driverKind == SybaseDriverKind.JTDS; } @Override @@ -100,6 +104,10 @@ public class SybaseDialect extends AbstractTransactSQLDialect { return MINIMUM_VERSION; } + public SybaseDriverKind getDriverKind() { + return driverKind; + } + @Override public JdbcType resolveSqlTypeDescriptor( String columnTypeName, @@ -179,7 +187,7 @@ public class SybaseDialect extends AbstractTransactSQLDialect { super.contributeTypes(typeContributions, serviceRegistry); final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() .getJdbcTypeRegistry(); - if ( jtdsDriver ) { + if ( driverKind == SybaseDriverKind.JTDS ) { jdbcTypeRegistry.addDescriptor( Types.TINYINT, TinyIntAsSmallIntJdbcType.INSTANCE ); // The jTDS driver doesn't support the JDBC4 signatures using 'long length' for stream bindings @@ -217,7 +225,7 @@ public class SybaseDialect extends AbstractTransactSQLDialect { @Override public NationalizationSupport getNationalizationSupport() { // At least the jTDS driver doesn't support this - return jtdsDriver ? NationalizationSupport.IMPLICIT : super.getNationalizationSupport(); + return driverKind == SybaseDriverKind.JTDS ? NationalizationSupport.IMPLICIT : super.getNationalizationSupport(); } @Override @@ -372,7 +380,7 @@ public class SybaseDialect extends AbstractTransactSQLDialect { @Override public NameQualifierSupport getNameQualifierSupport() { - if ( jtdsDriver ) { + if ( driverKind == SybaseDriverKind.JTDS ) { return NameQualifierSupport.CATALOG; } else { @@ -387,7 +395,15 @@ public class SybaseDialect extends AbstractTransactSQLDialect { @Override public CallableStatementSupport getCallableStatementSupport() { - return jtdsDriver ? JTDSCallableStatementSupport.INSTANCE : super.getCallableStatementSupport(); + return driverKind == SybaseDriverKind.JTDS ? + JTDSCallableStatementSupport.INSTANCE : + SybaseCallableStatementSupport.INSTANCE; + } + + @Override + public boolean supportsNamedParameters(DatabaseMetaData databaseMetaData) throws SQLException { + // Only the jTDS driver supports named parameters properly + return driverKind == SybaseDriverKind.JTDS && super.supportsNamedParameters( databaseMetaData ); } @Override @@ -402,7 +418,7 @@ public class SybaseDialect extends AbstractTransactSQLDialect { @Override public IdentityColumnSupport getIdentityColumnSupport() { - return jtdsDriver + return driverKind == SybaseDriverKind.JTDS ? AbstractTransactSQLIdentityColumnSupport.INSTANCE : SybaseJconnIdentityColumnSupport.INSTANCE; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDriverKind.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDriverKind.java new file mode 100644 index 0000000000..fcd762b516 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDriverKind.java @@ -0,0 +1,35 @@ +/* + * 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; + +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; + +/** + * + * @author Christian Beikov + */ +public enum SybaseDriverKind { + JCONNECT, + JTDS, + OTHER; + + public static SybaseDriverKind determineKind(DialectResolutionInfo dialectResolutionInfo) { + final String driverName = dialectResolutionInfo.getDriverName(); + // By default, we assume OTHER + if ( driverName == null ) { + return OTHER; + } + switch ( driverName ) { + case "jConnect (TM) for JDBC (TM)": + return JCONNECT; + case "jTDS Type 4 JDBC Driver for MS SQL Server and Sybase": + return JTDS; + default: + return OTHER; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/JTDSCallableStatementSupport.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/JTDSCallableStatementSupport.java index 61447f64b1..a54477f1ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/JTDSCallableStatementSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/JTDSCallableStatementSupport.java @@ -9,7 +9,6 @@ package org.hibernate.procedure.internal; import java.util.List; import org.hibernate.QueryException; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.procedure.spi.FunctionReturnImplementor; import org.hibernate.procedure.spi.ProcedureCallImplementor; import org.hibernate.procedure.spi.ProcedureParameterImplementor; @@ -26,6 +25,7 @@ import jakarta.persistence.ParameterMode; * and instead requires that we render this as `@param=?`. */ public class JTDSCallableStatementSupport extends AbstractStandardCallableStatementSupport { + public static final JTDSCallableStatementSupport INSTANCE = new JTDSCallableStatementSupport(); @Override @@ -33,7 +33,6 @@ public class JTDSCallableStatementSupport extends AbstractStandardCallableStatem final String procedureName = procedureCall.getProcedureName(); final FunctionReturnImplementor functionReturn = procedureCall.getFunctionReturn(); final ProcedureParameterMetadataImplementor parameterMetadata = procedureCall.getParameterMetadata(); - final SharedSessionContractImplementor session = procedureCall.getSession(); final List> registrations = parameterMetadata.getRegistrationsAsList(); final int paramStringSizeEstimate; if ( functionReturn == null && parameterMetadata.hasNamedParameters() ) { @@ -48,14 +47,10 @@ public class JTDSCallableStatementSupport extends AbstractStandardCallableStatem final StringBuilder buffer; final int offset; if ( functionReturn != null ) { - offset = 2; - buffer = new StringBuilder( 11 + procedureName.length() + paramStringSizeEstimate ).append( "{?=call " ); - builder.setFunctionReturn( functionReturn.toJdbcFunctionReturn( session ) ); - } - else { - offset = 1; - buffer = new StringBuilder( 9 + procedureName.length() + paramStringSizeEstimate ).append( "{call " ); + throw new QueryException( "The jTDS driver does not support calling functions through the JDBC CallableStatement API" ); } + offset = 1; + buffer = new StringBuilder( 9 + procedureName.length() + paramStringSizeEstimate ).append( "{call " ); buffer.append( procedureName ); @@ -67,7 +62,7 @@ public class JTDSCallableStatementSupport extends AbstractStandardCallableStatem for ( int i = 0; i < registrations.size(); i++ ) { final ProcedureParameterImplementor parameter = registrations.get( i ); if ( parameter.getMode() == ParameterMode.REF_CURSOR ) { - throw new QueryException( "Dialect [" + session.getJdbcServices().getJdbcEnvironment().getDialect().getClass().getName() + "] not known to support REF_CURSOR parameters" ); + throw new QueryException( "Dialect [" + procedureCall.getSession().getJdbcServices().getJdbcEnvironment().getDialect().getClass().getName() + "] not known to support REF_CURSOR parameters" ); } buffer.append( sep ); final JdbcCallParameterRegistration registration = parameter.toJdbcParameterRegistration( diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/SybaseCallableStatementSupport.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/SybaseCallableStatementSupport.java new file mode 100644 index 0000000000..1721cb3e77 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/SybaseCallableStatementSupport.java @@ -0,0 +1,102 @@ +/* + * 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.procedure.internal; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.procedure.spi.FunctionReturnImplementor; +import org.hibernate.procedure.spi.ProcedureCallImplementor; +import org.hibernate.procedure.spi.ProcedureParameterImplementor; +import org.hibernate.query.spi.ProcedureParameterMetadataImplementor; +import org.hibernate.sql.exec.internal.JdbcCallImpl; +import org.hibernate.sql.exec.spi.JdbcCallParameterRegistration; +import org.hibernate.sql.exec.spi.JdbcOperationQueryCall; + +import jakarta.persistence.ParameterMode; + +/** + * Sybase implementation of CallableStatementSupport. + * + * The JDBC driver of Sybase doesn't support function invocations, so we have to render a select statement instead. + */ +public class SybaseCallableStatementSupport extends AbstractStandardCallableStatementSupport { + /** + * Singleton access + */ + public static final SybaseCallableStatementSupport INSTANCE = new SybaseCallableStatementSupport(); + private static final String FUNCTION_SYNTAX_START = "select "; + private static final String FUNCTION_SYNTAX_END = ") from (select 1) t1(c1)"; + private static final String CALL_SYNTAX_START = "{call "; + private static final String CALL_SYNTAX_END = ")}"; + + @Override + public JdbcOperationQueryCall interpretCall(ProcedureCallImplementor procedureCall) { + final String procedureName = procedureCall.getProcedureName(); + final FunctionReturnImplementor functionReturn = procedureCall.getFunctionReturn(); + final ProcedureParameterMetadataImplementor parameterMetadata = procedureCall.getParameterMetadata(); + final List> registrations = parameterMetadata.getRegistrationsAsList(); + final int paramStringSizeEstimate; + if ( functionReturn == null && parameterMetadata.hasNamedParameters() ) { + // That's just a rough estimate. I guess most params will have fewer than 8 chars on average + paramStringSizeEstimate = registrations.size() * 12; + } + else { + // For every param rendered as '?' we have a comma, hence the estimate + paramStringSizeEstimate = registrations.size() * 2; + } + final JdbcCallImpl.Builder builder = new JdbcCallImpl.Builder(); + final StringBuilder buffer; + final int offset; + if ( functionReturn != null ) { + offset = 1; + buffer = new StringBuilder( FUNCTION_SYNTAX_START.length() + FUNCTION_SYNTAX_END.length() + procedureName.length() + paramStringSizeEstimate ) + .append( FUNCTION_SYNTAX_START ); + } + else { + offset = 1; + buffer = new StringBuilder( CALL_SYNTAX_START.length() + CALL_SYNTAX_END.length() + procedureName.length() + paramStringSizeEstimate ) + .append( CALL_SYNTAX_START ); + } + + buffer.append( procedureName ); + + if ( registrations.isEmpty() ) { + buffer.append( '(' ); + } + else { + char sep = '('; + for ( int i = 0; i < registrations.size(); i++ ) { + final ProcedureParameterImplementor parameter = registrations.get( i ); + if ( parameter.getMode() == ParameterMode.REF_CURSOR ) { + throw new QueryException( "Dialect [" + procedureCall.getSession().getJdbcServices().getJdbcEnvironment().getDialect().getClass().getName() + "] not known to support REF_CURSOR parameters" ); + } + buffer.append( sep ); + final JdbcCallParameterRegistration registration = parameter.toJdbcParameterRegistration( + i + offset, + procedureCall + ); + if ( registration.getName() != null ) { + throw new QueryException( "The JDBC driver does not support named parameters" ); + } + buffer.append( "?" ); + sep = ','; + builder.addParameterRegistration( registration ); + } + } + + if ( functionReturn != null ) { + buffer.append( FUNCTION_SYNTAX_END ); + } + else { + buffer.append( CALL_SYNTAX_END ); + } + + builder.setCallableName( buffer.toString() ); + return builder.buildJdbcCall(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/SybaseStoredProcedureTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/SybaseStoredProcedureTest.java new file mode 100644 index 0000000000..5134c01d2d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/SybaseStoredProcedureTest.java @@ -0,0 +1,131 @@ +/* + * 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.orm.test.procedure; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import org.hibernate.dialect.SybaseASEDialect; +import org.hibernate.jpa.HibernateHints; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.junit.Assert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.ParameterMode; +import jakarta.persistence.StoredProcedureQuery; + +import static org.hibernate.testing.transaction.TransactionUtil.doInAutoCommit; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@RequiresDialect(value = SybaseASEDialect.class) +@Jpa( + annotatedClasses = { + Person.class, + Phone.class, + } +) +public class SybaseStoredProcedureTest { + + @BeforeEach + public void init(EntityManagerFactoryScope scope) { + // Force creation of the tables because Sybase checks that dependent tables exist + scope.getEntityManagerFactory(); + doInAutoCommit( + "DROP PROCEDURE sp_count_phones", + "DROP FUNCTION fn_count_phones", + "CREATE PROCEDURE sp_count_phones " + + " @personId INT, " + + " @phoneCount INT OUTPUT " + + "AS " + + "BEGIN " + + " SELECT @phoneCount = COUNT(*) " + + " FROM Phone " + + " WHERE person_id = @personId " + + "END", + "CREATE FUNCTION fn_count_phones (@personId INT) " + + "RETURNS INT " + + "AS " + + "BEGIN " + + " DECLARE @phoneCount int " + + " SELECT @phoneCount = COUNT(*) " + + " FROM Phone " + + " WHERE person_id = @personId " + + " RETURN @phoneCount " + + "END", + "sp_procxmode sp_count_phones, 'chained'" + ); + + scope.inTransaction( entityManager -> { + Person person1 = new Person( 1L, "John Doe" ); + person1.setNickName( "JD" ); + person1.setAddress( "Earth" ); + person1.setCreatedOn( Timestamp.from( LocalDateTime.of( 2000, 1, 1, 0, 0, 0 ) + .toInstant( ZoneOffset.UTC ) ) ); + + entityManager.persist( person1 ); + + Phone phone1 = new Phone( "123-456-7890" ); + phone1.setId( 1L ); + + person1.addPhone( phone1 ); + + Phone phone2 = new Phone( "098_765-4321" ); + phone2.setId( 2L ); + + person1.addPhone( phone2 ); + } ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope){ + scope.releaseEntityManagerFactory(); + } + + @Test + public void testStoredProcedureOutParameter(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + StoredProcedureQuery query = entityManager.createStoredProcedureQuery( "sp_count_phones" ); + query.registerStoredProcedureParameter( "personId", Long.class, ParameterMode.IN ); + query.registerStoredProcedureParameter( "phoneCount", Long.class, ParameterMode.OUT ); + + query.setParameter( "personId", 1L ); + + query.execute(); + Long phoneCount = (Long) query.getOutputParameterValue( "phoneCount" ); + assertEquals( Long.valueOf( 2 ), phoneCount ); + } ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.IsJtds.class, reverse = true, comment = "jTDS can't handle calling functions") + public void testFunctionReturnValue(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + StoredProcedureQuery query = entityManager.createStoredProcedureQuery( "hibernate_orm_test.fn_count_phones", Long.class ); + query.registerStoredProcedureParameter( "personId", Long.class, ParameterMode.IN ); + query.setHint( HibernateHints.HINT_CALLABLE_FUNCTION, "true" ); + + query.setParameter( "personId", 1L ); + + query.execute(); + Long phoneCount = (Long) query.getSingleResult(); + Assert.assertEquals( Long.valueOf( 2 ), phoneCount ); + }); + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 6313dbb321..e041c53962 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -26,6 +26,7 @@ import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SpannerDialect; import org.hibernate.dialect.SybaseDialect; +import org.hibernate.dialect.SybaseDriverKind; import org.hibernate.dialect.TimeZoneSupport; import org.hibernate.dialect.TiDBDialect; import org.hibernate.query.sqm.FetchClauseType; @@ -651,4 +652,10 @@ abstract public class DialectFeatureChecks { } } } + + public static class IsJtds implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; + } + } }