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 new file mode 100644 index 0000000000..83cc0d425d --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java @@ -0,0 +1,724 @@ +/* + * 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.community.dialect; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Map; + +import org.hibernate.LockOptions; +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.pagination.LimitHandler; +import org.hibernate.dialect.pagination.TopLimitHandler; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.LockTimeoutException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; +import org.hibernate.internal.util.JdbcExceptionHelper; +import org.hibernate.query.sqm.IntervalType; +import org.hibernate.query.sqm.TemporalUnit; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.ForUpdateFragment; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.TimestampJdbcType; +import org.hibernate.type.descriptor.jdbc.TinyIntJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; +import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; + +import jakarta.persistence.TemporalType; + +import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; +import static org.hibernate.type.SqlTypes.BIGINT; +import static org.hibernate.type.SqlTypes.BOOLEAN; +import static org.hibernate.type.SqlTypes.DATE; +import static org.hibernate.type.SqlTypes.TIME; +import static org.hibernate.type.SqlTypes.TIMESTAMP; +import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; + +/** + * A {@linkplain Dialect SQL dialect} for Sybase Adaptive Server Enterprise 11.9 and above. + */ +public class SybaseASELegacyDialect extends SybaseLegacyDialect { + + private final SizeStrategy sizeStrategy = new SizeStrategyImpl() { + @Override + public Size resolveSize( + JdbcType jdbcType, + JavaType javaType, + Integer precision, + Integer scale, + Long length) { + switch ( jdbcType.getDefaultSqlTypeCode() ) { + case Types.FLOAT: + // Sybase ASE allows FLOAT with a precision up to 48 + if ( precision != null ) { + return Size.precision( Math.min( Math.max( precision, 1 ), 48 ) ); + } + } + return super.resolveSize( jdbcType, javaType, precision, scale, length ); + } + }; + + private final boolean ansiNull; + + public SybaseASELegacyDialect() { + this( DatabaseVersion.make( 11 ) ); + } + + public SybaseASELegacyDialect(DatabaseVersion version) { + super(version); + ansiNull = false; + } + + public SybaseASELegacyDialect(DialectResolutionInfo info) { + super(info); + ansiNull = isAnsiNull( info.getDatabaseMetadata() ); + } + + @Override + protected String columnType(int sqlTypeCode) { + switch ( sqlTypeCode ) { + case BOOLEAN: + // On Sybase ASE, the 'bit' type cannot be null, + // and cannot have indexes (while we don't use + // tinyint to store signed bytes, we can use it + // to store boolean values) + return "tinyint"; + case BIGINT: + // Sybase ASE didn't introduce 'bigint' until version 15.0 + return getVersion().isBefore( 15 ) ? "numeric(19,0)" : super.columnType( sqlTypeCode ); + case DATE: + return getVersion().isSameOrAfter( 12 ) ? "date" : super.columnType( sqlTypeCode ); + case TIME: + return getVersion().isSameOrAfter( 12 ) ? "time" : super.columnType( sqlTypeCode ); + } + return super.columnType( sqlTypeCode ); + } + + @Override + protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.registerColumnTypes( typeContributions, serviceRegistry ); + final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + + // 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 ) { + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( DATE, "bigdatetime", "bigdatetime", this ) + .withTypeCapacity( 3, "datetime" ) + .build() + ); + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( TIME, "bigdatetime", "bigdatetime", this ) + .withTypeCapacity( 3, "datetime" ) + .build() + ); + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( TIMESTAMP, "bigdatetime", "bigdatetime", this ) + .withTypeCapacity( 3, "datetime" ) + .build() + ); + ddlTypeRegistry.addDescriptor( + CapacityDependentDdlType.builder( TIMESTAMP_WITH_TIMEZONE, "bigdatetime", "bigdatetime", this ) + .withTypeCapacity( 3, "datetime" ) + .build() + ); + } + } + + @Override + public int getMaxVarcharLength() { + // the maximum length of a VARCHAR or VARBINARY + // column depends on the page size and ASE version + // and is actually a limit on the whole row length, + // not the individual column length -- anyway, the + // largest possible page size is 16k, so that's a + // hard upper limit + return 16_384; + } + + private static boolean isAnsiNull(DatabaseMetaData databaseMetaData) { + if ( databaseMetaData != null ) { + try (java.sql.Statement s = databaseMetaData.getConnection().createStatement() ) { + final ResultSet rs = s.executeQuery( "SELECT @@options" ); + if ( rs.next() ) { + final byte[] optionBytes = rs.getBytes( 1 ); + // By trial and error, enabling and disabling ansinull revealed that this bit is the indicator + return ( optionBytes[4] & 2 ) == 2; + } + } + catch (SQLException ex) { + // Ignore + } + } + return false; + } + + @Override + public boolean isAnsiNullOn() { + return ansiNull; + } + + @Override + public int getFloatPrecision() { + return 15; + } + + @Override + public int getDoublePrecision() { + return 48; + } + + @Override + public SizeStrategy getSizeStrategy() { + return sizeStrategy; + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + return new SybaseASELegacySqlAstTranslator<>( sessionFactory, statement ); + } + }; + } + + /** + * The Sybase ASE {@code BIT} type does not allow + * null values, so we don't use it. + * + * @return false + */ + @Override + public boolean supportsBitType() { + return false; + } + + @Override + public boolean supportsDistinctFromPredicate() { + return getVersion().isSameOrAfter( 16, 3 ); + } + + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes( typeContributions, serviceRegistry ); + + final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() + .getJdbcTypeRegistry(); + jdbcTypeRegistry.addDescriptor( Types.BOOLEAN, TinyIntJdbcType.INSTANCE ); + // At least the jTDS driver does not support this type code + if ( jtdsDriver ) { + jdbcTypeRegistry.addDescriptor( Types.TIMESTAMP_WITH_TIMEZONE, TimestampJdbcType.INSTANCE ); + } + } + + @Override + public int resolveSqlTypeLength( + String columnTypeName, + int jdbcTypeCode, + int precision, + int scale, + int displaySize) { + // Sybase ASE reports the "actual" precision in the display size + switch ( jdbcTypeCode ) { + case Types.REAL: + case Types.DOUBLE: + return displaySize; + } + return super.resolveSqlTypeLength( columnTypeName, jdbcTypeCode, precision, scale, displaySize ); + } + + @Override + public String currentDate() { + return "current_date()"; + } + + @Override + public String currentTime() { + return "current_time()"; + } + + @Override + public String currentTimestamp() { + return "current_bigdatetime()"; + } + + @Override + public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { + //TODO!! + switch ( unit ) { + case NANOSECOND: + case NATIVE: + // If the driver or database do not support bigdatetime and bigtime types, + // we try to operate on milliseconds instead + if ( getVersion().isBefore( 15, 5 ) || jtdsDriver ) { + return "dateadd(millisecond,?2/1000000,?3)"; + } + else { + return "dateadd(mcs,?2/1000,?3)"; + } + default: + return "dateadd(?1,?2,?3)"; + } + } + + @Override + public long getFractionalSecondPrecisionInNanos() { + // If the database does not support bigdatetime and bigtime types, + // we try to operate on milliseconds instead + if ( getVersion().isBefore( 15, 5 ) ) { + return 1_000_000; + } + else { + return 1_000; + } + } + + @Override + public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { + //TODO!! + switch ( unit ) { + case NANOSECOND: + case NATIVE: + // If the database does not support bigdatetime and bigtime types, + // we try to operate on milliseconds instead + if ( getVersion().isBefore( 15, 5 ) ) { + return "cast(datediff(ms,?2,?3) as numeric(21))"; + } + else { + return "cast(datediff(mcs,cast(?2 as bigdatetime),cast(?3 as bigdatetime)) as numeric(21))"; + } + default: + return "datediff(?1,?2,?3)"; + } + } + + @Override + protected void registerDefaultKeywords() { + super.registerDefaultKeywords(); + registerKeyword( "add" ); + registerKeyword( "all" ); + registerKeyword( "alter" ); + registerKeyword( "and" ); + registerKeyword( "any" ); + registerKeyword( "arith_overflow" ); + registerKeyword( "as" ); + registerKeyword( "asc" ); + registerKeyword( "at" ); + registerKeyword( "authorization" ); + registerKeyword( "avg" ); + registerKeyword( "begin" ); + registerKeyword( "between" ); + registerKeyword( "break" ); + registerKeyword( "browse" ); + registerKeyword( "bulk" ); + registerKeyword( "by" ); + registerKeyword( "cascade" ); + registerKeyword( "case" ); + registerKeyword( "char_convert" ); + registerKeyword( "check" ); + registerKeyword( "checkpoint" ); + registerKeyword( "close" ); + registerKeyword( "clustered" ); + registerKeyword( "coalesce" ); + registerKeyword( "commit" ); + registerKeyword( "compute" ); + registerKeyword( "confirm" ); + registerKeyword( "connect" ); + registerKeyword( "constraint" ); + registerKeyword( "continue" ); + registerKeyword( "controlrow" ); + registerKeyword( "convert" ); + registerKeyword( "count" ); + registerKeyword( "count_big" ); + registerKeyword( "create" ); + registerKeyword( "current" ); + registerKeyword( "cursor" ); + registerKeyword( "database" ); + registerKeyword( "dbcc" ); + registerKeyword( "deallocate" ); + registerKeyword( "declare" ); + registerKeyword( "decrypt" ); + registerKeyword( "default" ); + registerKeyword( "delete" ); + registerKeyword( "desc" ); + registerKeyword( "determnistic" ); + registerKeyword( "disk" ); + registerKeyword( "distinct" ); + registerKeyword( "drop" ); + registerKeyword( "dummy" ); + registerKeyword( "dump" ); + registerKeyword( "else" ); + registerKeyword( "encrypt" ); + registerKeyword( "end" ); + registerKeyword( "endtran" ); + registerKeyword( "errlvl" ); + registerKeyword( "errordata" ); + registerKeyword( "errorexit" ); + registerKeyword( "escape" ); + registerKeyword( "except" ); + registerKeyword( "exclusive" ); + registerKeyword( "exec" ); + registerKeyword( "execute" ); + registerKeyword( "exist" ); + registerKeyword( "exit" ); + registerKeyword( "exp_row_size" ); + registerKeyword( "external" ); + registerKeyword( "fetch" ); + registerKeyword( "fillfactor" ); + registerKeyword( "for" ); + registerKeyword( "foreign" ); + registerKeyword( "from" ); + registerKeyword( "goto" ); + registerKeyword( "grant" ); + registerKeyword( "group" ); + registerKeyword( "having" ); + registerKeyword( "holdlock" ); + registerKeyword( "identity" ); + registerKeyword( "identity_gap" ); + registerKeyword( "identity_start" ); + registerKeyword( "if" ); + registerKeyword( "in" ); + registerKeyword( "index" ); + registerKeyword( "inout" ); + registerKeyword( "insensitive" ); + registerKeyword( "insert" ); + registerKeyword( "install" ); + registerKeyword( "intersect" ); + registerKeyword( "into" ); + registerKeyword( "is" ); + registerKeyword( "isolation" ); + registerKeyword( "jar" ); + registerKeyword( "join" ); + registerKeyword( "key" ); + registerKeyword( "kill" ); + registerKeyword( "level" ); + registerKeyword( "like" ); + registerKeyword( "lineno" ); + registerKeyword( "load" ); + registerKeyword( "lock" ); + registerKeyword( "materialized" ); + registerKeyword( "max" ); + registerKeyword( "max_rows_per_page" ); + registerKeyword( "min" ); + registerKeyword( "mirror" ); + registerKeyword( "mirrorexit" ); + registerKeyword( "modify" ); + registerKeyword( "national" ); + registerKeyword( "new" ); + registerKeyword( "noholdlock" ); + registerKeyword( "nonclustered" ); + registerKeyword( "nonscrollable" ); + registerKeyword( "non_sensitive" ); + registerKeyword( "not" ); + registerKeyword( "null" ); + registerKeyword( "nullif" ); + registerKeyword( "numeric_truncation" ); + registerKeyword( "of" ); + registerKeyword( "off" ); + registerKeyword( "offsets" ); + registerKeyword( "on" ); + registerKeyword( "once" ); + registerKeyword( "online" ); + registerKeyword( "only" ); + registerKeyword( "open" ); + registerKeyword( "option" ); + registerKeyword( "or" ); + registerKeyword( "order" ); + registerKeyword( "out" ); + registerKeyword( "output" ); + registerKeyword( "over" ); + registerKeyword( "artition" ); + registerKeyword( "perm" ); + registerKeyword( "permanent" ); + registerKeyword( "plan" ); + registerKeyword( "prepare" ); + registerKeyword( "primary" ); + registerKeyword( "print" ); + registerKeyword( "privileges" ); + registerKeyword( "proc" ); + registerKeyword( "procedure" ); + registerKeyword( "processexit" ); + registerKeyword( "proxy_table" ); + registerKeyword( "public" ); + registerKeyword( "quiesce" ); + registerKeyword( "raiserror" ); + registerKeyword( "read" ); + registerKeyword( "readpast" ); + registerKeyword( "readtext" ); + registerKeyword( "reconfigure" ); + registerKeyword( "references" ); + registerKeyword( "remove" ); + registerKeyword( "reorg" ); + registerKeyword( "replace" ); + registerKeyword( "replication" ); + registerKeyword( "reservepagegap" ); + registerKeyword( "return" ); + registerKeyword( "returns" ); + registerKeyword( "revoke" ); + registerKeyword( "role" ); + registerKeyword( "rollback" ); + registerKeyword( "rowcount" ); + registerKeyword( "rows" ); + registerKeyword( "rule" ); + registerKeyword( "save" ); + registerKeyword( "schema" ); + registerKeyword( "scroll" ); + registerKeyword( "scrollable" ); + registerKeyword( "select" ); + registerKeyword( "semi_sensitive" ); + registerKeyword( "set" ); + registerKeyword( "setuser" ); + registerKeyword( "shared" ); + registerKeyword( "shutdown" ); + registerKeyword( "some" ); + registerKeyword( "statistics" ); + registerKeyword( "stringsize" ); + registerKeyword( "stripe" ); + registerKeyword( "sum" ); + registerKeyword( "syb_identity" ); + registerKeyword( "syb_restree" ); + registerKeyword( "syb_terminate" ); + registerKeyword( "top" ); + registerKeyword( "table" ); + registerKeyword( "temp" ); + registerKeyword( "temporary" ); + registerKeyword( "textsize" ); + registerKeyword( "to" ); + registerKeyword( "tracefile" ); + registerKeyword( "tran" ); + registerKeyword( "transaction" ); + registerKeyword( "trigger" ); + registerKeyword( "truncate" ); + registerKeyword( "tsequal" ); + registerKeyword( "union" ); + registerKeyword( "unique" ); + registerKeyword( "unpartition" ); + registerKeyword( "update" ); + registerKeyword( "use" ); + registerKeyword( "user" ); + registerKeyword( "user_option" ); + registerKeyword( "using" ); + registerKeyword( "values" ); + registerKeyword( "varying" ); + registerKeyword( "view" ); + registerKeyword( "waitfor" ); + registerKeyword( "when" ); + registerKeyword( "where" ); + registerKeyword( "while" ); + registerKeyword( "with" ); + registerKeyword( "work" ); + registerKeyword( "writetext" ); + registerKeyword( "xmlextract" ); + registerKeyword( "xmlparse" ); + registerKeyword( "xmltest" ); + registerKeyword( "xmlvalidate" ); + } + +// Overridden informational metadata ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + @Override + public boolean supportsCascadeDelete() { + return false; + } + + @Override + public int getMaxAliasLength() { + return 30; + } + + @Override + public int getMaxIdentifierLength() { + return 255; + } + + @Override + public boolean supportsValuesListForInsert() { + return false; + } + + @Override + public boolean supportsLockTimeouts() { + return false; + } + + @Override + public boolean supportsOrderByInSubquery() { + return false; + } + + @Override + public boolean supportsUnionInSubquery() { + // At least not according to HHH-3637 + return false; + } + + @Override + public boolean supportsPartitionBy() { + return false; + } + + @Override + public String getTableTypeString() { + //HHH-7298 I don't know if this would break something or cause some side affects + //but it is required to use 'select for update' + return getVersion().isBefore( 15, 7 ) ? super.getTableTypeString() : " lock datarows"; + } + + @Override + public boolean supportsExpectedLobUsagePattern() { + // Earlier Sybase did not support LOB locators at all + return getVersion().isSameOrAfter( 15, 7 ); + } + + @Override + public boolean supportsLobValueChangePropagation() { + return false; + } + + @Override + public RowLockStrategy getWriteRowLockStrategy() { + return getVersion().isSameOrAfter( 15, 7 ) ? RowLockStrategy.COLUMN : RowLockStrategy.TABLE; + } + + @Override + public String getForUpdateString() { + return getVersion().isBefore( 15, 7 ) ? "" : " for update"; + } + + @Override + public String getForUpdateString(String aliases) { + return getVersion().isBefore( 15, 7 ) + ? "" + : getForUpdateString() + " of " + aliases; + } + + @Override + public String appendLockHint(LockOptions mode, String tableName) { + //TODO: is this really necessary??! + return getVersion().isBefore( 15, 7 ) ? super.appendLockHint( mode, tableName ) : tableName; + } + + @Override + public String applyLocksToSql(String sql, LockOptions aliasedLockOptions, Map keyColumnNames) { + //TODO: is this really correct? + return getVersion().isBefore( 15, 7 ) + ? super.applyLocksToSql( sql, aliasedLockOptions, keyColumnNames ) + : sql + new ForUpdateFragment( this, aliasedLockOptions, keyColumnNames ).toFragmentString(); + } + + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return EXTRACTOR; + } + + /** + * Constraint-name extractor for Sybase ASE constraint violation exceptions. + * Orginally contributed by Denny Bartelt. + */ + private static final ViolatedConstraintNameExtractor EXTRACTOR = + new TemplatedViolatedConstraintNameExtractor( sqle -> { + final int errorCode = JdbcExceptionHelper.extractErrorCode( sqle ); + switch ( JdbcExceptionHelper.extractSqlState( sqle ) ) { + // UNIQUE VIOLATION + case "S1000": + if (2601 == errorCode) { + return extractUsingTemplate( "with unique index '", "'", sqle.getMessage() ); + } + break; + case "23000": + if (546 == errorCode) { + // Foreign key violation + return extractUsingTemplate( "constraint name = '", "'", sqle.getMessage() ); + } + break; +// // FOREIGN KEY VIOLATION +// case 23503: +// return extractUsingTemplate( "violates foreign key constraint \"","\"", sqle.getMessage() ); +// // NOT NULL VIOLATION +// case 23502: +// return extractUsingTemplate( "null value in column \"","\" violates not-null constraint", sqle.getMessage() ); +// // TODO: RESTRICT VIOLATION +// case 23001: +// return null; + // ALL OTHER + default: + return null; + } + return null; + } ); + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + if ( getVersion().isBefore( 15, 7 ) ) { + return null; + } + + return (sqlException, message, sql) -> { + final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException ); + final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); + switch ( sqlState ) { + case "JZ0TO": + case "JZ006": + throw new LockTimeoutException( message, sqlException, sql ); + case "S1000": + switch ( errorCode ) { + case 515: + // Attempt to insert NULL value into column; column does not allow nulls. + case 2601: + // Unique constraint violation + final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); + return new ConstraintViolationException( message, sqlException, sql, constraintName ); + } + break; + case "ZZZZZ": + if (515 == errorCode) { + // Attempt to insert NULL value into column; column does not allow nulls. + final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); + return new ConstraintViolationException( message, sqlException, sql, constraintName ); + } + break; + case "23000": + if (546 == errorCode) { + // Foreign key violation + final String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); + return new ConstraintViolationException( message, sqlException, sql, constraintName ); + } + break; + } + return null; + }; + } + + @Override + public LimitHandler getLimitHandler() { + if ( getVersion().isBefore( 12, 5 ) ) { + //support for SELECT TOP was introduced in Sybase ASE 12.5.3 + return super.getLimitHandler(); + } + return new TopLimitHandler(false); + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java new file mode 100644 index 0000000000..c2fe947702 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java @@ -0,0 +1,403 @@ +/* + * 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 java.util.List; +import java.util.function.Consumer; + +import org.hibernate.LockMode; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; +import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.UnionTableReference; +import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.QueryGroup; +import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectClause; +import org.hibernate.sql.exec.spi.JdbcOperation; + +/** + * A SQL AST translator for Sybase ASE. + * + * @author Christian Beikov + */ +public class SybaseASELegacySqlAstTranslator extends AbstractSqlAstTranslator { + + public SybaseASELegacySqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { + super( sessionFactory, statement ); + } + + // Sybase ASE does not allow CASE expressions where all result arms contain plain parameters. + // At least one result arm must provide some type context for inference, + // so we cast the first result arm if we encounter this condition + + @Override + protected void visitAnsiCaseSearchedExpression( + CaseSearchedExpression caseSearchedExpression, + Consumer resultRenderer) { + if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( caseSearchedExpression ) ) { + final List whenFragments = caseSearchedExpression.getWhenFragments(); + final Expression firstResult = whenFragments.get( 0 ).getResult(); + super.visitAnsiCaseSearchedExpression( + caseSearchedExpression, + e -> { + if ( e == firstResult ) { + renderCasted( e ); + } + else { + resultRenderer.accept( e ); + } + } + ); + } + else { + super.visitAnsiCaseSearchedExpression( caseSearchedExpression, resultRenderer ); + } + } + + @Override + protected void visitAnsiCaseSimpleExpression( + CaseSimpleExpression caseSimpleExpression, + Consumer resultRenderer) { + if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( caseSimpleExpression ) ) { + final List whenFragments = caseSimpleExpression.getWhenFragments(); + final Expression firstResult = whenFragments.get( 0 ).getResult(); + super.visitAnsiCaseSimpleExpression( + caseSimpleExpression, + e -> { + if ( e == firstResult ) { + renderCasted( e ); + } + else { + resultRenderer.accept( e ); + } + } + ); + } + else { + super.visitAnsiCaseSimpleExpression( caseSimpleExpression, resultRenderer ); + } + } + + @Override + protected boolean renderNamedTableReference(NamedTableReference tableReference, LockMode lockMode) { + super.renderNamedTableReference( tableReference, lockMode ); + if ( getDialect().getVersion().isBefore( 15, 7 ) ) { + if ( LockMode.READ.lessThan( lockMode ) ) { + appendSql( " holdlock" ); + } + return true; + } + return false; + } + + @Override + protected void renderTableGroupJoin(TableGroupJoin tableGroupJoin, List tableGroupJoinCollector) { + if ( tableGroupJoin.getJoinType() == SqlAstJoinType.CROSS ) { + appendSql( ", " ); + } + else { + appendSql( WHITESPACE ); + appendSql( tableGroupJoin.getJoinType().getText() ); + appendSql( "join " ); + } + + final Predicate predicate; + if ( tableGroupJoin.getPredicate() == null ) { + if ( tableGroupJoin.getJoinType() == SqlAstJoinType.CROSS ) { + predicate = null; + } + else { + predicate = new BooleanExpressionPredicate( new QueryLiteral<>( true, getBooleanType() ) ); + } + } + else { + predicate = tableGroupJoin.getPredicate(); + } + if ( predicate != null && !predicate.isEmpty() ) { + renderTableGroup( tableGroupJoin.getJoinedGroup(), predicate, tableGroupJoinCollector ); + } + else { + renderTableGroup( tableGroupJoin.getJoinedGroup(), null, tableGroupJoinCollector ); + } + } + + @Override + protected void renderForUpdateClause(QuerySpec querySpec, ForUpdateClause forUpdateClause) { + if ( getDialect().getVersion().isBefore( 15, 7 ) ) { + return; + } + super.renderForUpdateClause( querySpec, forUpdateClause ); + } + + @Override + protected void renderSearchClause(CteStatement cte) { + // Sybase ASE does not support this, but it's just a hint anyway + } + + @Override + protected void renderCycleClause(CteStatement cte) { + // Sybase ASE does not support this, but it can be emulated + } + + @Override + protected void visitSqlSelections(SelectClause selectClause) { + if ( supportsTopClause() ) { + renderTopClause( (QuerySpec) getQueryPartStack().getCurrent(), true, false ); + } + super.visitSqlSelections( selectClause ); + } + + @Override + protected void renderFetchPlusOffsetExpression( + Expression fetchClauseExpression, + Expression offsetClauseExpression, + int offset) { + renderFetchPlusOffsetExpressionAsLiteral( fetchClauseExpression, offsetClauseExpression, offset ); + } + + @Override + public void visitQueryGroup(QueryGroup queryGroup) { + if ( queryGroup.hasSortSpecifications() || queryGroup.hasOffsetOrFetchClause() ) { + appendSql( "select " ); + renderTopClause( + queryGroup.getOffsetClauseExpression(), + queryGroup.getFetchClauseExpression(), + queryGroup.getFetchClauseType(), + true, + false + ); + appendSql( "* from (" ); + renderQueryGroup( queryGroup, false ); + appendSql( ") grp_(c0" ); + // Sybase doesn't have implicit names for non-column select expressions, so we need to assign names + final int itemCount = queryGroup.getFirstQuerySpec().getSelectClause().getSqlSelections().size(); + for (int i = 1; i < itemCount; i++) { + appendSql( ",c" ); + appendSql( i ); + } + appendSql( ')' ); + visitOrderBy( queryGroup.getSortSpecifications() ); + } + else { + super.visitQueryGroup( queryGroup ); + } + } + + @Override + public void visitOffsetFetchClause(QueryPart queryPart) { + assertRowsOnlyFetchClauseType( queryPart ); + if ( !queryPart.isRoot() && queryPart.hasOffsetOrFetchClause() ) { + if ( queryPart.getFetchClauseExpression() != null && !supportsTopClause() || queryPart.getOffsetClauseExpression() != null ) { + throw new IllegalArgumentException( "Can't emulate offset fetch clause in subquery" ); + } + } + } + + @Override + protected void renderFetchExpression(Expression fetchExpression) { + if ( supportsParameterOffsetFetchExpression() ) { + super.renderFetchExpression( fetchExpression ); + } + else { + renderExpressionAsLiteral( fetchExpression, getJdbcParameterBindings() ); + } + } + + @Override + protected void renderOffsetExpression(Expression offsetExpression) { + if ( supportsParameterOffsetFetchExpression() ) { + super.renderOffsetExpression( offsetExpression ); + } + else { + renderExpressionAsLiteral( offsetExpression, getJdbcParameterBindings() ); + } + } + + @Override + protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + // I think intersect is only supported in 16.0 SP3 + if ( getDialect().isAnsiNullOn() ) { + if ( supportsDistinctFromPredicate() ) { + renderComparisonEmulateIntersect( lhs, operator, rhs ); + } + else { + renderComparisonEmulateCase( lhs, operator, rhs ); + } + } + else { + // The ansinull setting only matters if using a parameter or literal and the eq operator according to the docs + // http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc32300.1570/html/sqlug/sqlug89.htm + boolean rhsNotNullPredicate = + lhs instanceof Literal + || isParameter( lhs ); + boolean lhsNotNullPredicate = + rhs instanceof Literal + || isParameter( rhs ); + if ( rhsNotNullPredicate || lhsNotNullPredicate ) { + lhs.accept( this ); + switch ( operator ) { + case DISTINCT_FROM: + appendSql( "<>" ); + break; + case NOT_DISTINCT_FROM: + appendSql( '=' ); + break; + case LESS_THAN: + case GREATER_THAN: + case LESS_THAN_OR_EQUAL: + case GREATER_THAN_OR_EQUAL: + // These operators are not affected by ansinull=off + lhsNotNullPredicate = false; + rhsNotNullPredicate = false; + default: + appendSql( operator.sqlText() ); + break; + } + rhs.accept( this ); + if ( lhsNotNullPredicate ) { + appendSql( " and " ); + lhs.accept( this ); + appendSql( " is not null" ); + } + if ( rhsNotNullPredicate ) { + appendSql( " and " ); + rhs.accept( this ); + appendSql( " is not null" ); + } + } + else { + if ( supportsDistinctFromPredicate() ) { + renderComparisonEmulateIntersect( lhs, operator, rhs ); + } + else { + renderComparisonEmulateCase( lhs, operator, rhs ); + } + } + } + } + + @Override + protected boolean supportsIntersect() { + // At least the version that + return false; + } + + @Override + protected void renderSelectTupleComparison( + List lhsExpressions, + SqlTuple tuple, + ComparisonOperator operator) { + emulateSelectTupleComparison( lhsExpressions, tuple.getExpressions(), operator, true ); + } + + @Override + protected void renderPartitionItem(Expression expression) { + if ( expression instanceof Literal ) { + // Note that this depends on the SqmToSqlAstConverter to add a dummy table group + appendSql( "dummy_.x" ); + } + else if ( expression instanceof Summarization ) { + // This could theoretically be emulated by rendering all grouping variations of the query and + // connect them via union all but that's probably pretty inefficient and would have to happen + // on the query spec level + throw new UnsupportedOperationException( "Summarization is not supported by DBMS" ); + } + else { + expression.accept( this ); + } + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + final String dmlTargetTableAlias = getDmlTargetTableAlias(); + if ( dmlTargetTableAlias != null && dmlTargetTableAlias.equals( columnReference.getQualifier() ) ) { + // Sybase needs a table name prefix + // but not if this is a restricted union table reference subquery + final QuerySpec currentQuerySpec = (QuerySpec) getQueryPartStack().getCurrent(); + final List roots; + if ( currentQuerySpec != null && !currentQuerySpec.isRoot() + && (roots = currentQuerySpec.getFromClause().getRoots()).size() == 1 + && roots.get( 0 ).getPrimaryTableReference() instanceof UnionTableReference ) { + columnReference.appendReadExpression( this ); + } + // for now, use the unqualified form + else if ( columnReference.isColumnExpressionFormula() ) { + // For formulas, we have to replace the qualifier as the alias was already rendered into the formula + // This is fine for now as this is only temporary anyway until we render aliases for table references + appendSql( + columnReference.getColumnExpression() + .replaceAll( "(\\b)(" + dmlTargetTableAlias + "\\.)(\\b)", "$1$3" ) + ); + } + else { + appendSql( getCurrentDmlStatement().getTargetTable().getTableExpression() ); + appendSql( '.' ); + appendSql( columnReference.getColumnExpression() ); + } + } + else { + columnReference.appendReadExpression( this ); + } + } + + @Override + protected boolean needsRowsToSkip() { + return true; + } + + @Override + protected boolean needsMaxRows() { + return !supportsTopClause(); + } + + @Override + protected boolean supportsRowValueConstructorSyntax() { + return false; + } + + @Override + protected boolean supportsRowValueConstructorSyntaxInInList() { + return false; + } + + @Override + protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + return false; + } + + @Override + protected String getFromDual() { + return " from (select 1) dual(c1)"; + } + + private boolean supportsTopClause() { + return getDialect().getVersion().isSameOrAfter( 12, 5 ); + } + + private boolean supportsParameterOffsetFetchExpression() { + return false; + } +} 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 new file mode 100644 index 0000000000..844aaa7b20 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java @@ -0,0 +1,335 @@ +/* + * 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.community.dialect; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Types; + +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.function.CommonFunctionFactory; +import org.hibernate.dialect.function.CountFunction; +import org.hibernate.dialect.function.IntegralTimestampaddFunction; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; +import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.sqm.CastType; +import org.hibernate.query.sqm.IntervalType; +import org.hibernate.query.sqm.TemporalUnit; +import org.hibernate.query.sqm.TrimSpec; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.sql.SqmTranslator; +import org.hibernate.query.sqm.sql.SqmTranslatorFactory; +import org.hibernate.query.sqm.sql.StandardSqmTranslatorFactory; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.spi.SqlAstCreationContext; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.type.JavaObjectType; +import org.hibernate.type.descriptor.jdbc.BlobJdbcType; +import org.hibernate.type.descriptor.jdbc.ClobJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.ObjectNullAsNullTypeJdbcType; +import org.hibernate.type.descriptor.jdbc.SmallIntJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; + +import jakarta.persistence.TemporalType; + + +/** + * Superclass for all Sybase dialects. + * + * @author Brett Meyer + */ +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; + + public SybaseLegacyDialect() { + this( DatabaseVersion.make( 11, 0 ) ); + } + + public SybaseLegacyDialect(DatabaseVersion version) { + super(version); + jtdsDriver = true; + } + + public SybaseLegacyDialect(DialectResolutionInfo info) { + super(info); + jtdsDriver = info.getDriverName() != null + && info.getDriverName().contains( "jTDS" ); + } + + @Override + public JdbcType resolveSqlTypeDescriptor( + String columnTypeName, + int jdbcTypeCode, + int precision, + int scale, + JdbcTypeRegistry jdbcTypeRegistry) { + switch ( jdbcTypeCode ) { + case Types.NUMERIC: + case Types.DECIMAL: + if ( precision == 19 && scale == 0 ) { + return jdbcTypeRegistry.getDescriptor( Types.BIGINT ); + } + case Types.TINYINT: + if ( jtdsDriver ) { + return jdbcTypeRegistry.getDescriptor( Types.SMALLINT ); + } + } + return super.resolveSqlTypeDescriptor( + columnTypeName, + jdbcTypeCode, + precision, + scale, + jdbcTypeRegistry + ); + } + + @Override + public SqmTranslatorFactory getSqmTranslatorFactory() { + return new StandardSqmTranslatorFactory() { + @Override + public SqmTranslator createSelectTranslator( + SqmSelectStatement sqmSelectStatement, + QueryOptions queryOptions, + DomainParameterXref domainParameterXref, + QueryParameterBindings domainParameterBindings, + LoadQueryInfluencers loadQueryInfluencers, + SqlAstCreationContext creationContext, + boolean deduplicateSelectionItems) { + return new SybaseLegacySqmToSqlAstConverter<>( + sqmSelectStatement, + queryOptions, + domainParameterXref, + domainParameterBindings, + loadQueryInfluencers, + creationContext, + deduplicateSelectionItems + ); + } + }; + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + return new SybaseLegacySqlAstTranslator<>( sessionFactory, statement ); + } + }; + } + + @Override + public boolean supportsNullPrecedence() { + return false; + } + + @Override + public int getInExpressionCountLimit() { + return PARAM_LIST_SIZE_LIMIT; + } + + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes(typeContributions, serviceRegistry); + final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration() + .getJdbcTypeRegistry(); + if ( jtdsDriver ) { + jdbcTypeRegistry.addDescriptor( Types.TINYINT, SmallIntJdbcType.INSTANCE ); + + // The jTDS driver doesn't support the JDBC4 signatures using 'long length' for stream bindings + jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.CLOB_BINDING ); + + // The jTDS driver doesn't support nationalized types + jdbcTypeRegistry.addDescriptor( Types.NCLOB, ClobJdbcType.CLOB_BINDING ); + jdbcTypeRegistry.addDescriptor( Types.NVARCHAR, ClobJdbcType.CLOB_BINDING ); + } + else { + // Some Sybase drivers cannot support getClob. See HHH-7889 + jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.STREAM_BINDING_EXTRACTING ); + } + + jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.PRIMITIVE_ARRAY_BINDING ); + + // Sybase requires a custom binder for binding untyped nulls with the NULL type + typeContributions.contributeJdbcType( ObjectNullAsNullTypeJdbcType.INSTANCE ); + + // Until we remove StandardBasicTypes, we have to keep this + typeContributions.contributeType( + new JavaObjectType( + ObjectNullAsNullTypeJdbcType.INSTANCE, + typeContributions.getTypeConfiguration() + .getJavaTypeRegistry() + .getDescriptor( Object.class ) + ) + ); + } + + @Override + public NationalizationSupport getNationalizationSupport() { + // At least the jTDS driver doesn't support this + return jtdsDriver ? NationalizationSupport.IMPLICIT : super.getNationalizationSupport(); + } + + @Override + public void initializeFunctionRegistry(QueryEngine queryEngine) { + super.initializeFunctionRegistry(queryEngine); + + CommonFunctionFactory functionFactory = new CommonFunctionFactory(queryEngine); + + // For SQL-Server we need to cast certain arguments to varchar(16384) to be able to concat them + queryEngine.getSqmFunctionRegistry().register( + "count", + new CountFunction( + this, + queryEngine.getTypeConfiguration(), + SqlAstNodeRenderingMode.DEFAULT, + "+", + "varchar(16384)", + false + ) + ); + + // AVG by default uses the input type, so we possibly need to cast the argument type, hence a special function + functionFactory.avg_castingNonDoubleArguments( this, SqlAstNodeRenderingMode.DEFAULT ); + + //this doesn't work 100% on earlier versions of Sybase + //which were missing the third parameter in charindex() + //TODO: we could emulate it with substring() like in Postgres + functionFactory.locate_charindex(); + + functionFactory.replace_strReplace(); + functionFactory.everyAny_minMaxCase(); + functionFactory.octetLength_pattern( "datalength(?1)" ); + functionFactory.bitLength_pattern( "datalength(?1)*8" ); + + queryEngine.getSqmFunctionRegistry().register( "timestampadd", + new IntegralTimestampaddFunction( this, queryEngine.getTypeConfiguration() ) ); + } + + @Override + public String getNullColumnString() { + return " null"; + } + + @Override + public boolean canCreateSchema() { + // As far as I can tell, it does not + return false; + } + + @Override + public String getCurrentSchemaCommand() { + return "select db_name()"; + } + + @Override + public int getMaxIdentifierLength() { + return 128; + } + + @Override + public String castPattern(CastType from, CastType to) { + if ( to == CastType.STRING ) { + switch ( from ) { + case DATE: + return "str_replace(convert(varchar,?1,102),'.','-')"; + case TIME: + return "convert(varchar,?1,108)"; + case TIMESTAMP: + return "str_replace(convert(varchar,?1,23),'T',' ')"; + } + } + return super.castPattern( from, to ); + } + + @Override + public String translateExtractField(TemporalUnit unit) { + switch ( unit ) { + case WEEK: return "calweekofyear"; //the ISO week number I think + default: return super.translateExtractField(unit); + } + } + + @Override + public String extractPattern(TemporalUnit unit) { + //TODO!! + return "datepart(?1,?2)"; + } + + @Override + public boolean supportsFractionalTimestampArithmetic() { + return false; + } + + @Override + public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { + //TODO!! + return "dateadd(?1,?2,?3)"; + } + + @Override + public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { + //TODO!! + return "datediff(?1,?2,?3)"; + } + + @Override + public String trimPattern(TrimSpec specification, char character) { + return super.trimPattern(specification, character) + .replace("replace", "str_replace"); + } + + @Override + public void appendDatetimeFormat(SqlAppender appender, String format) { + throw new UnsupportedOperationException( "format() function not supported on Sybase"); + } + + @Override + public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) + throws SQLException { + if ( dbMetaData == null ) { + builder.setUnquotedCaseStrategy( IdentifierCaseStrategy.MIXED ); + builder.setQuotedCaseStrategy( IdentifierCaseStrategy.MIXED ); + } + + return super.buildIdentifierHelper( builder, dbMetaData ); + } + + @Override + public NameQualifierSupport getNameQualifierSupport() { + if ( getVersion().isSameOrAfter( 15 ) ) { + return NameQualifierSupport.BOTH; + } + return NameQualifierSupport.CATALOG; + } + +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java new file mode 100644 index 0000000000..b496b097d2 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java @@ -0,0 +1,180 @@ +/* + * 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 java.util.List; +import java.util.function.Consumer; + +import org.hibernate.LockMode; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; +import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.exec.spi.JdbcOperation; + +/** + * A SQL AST translator for Sybase. + * + * @author Christian Beikov + */ +public class SybaseLegacySqlAstTranslator extends AbstractSqlAstTranslator { + + public SybaseLegacySqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { + super( sessionFactory, statement ); + } + + // Sybase does not allow CASE expressions where all result arms contain plain parameters. + // At least one result arm must provide some type context for inference, + // so we cast the first result arm if we encounter this condition + + @Override + protected void visitAnsiCaseSearchedExpression( + CaseSearchedExpression caseSearchedExpression, + Consumer resultRenderer) { + if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( caseSearchedExpression ) ) { + final List whenFragments = caseSearchedExpression.getWhenFragments(); + final Expression firstResult = whenFragments.get( 0 ).getResult(); + super.visitAnsiCaseSearchedExpression( + caseSearchedExpression, + e -> { + if ( e == firstResult ) { + renderCasted( e ); + } + else { + resultRenderer.accept( e ); + } + } + ); + } + else { + super.visitAnsiCaseSearchedExpression( caseSearchedExpression, resultRenderer ); + } + } + + @Override + protected void visitAnsiCaseSimpleExpression( + CaseSimpleExpression caseSimpleExpression, + Consumer resultRenderer) { + if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( caseSimpleExpression ) ) { + final List whenFragments = caseSimpleExpression.getWhenFragments(); + final Expression firstResult = whenFragments.get( 0 ).getResult(); + super.visitAnsiCaseSimpleExpression( + caseSimpleExpression, + e -> { + if ( e == firstResult ) { + renderCasted( e ); + } + else { + resultRenderer.accept( e ); + } + } + ); + } + else { + super.visitAnsiCaseSimpleExpression( caseSimpleExpression, resultRenderer ); + } + } + + @Override + protected boolean renderNamedTableReference(NamedTableReference tableReference, LockMode lockMode) { + super.renderNamedTableReference( tableReference, lockMode ); + if ( LockMode.READ.lessThan( lockMode ) ) { + appendSql( " holdlock" ); + } + return true; + } + + @Override + protected void renderForUpdateClause(QuerySpec querySpec, ForUpdateClause forUpdateClause) { + // Sybase does not support the FOR UPDATE clause + } + + @Override + protected void renderSearchClause(CteStatement cte) { + // Sybase does not support this, but it's just a hint anyway + } + + @Override + protected void renderCycleClause(CteStatement cte) { + // Sybase does not support this, but it can be emulated + } + + @Override + public void visitOffsetFetchClause(QueryPart queryPart) { + assertRowsOnlyFetchClauseType( queryPart ); + if ( !queryPart.isRoot() && queryPart.getOffsetClauseExpression() != null ) { + throw new IllegalArgumentException( "Can't emulate offset clause in subquery" ); + } + } + + @Override + protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + renderComparisonEmulateIntersect( lhs, operator, rhs ); + } + + @Override + protected void renderSelectTupleComparison( + List lhsExpressions, + SqlTuple tuple, + ComparisonOperator operator) { + emulateSelectTupleComparison( lhsExpressions, tuple.getExpressions(), operator, true ); + } + + @Override + protected void renderPartitionItem(Expression expression) { + if ( expression instanceof Literal ) { + // Note that this depends on the SqmToSqlAstConverter to add a dummy table group + appendSql( "dummy_.x" ); + } + else if ( expression instanceof Summarization ) { + // This could theoretically be emulated by rendering all grouping variations of the query and + // connect them via union all but that's probably pretty inefficient and would have to happen + // on the query spec level + throw new UnsupportedOperationException( "Summarization is not supported by DBMS" ); + } + else { + expression.accept( this ); + } + } + + @Override + protected boolean supportsRowValueConstructorSyntax() { + return false; + } + + @Override + protected boolean supportsRowValueConstructorSyntaxInInList() { + return false; + } + + @Override + protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + return false; + } + + @Override + protected boolean needsRowsToSkip() { + return true; + } + + @Override + protected boolean needsMaxRows() { + return true; + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqmToSqlAstConverter.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqmToSqlAstConverter.java new file mode 100644 index 0000000000..315ddc6d4a --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqmToSqlAstConverter.java @@ -0,0 +1,93 @@ +/* + * 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.engine.spi.LoadQueryInfluencers; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.expression.SqmExpression; +import org.hibernate.query.sqm.tree.select.SqmQuerySpec; +import org.hibernate.sql.ast.spi.SqlAstCreationContext; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; +import org.hibernate.sql.ast.tree.select.QuerySpec; + +/** + * A SQM to SQL AST translator for Sybase ASE. + * + * @author Christian Beikov + */ +public class SybaseLegacySqmToSqlAstConverter extends BaseSqmToSqlAstConverter { + + private boolean needsDummyTableGroup; + + public SybaseLegacySqmToSqlAstConverter( + SqmStatement statement, + QueryOptions queryOptions, + DomainParameterXref domainParameterXref, + QueryParameterBindings domainParameterBindings, + LoadQueryInfluencers fetchInfluencers, + SqlAstCreationContext creationContext, + boolean deduplicateSelectionItems) { + super( + creationContext, + statement, + queryOptions, + fetchInfluencers, + domainParameterXref, + domainParameterBindings, + deduplicateSelectionItems + ); + } + + @Override + public QuerySpec visitQuerySpec(SqmQuerySpec sqmQuerySpec) { + final boolean needsDummy = this.needsDummyTableGroup; + this.needsDummyTableGroup = false; + try { + final QuerySpec querySpec = super.visitQuerySpec( sqmQuerySpec ); + if ( this.needsDummyTableGroup ) { + querySpec.getFromClause().addRoot( + new StandardTableGroup( + true, + null, + null, + null, + new NamedTableReference( + "(select 1)", + "dummy_(x)", + false, + getCreationContext().getSessionFactory() + ), + null, + getCreationContext().getSessionFactory() + ) + ); + } + return querySpec; + } + finally { + this.needsDummyTableGroup = needsDummy; + } + } + + @Override + protected Expression resolveGroupOrOrderByExpression(SqmExpression groupByClauseExpression) { + final Expression expression = super.resolveGroupOrOrderByExpression( groupByClauseExpression ); + if ( expression instanceof Literal ) { + // Note that SqlAstTranslator.renderPartitionItem depends on this + this.needsDummyTableGroup = true; + } + return expression; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/lockhint/SybaseASE15LockHintsTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/unit/lockhint/SybaseASE15LockHintsTest.java similarity index 68% rename from hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/lockhint/SybaseASE15LockHintsTest.java rename to hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/unit/lockhint/SybaseASE15LockHintsTest.java index f64da376e5..3cbc8d07d8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/unit/lockhint/SybaseASE15LockHintsTest.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/unit/lockhint/SybaseASE15LockHintsTest.java @@ -4,16 +4,18 @@ * 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.dialect.unit.lockhint; +package org.hibernate.community.dialect.unit.lockhint; +import org.hibernate.community.dialect.SybaseASELegacyDialect; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.SybaseASE15Dialect; +import org.hibernate.orm.test.dialect.unit.lockhint.AbstractLockHintTest; /** * @author Gail Badner */ public class SybaseASE15LockHintsTest extends AbstractLockHintTest { - public static final Dialect DIALECT = new SybaseASE15Dialect(); + public static final Dialect DIALECT = new SybaseASELegacyDialect(); protected String getLockHintUsed() { return "holdlock"; 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 c0bf4a40d0..1d66b56d2f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java @@ -48,10 +48,12 @@ import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtract import static org.hibernate.type.SqlTypes.*; /** - * A {@linkplain Dialect SQL dialect} for Sybase Adaptive Server Enterprise 11.9 and above. + * A {@linkplain Dialect SQL dialect} for Sybase Adaptive Server Enterprise 16 and above. */ public class SybaseASEDialect extends SybaseDialect { + private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 16, 0 ); + private final SizeStrategy sizeStrategy = new SizeStrategyImpl() { @Override public Size resolveSize( @@ -74,7 +76,7 @@ public class SybaseASEDialect extends SybaseDialect { private final boolean ansiNull; public SybaseASEDialect() { - this( DatabaseVersion.make( 11 ) ); + this( MINIMUM_VERSION ); } public SybaseASEDialect(DatabaseVersion version) { @@ -96,13 +98,10 @@ public class SybaseASEDialect extends SybaseDialect { // tinyint to store signed bytes, we can use it // to store boolean values) return "tinyint"; - case BIGINT: - // Sybase ASE didn't introduce 'bigint' until version 15.0 - return getVersion().isBefore( 15 ) ? "numeric(19,0)" : super.columnType( sqlTypeCode ); case DATE: - return getVersion().isSameOrAfter( 12 ) ? "date" : super.columnType( sqlTypeCode ); + return "date"; case TIME: - return getVersion().isSameOrAfter( 12 ) ? "time" : super.columnType( sqlTypeCode ); + return "time"; } return super.columnType( sqlTypeCode ); } @@ -114,7 +113,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 ( getVersion().isSameOrAfter( 15, 5 ) && !jtdsDriver ) { + if ( !jtdsDriver ) { ddlTypeRegistry.addDescriptor( CapacityDependentDdlType.builder( DATE, "bigdatetime", "bigdatetime", this ) .withTypeCapacity( 3, "datetime" ) @@ -265,7 +264,7 @@ public class SybaseASEDialect extends SybaseDialect { case NATIVE: // If the driver or database do not support bigdatetime and bigtime types, // we try to operate on milliseconds instead - if ( getVersion().isBefore( 15, 5 ) || jtdsDriver ) { + if ( jtdsDriver ) { return "dateadd(millisecond,?2/1000000,?3)"; } else { @@ -280,12 +279,7 @@ public class SybaseASEDialect extends SybaseDialect { public long getFractionalSecondPrecisionInNanos() { // If the database does not support bigdatetime and bigtime types, // we try to operate on milliseconds instead - if ( getVersion().isBefore( 15, 5 ) ) { - return 1_000_000; - } - else { - return 1_000; - } + return 1_000; } @Override @@ -294,14 +288,7 @@ public class SybaseASEDialect extends SybaseDialect { switch ( unit ) { case NANOSECOND: case NATIVE: - // If the database does not support bigdatetime and bigtime types, - // we try to operate on milliseconds instead - if ( getVersion().isBefore( 15, 5 ) ) { - return "cast(datediff(ms,?2,?3) as numeric(21))"; - } - else { - return "cast(datediff(mcs,cast(?2 as bigdatetime),cast(?3 as bigdatetime)) as numeric(21))"; - } + return "cast(datediff(mcs,cast(?2 as bigdatetime),cast(?3 as bigdatetime)) as numeric(21))"; default: return "datediff(?1,?2,?3)"; } @@ -577,13 +564,7 @@ public class SybaseASEDialect extends SybaseDialect { public String getTableTypeString() { //HHH-7298 I don't know if this would break something or cause some side affects //but it is required to use 'select for update' - return getVersion().isBefore( 15, 7 ) ? super.getTableTypeString() : " lock datarows"; - } - - @Override - public boolean supportsExpectedLobUsagePattern() { - // Earlier Sybase did not support LOB locators at all - return getVersion().isSameOrAfter( 15, 7 ); + return " lock datarows"; } @Override @@ -593,33 +574,29 @@ public class SybaseASEDialect extends SybaseDialect { @Override public RowLockStrategy getWriteRowLockStrategy() { - return getVersion().isSameOrAfter( 15, 7 ) ? RowLockStrategy.COLUMN : RowLockStrategy.TABLE; + return RowLockStrategy.COLUMN; } @Override public String getForUpdateString() { - return getVersion().isBefore( 15, 7 ) ? "" : " for update"; + return " for update"; } @Override public String getForUpdateString(String aliases) { - return getVersion().isBefore( 15, 7 ) - ? "" - : getForUpdateString() + " of " + aliases; + return getForUpdateString() + " of " + aliases; } @Override public String appendLockHint(LockOptions mode, String tableName) { //TODO: is this really necessary??! - return getVersion().isBefore( 15, 7 ) ? super.appendLockHint( mode, tableName ) : tableName; + return tableName; } @Override public String applyLocksToSql(String sql, LockOptions aliasedLockOptions, Map keyColumnNames) { //TODO: is this really correct? - return getVersion().isBefore( 15, 7 ) - ? super.applyLocksToSql( sql, aliasedLockOptions, keyColumnNames ) - : sql + new ForUpdateFragment( this, aliasedLockOptions, keyColumnNames ).toFragmentString(); + return sql + new ForUpdateFragment( this, aliasedLockOptions, keyColumnNames ).toFragmentString(); } @Override @@ -665,10 +642,6 @@ public class SybaseASEDialect extends SybaseDialect { @Override public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { - if ( getVersion().isBefore( 15, 7 ) ) { - return null; - } - return (sqlException, message, sql) -> { final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException ); final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException ); @@ -707,10 +680,6 @@ public class SybaseASEDialect extends SybaseDialect { @Override public LimitHandler getLimitHandler() { - if ( getVersion().isBefore( 12, 5 ) ) { - //support for SELECT TOP was introduced in Sybase ASE 12.5.3 - return super.getLimitHandler(); - } return new TopLimitHandler(false); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java index 47cbf55401..721a11b85d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java @@ -104,12 +104,6 @@ public class SybaseASESqlAstTranslator extends Abstract @Override protected boolean renderNamedTableReference(NamedTableReference tableReference, LockMode lockMode) { super.renderNamedTableReference( tableReference, lockMode ); - if ( getDialect().getVersion().isBefore( 15, 7 ) ) { - if ( LockMode.READ.lessThan( lockMode ) ) { - appendSql( " holdlock" ); - } - return true; - } return false; } @@ -144,14 +138,6 @@ public class SybaseASESqlAstTranslator extends Abstract } } - @Override - protected void renderForUpdateClause(QuerySpec querySpec, ForUpdateClause forUpdateClause) { - if ( getDialect().getVersion().isBefore( 15, 7 ) ) { - return; - } - super.renderForUpdateClause( querySpec, forUpdateClause ); - } - @Override protected void renderSearchClause(CteStatement cte) { // Sybase ASE does not support this, but it's just a hint anyway @@ -394,7 +380,7 @@ public class SybaseASESqlAstTranslator extends Abstract } private boolean supportsTopClause() { - return getDialect().getVersion().isSameOrAfter( 12, 5 ); + return true; } private boolean supportsParameterOffsetFetchExpression() { 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 811a81a737..ca2025e494 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java @@ -60,17 +60,20 @@ import jakarta.persistence.TemporalType; */ public class SybaseDialect extends AbstractTransactSQLDialect { - protected boolean jtdsDriver; + protected final boolean jtdsDriver; + + private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 16, 0 ); //All Sybase dialects share an IN list size limit. private static final int PARAM_LIST_SIZE_LIMIT = 250000; public SybaseDialect() { - this( DatabaseVersion.make( 11, 0 ) ); + this( MINIMUM_VERSION ); } public SybaseDialect(DatabaseVersion version) { super(version); + jtdsDriver = true; } public SybaseDialect(DialectResolutionInfo info) { @@ -79,6 +82,11 @@ public class SybaseDialect extends AbstractTransactSQLDialect { && info.getDriverName().contains( "jTDS" ); } + @Override + protected DatabaseVersion getMinimumSupportedVersion() { + return MINIMUM_VERSION; + } + @Override public JdbcType resolveSqlTypeDescriptor( String columnTypeName, @@ -321,10 +329,12 @@ public class SybaseDialect extends AbstractTransactSQLDialect { @Override public NameQualifierSupport getNameQualifierSupport() { - if ( getVersion().isSameOrAfter( 15 ) ) { + if ( jtdsDriver ) { + return NameQualifierSupport.CATALOG; + } + else { return NameQualifierSupport.BOTH; } - return NameQualifierSupport.CATALOG; } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/function/SybaseASEFunctionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/function/SybaseASEFunctionTest.java index 661c99d614..b695543c91 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/function/SybaseASEFunctionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/function/SybaseASEFunctionTest.java @@ -51,7 +51,7 @@ import org.junit.jupiter.api.Test; xmlMappings = "org/hibernate/orm/test/dialect/function/Product.hbm.xml" ) @SessionFactory -@RequiresDialect(value = SybaseASEDialect.class, majorVersion = 11) +@RequiresDialect(value = SybaseASEDialect.class) @SuppressWarnings("rawtypes") public class SybaseASEFunctionTest {