diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java index d822cad324..6cb3d5603b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java @@ -31,6 +31,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -209,7 +210,17 @@ public class H2LegacySqlAstTranslator extends AbstractS } } return super.renderPrimaryTableReference( tableGroup, lockMode ); + } + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + super.visitLikePredicate( likePredicate ); + // Custom implementation because H2 uses backslash as the default escape character + // We can override this by specifying an empty escape character + // See http://www.h2database.com/html/grammar.html#like_predicate_right_hand_side + if ( likePredicate.getEscapeCharacter() == null ) { + appendSql( " escape ''" ); + } } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java index a8b825e505..1f6001f61b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java @@ -12,6 +12,7 @@ import java.sql.SQLException; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.InnoDBStorageEngine; +import org.hibernate.dialect.MySQLServerConfiguration; import org.hibernate.dialect.MySQLStorageEngine; import org.hibernate.dialect.NationalizationSupport; import org.hibernate.dialect.function.CommonFunctionFactory; @@ -25,6 +26,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.spi.QueryEngine; 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.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -51,7 +53,7 @@ public class MariaDBLegacyDialect extends MySQLLegacyDialect { } public MariaDBLegacyDialect(DialectResolutionInfo info) { - super( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + super( createVersion( info ), MySQLServerConfiguration.fromDatabaseMetadata( info.getDatabaseMetadata() ) ); registerKeywords( info ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java index 2c786af554..a398b51cbb 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java @@ -6,6 +6,8 @@ */ package org.hibernate.community.dialect; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; @@ -16,6 +18,7 @@ import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; @@ -157,6 +160,44 @@ public class MariaDBLegacySqlAstTranslator extends Abst } } + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + if ( likePredicate.isCaseSensitive() ) { + likePredicate.getMatchExpression().accept( this ); + if ( likePredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " like " ); + renderBackslashEscapedLikePattern( + likePredicate.getPattern(), + likePredicate.getEscapeCharacter(), + getDialect().isNoBackslashEscapesEnabled() + ); + } + else { + appendSql( getDialect().getLowercaseFunction() ); + appendSql( OPEN_PARENTHESIS ); + likePredicate.getMatchExpression().accept( this ); + appendSql( CLOSE_PARENTHESIS ); + if ( likePredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " like " ); + appendSql( getDialect().getLowercaseFunction() ); + appendSql( OPEN_PARENTHESIS ); + renderBackslashEscapedLikePattern( + likePredicate.getPattern(), + likePredicate.getEscapeCharacter(), + getDialect().isNoBackslashEscapesEnabled() + ); + appendSql( CLOSE_PARENTHESIS ); + } + if ( likePredicate.getEscapeCharacter() != null ) { + appendSql( " escape " ); + likePredicate.getEscapeCharacter().accept( this ); + } + } + @Override public boolean supportsRowValueConstructorSyntaxInSet() { return false; @@ -178,6 +219,11 @@ public class MariaDBLegacySqlAstTranslator extends Abst return true; } + @Override + public MariaDBLegacyDialect getDialect() { + return (MariaDBLegacyDialect) super.getDialect(); + } + private boolean supportsWindowFunctions() { return getDialect().getVersion().isSameOrAfter( 10, 2 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 6e1631a09c..99d1ada02f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -11,6 +11,7 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.util.Arrays; import org.hibernate.LockOptions; import org.hibernate.PessimisticLockException; @@ -20,6 +21,7 @@ import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.InnoDBStorageEngine; import org.hibernate.dialect.MyISAMStorageEngine; +import org.hibernate.dialect.MySQLServerConfiguration; import org.hibernate.dialect.MySQLStorageEngine; import org.hibernate.dialect.Replacer; import org.hibernate.dialect.RowLockStrategy; @@ -141,6 +143,8 @@ public class MySQLLegacyDialect extends Dialect { private final int maxVarcharLength; private final int maxVarbinaryLength; + private final boolean noBackslashEscapesEnabled; + public MySQLLegacyDialect() { this( DatabaseVersion.make( 5, 0 ) ); } @@ -150,13 +154,22 @@ public class MySQLLegacyDialect extends Dialect { } public MySQLLegacyDialect(DatabaseVersion version, int bytesPerCharacter) { + this( version, bytesPerCharacter, false ); + } + + public MySQLLegacyDialect(DatabaseVersion version, MySQLServerConfiguration serverConfiguration) { + this( version, serverConfiguration.getBytesPerCharacter(), serverConfiguration.isNoBackslashEscapesEnabled() ); + } + + public MySQLLegacyDialect(DatabaseVersion version, int bytesPerCharacter, boolean noBackslashEscapes) { super( version ); maxVarcharLength = maxVarcharLength( getMySQLVersion(), bytesPerCharacter ); //conservative assumption maxVarbinaryLength = maxVarbinaryLength( getMySQLVersion() ); + noBackslashEscapesEnabled = noBackslashEscapes; } public MySQLLegacyDialect(DialectResolutionInfo info) { - this( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + this( createVersion( info ), MySQLServerConfiguration.fromDatabaseMetadata( info.getDatabaseMetadata() ) ); registerKeywords( info ); } @@ -356,6 +369,7 @@ public class MySQLLegacyDialect extends Dialect { ); } + @Deprecated protected static int getCharacterSetBytesPerCharacter(DatabaseMetaData databaseMetaData) { if ( databaseMetaData != null ) { try (java.sql.Statement s = databaseMetaData.getConnection().createStatement() ) { @@ -430,6 +444,10 @@ public class MySQLLegacyDialect extends Dialect { return maxVarbinaryLength; } + public boolean isNoBackslashEscapesEnabled() { + return noBackslashEscapesEnabled; + } + @Override public String getNullColumnString(String columnType) { // Good job MySQL https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java index 4d0801af9a..fff99f4493 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java @@ -6,6 +6,7 @@ */ package org.hibernate.community.dialect; +import org.hibernate.dialect.MySQLDialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; @@ -16,6 +17,7 @@ import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; @@ -139,6 +141,17 @@ public class MySQLLegacySqlAstTranslator extends Abstra } } + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + super.visitLikePredicate( likePredicate ); + // Custom implementation because MySQL uses backslash as the default escape character + // We can override this by specifying an empty escape character + // See https://dev.mysql.com/doc/refman/8.0/en/string-comparison-functions.html#operator_like + if ( !( (MySQLLegacyDialect) getDialect() ).isNoBackslashEscapesEnabled() && likePredicate.getEscapeCharacter() == null ) { + appendSql( " escape ''" ); + } + } + @Override public boolean supportsRowValueConstructorSyntaxInSet() { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java index 8d38ace00b..dabe069ff6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java @@ -32,6 +32,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -244,7 +245,17 @@ public class H2SqlAstTranslator extends AbstractSqlAstT } } return super.renderPrimaryTableReference( tableGroup, lockMode ); + } + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + super.visitLikePredicate( likePredicate ); + // Custom implementation because H2 uses backslash as the default escape character + // We can override this by specifying an empty escape character + // See http://www.h2database.com/html/grammar.html#like_predicate_right_hand_side + if ( likePredicate.getEscapeCharacter() == null ) { + appendSql( " escape ''" ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java index 39d377e27e..11837d7509 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -22,6 +22,7 @@ import org.hibernate.query.spi.QueryEngine; import org.hibernate.service.ServiceRegistry; 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.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -56,7 +57,7 @@ public class MariaDBDialect extends MySQLDialect { } public MariaDBDialect(DialectResolutionInfo info) { - super( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + super( createVersion( info ), MySQLServerConfiguration.fromDatabaseMetadata( info.getDatabaseMetadata() ) ); registerKeywords( info ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java index cde752f5df..d3560b6a1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java @@ -10,12 +10,12 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; @@ -152,6 +152,44 @@ public class MariaDBSqlAstTranslator extends AbstractSq } } + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + if ( likePredicate.isCaseSensitive() ) { + likePredicate.getMatchExpression().accept( this ); + if ( likePredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " like " ); + renderBackslashEscapedLikePattern( + likePredicate.getPattern(), + likePredicate.getEscapeCharacter(), + getDialect().isNoBackslashEscapesEnabled() + ); + } + else { + appendSql( getDialect().getLowercaseFunction() ); + appendSql( OPEN_PARENTHESIS ); + likePredicate.getMatchExpression().accept( this ); + appendSql( CLOSE_PARENTHESIS ); + if ( likePredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " like " ); + appendSql( getDialect().getLowercaseFunction() ); + appendSql( OPEN_PARENTHESIS ); + renderBackslashEscapedLikePattern( + likePredicate.getPattern(), + likePredicate.getEscapeCharacter(), + getDialect().isNoBackslashEscapesEnabled() + ); + appendSql( CLOSE_PARENTHESIS ); + } + if ( likePredicate.getEscapeCharacter() != null ) { + appendSql( " escape " ); + likePredicate.getEscapeCharacter().accept( this ); + } + } + @Override public boolean supportsRowValueConstructorSyntaxInSet() { return false; @@ -173,6 +211,11 @@ public class MariaDBSqlAstTranslator extends AbstractSq return true; } + @Override + public MariaDBDialect getDialect() { + return (MariaDBDialect) super.getDialect(); + } + private boolean supportsWindowFunctions() { return true; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index ba5059e279..18cca8ba16 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -11,6 +11,7 @@ import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; +import java.util.Arrays; import org.hibernate.LockOptions; import org.hibernate.PessimisticLockException; @@ -137,6 +138,8 @@ public class MySQLDialect extends Dialect { private final int maxVarcharLength; private final int maxVarbinaryLength; + private final boolean noBackslashEscapesEnabled; + public MySQLDialect() { this( MINIMUM_VERSION ); } @@ -146,13 +149,22 @@ public class MySQLDialect extends Dialect { } public MySQLDialect(DatabaseVersion version, int bytesPerCharacter) { + this( version, bytesPerCharacter, false ); + } + + public MySQLDialect(DatabaseVersion version, MySQLServerConfiguration serverConfiguration) { + this( version, serverConfiguration.getBytesPerCharacter(), serverConfiguration.isNoBackslashEscapesEnabled() ); + } + + public MySQLDialect(DatabaseVersion version, int bytesPerCharacter, boolean noBackslashEscapes) { super( version ); maxVarcharLength = maxVarcharLength( getMySQLVersion(), bytesPerCharacter ); //conservative assumption maxVarbinaryLength = maxVarbinaryLength( getMySQLVersion() ); + noBackslashEscapesEnabled = noBackslashEscapes; } public MySQLDialect(DialectResolutionInfo info) { - this( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + this( createVersion( info ), MySQLServerConfiguration.fromDatabaseMetadata( info.getDatabaseMetadata() ) ); registerKeywords( info ); } @@ -355,6 +367,7 @@ public class MySQLDialect extends Dialect { ); } + @Deprecated protected static int getCharacterSetBytesPerCharacter(DatabaseMetaData databaseMetaData) { if ( databaseMetaData != null ) { try (java.sql.Statement s = databaseMetaData.getConnection().createStatement() ) { @@ -423,6 +436,10 @@ public class MySQLDialect extends Dialect { return maxVarbinaryLength; } + public boolean isNoBackslashEscapesEnabled() { + return noBackslashEscapesEnabled; + } + @Override public String getNullColumnString(String columnType) { // Good job MySQL https://dev.mysql.com/doc/refman/8.0/en/timestamp-initialization.html @@ -1114,7 +1131,10 @@ public class MySQLDialect extends Dialect { appender.appendSql( '\'' ); break; case '\\': - appender.appendSql( '\\' ); + if ( !noBackslashEscapesEnabled ) { + // See https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_no_backslash_escapes + appender.appendSql( '\\' ); + } break; } appender.appendSql( c ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLServerConfiguration.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLServerConfiguration.java new file mode 100644 index 0000000000..481baf06e4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLServerConfiguration.java @@ -0,0 +1,80 @@ +/* + * 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.dialect; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; + +public class MySQLServerConfiguration { + + private final int bytesPerCharacter; + private final boolean noBackslashEscapesEnabled; + + public MySQLServerConfiguration(int bytesPerCharacter, boolean noBackslashEscapesEnabled) { + this.bytesPerCharacter = bytesPerCharacter; + this.noBackslashEscapesEnabled = noBackslashEscapesEnabled; + } + + public int getBytesPerCharacter() { + return bytesPerCharacter; + } + + public boolean isNoBackslashEscapesEnabled() { + return noBackslashEscapesEnabled; + } + + public static MySQLServerConfiguration fromDatabaseMetadata(DatabaseMetaData databaseMetaData) { + int bytesPerCharacter = 4; + boolean noBackslashEscapes = false; + if ( databaseMetaData != null ) { + try (java.sql.Statement s = databaseMetaData.getConnection().createStatement()) { + final ResultSet rs = s.executeQuery( "SELECT @@character_set_database, @@sql_mode" ); + if ( rs.next() ) { + final String characterSet = rs.getString( 1 ); + final int collationIndex = characterSet.indexOf( '_' ); + // According to https://dev.mysql.com/doc/refman/8.0/en/charset-charsets.html + switch ( collationIndex == -1 ? characterSet : characterSet.substring( 0, collationIndex ) ) { + case "utf16": + case "utf16le": + case "utf32": + case "utf8mb4": + case "gb18030": + break; + case "utf8": + case "utf8mb3": + case "eucjpms": + case "ujis": + bytesPerCharacter = 3; + break; + case "ucs2": + case "cp932": + case "big5": + case "euckr": + case "gb2312": + case "gbk": + case "sjis": + bytesPerCharacter = 2; + break; + default: + bytesPerCharacter = 1; + } + // NO_BACKSLASH_ESCAPES + final String sqlMode = rs.getString( 2 ); + if ( sqlMode.toLowerCase().contains( "no_backslash_escapes" ) ) { + noBackslashEscapes = true; + } + } + } + catch (SQLException ex) { + // Ignore + } + } + return new MySQLServerConfiguration(bytesPerCharacter, noBackslashEscapes); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java index d846a091d3..cb2ebc9392 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java @@ -16,6 +16,7 @@ import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; @@ -139,6 +140,17 @@ public class MySQLSqlAstTranslator extends AbstractSqlA } } + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + super.visitLikePredicate( likePredicate ); + // Custom implementation because MySQL uses backslash as the default escape character + // We can override this by specifying an empty escape character + // See https://dev.mysql.com/doc/refman/8.0/en/string-comparison-functions.html#operator_like + if ( !( (MySQLDialect) getDialect() ).isNoBackslashEscapesEnabled() && likePredicate.getEscapeCharacter() == null ) { + appendSql( " escape ''" ); + } + } + @Override public boolean supportsRowValueConstructorSyntaxInSet() { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java index 532469f372..2ee7facb86 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java @@ -15,6 +15,7 @@ import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.spi.SessionFactoryImplementor; 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.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -39,7 +40,7 @@ public class TiDBDialect extends MySQLDialect { } public TiDBDialect(DialectResolutionInfo info) { - super( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) ); + super(createVersion( info ), MySQLServerConfiguration.fromDatabaseMetadata( info.getDatabaseMetadata() )); registerKeywords( info ); } @@ -157,5 +158,4 @@ public class TiDBDialect extends MySQLDialect { Duration duration = Duration.ofMillis( timeoutInMilliseconds ); return duration.getSeconds(); } - } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java index a5685b2010..bc24536456 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java @@ -18,6 +18,7 @@ import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; @@ -116,6 +117,44 @@ public class TiDBSqlAstTranslator extends AbstractSqlAs } } + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + if ( likePredicate.isCaseSensitive() ) { + likePredicate.getMatchExpression().accept( this ); + if ( likePredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " like " ); + renderBackslashEscapedLikePattern( + likePredicate.getPattern(), + likePredicate.getEscapeCharacter(), + getDialect().isNoBackslashEscapesEnabled() + ); + } + else { + appendSql( getDialect().getLowercaseFunction() ); + appendSql( OPEN_PARENTHESIS ); + likePredicate.getMatchExpression().accept( this ); + appendSql( CLOSE_PARENTHESIS ); + if ( likePredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " like " ); + appendSql( getDialect().getLowercaseFunction() ); + appendSql( OPEN_PARENTHESIS ); + renderBackslashEscapedLikePattern( + likePredicate.getPattern(), + likePredicate.getEscapeCharacter(), + getDialect().isNoBackslashEscapesEnabled() + ); + appendSql( CLOSE_PARENTHESIS ); + } + if ( likePredicate.getEscapeCharacter() != null ) { + appendSql( " escape " ); + likePredicate.getEscapeCharacter().accept( this ); + } + } + @Override public boolean supportsRowValueConstructorSyntaxInSet() { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 5fd19fab37..298cf46a9c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -7087,6 +7087,70 @@ public abstract class AbstractSqlAstTranslator implemen } } + protected void renderBackslashEscapedLikePattern( + Expression pattern, + Expression escapeCharacter, + boolean noBackslashEscapes) { + // Check if escapeCharacter was explicitly set and do nothing in that case + // Note: this does not cover cases where it's set via parameter binding + boolean isExplicitEscape = false; + if ( escapeCharacter instanceof Literal ) { + Object literalValue = ( (Literal) escapeCharacter ).getLiteralValue(); + isExplicitEscape = literalValue != null && !literalValue.toString().equals( "" ); + } + if ( isExplicitEscape ) { + pattern.accept( this ); + } + else { + // Since escape with empty or null character is ignored we need + // four backslashes to render a single one in a like pattern + if ( pattern instanceof Literal ) { + Object literalValue = ( (Literal) pattern ).getLiteralValue(); + if ( literalValue == null ) { + pattern.accept( this ); + } + else { + appendBackslashEscapedLikeLiteral( this, literalValue.toString(), noBackslashEscapes ); + } + } + else { + // replace(,'\\','\\\\') + appendSql( "replace" ); + appendSql( OPEN_PARENTHESIS ); + pattern.accept( this ); + if ( noBackslashEscapes ) { + appendSql( ",'\\','\\\\'" ); + } + else { + appendSql( ",'\\\\','\\\\\\\\'" ); + } + appendSql( CLOSE_PARENTHESIS ); + } + } + } + + protected void appendBackslashEscapedLikeLiteral(SqlAppender appender, String literal, boolean noBackslashEscapes) { + appender.appendSql( '\'' ); + for ( int i = 0; i < literal.length(); i++ ) { + final char c = literal.charAt( i ); + switch ( c ) { + case '\'': + appender.appendSql( '\'' ); + break; + case '\\': + if ( noBackslashEscapes ) { + appender.appendSql( '\\' ); + } + else { + appender.appendSql( "\\\\\\" ); + } + break; + } + appender.appendSql( c ); + } + appender.appendSql( '\'' ); + } + @Override public void visitNegatedPredicate(NegatedPredicate negatedPredicate) { if ( negatedPredicate.isEmpty() ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LikeEscapeDefaultTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LikeEscapeDefaultTest.java new file mode 100644 index 0000000000..36f2ec4902 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LikeEscapeDefaultTest.java @@ -0,0 +1,118 @@ +/* + * 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.orm.test.query.hql; + +import java.util.List; + +import org.hibernate.query.Query; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Marco Belladelli + */ +@ServiceRegistry +@DomainModel( + standardModels = StandardDomainModel.GAMBIT +) +@SessionFactory +public class LikeEscapeDefaultTest { + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + BasicEntity be1 = new BasicEntity( 1, "Product\\one" ); + em.persist( be1 ); + BasicEntity be2 = new BasicEntity( 2, "Product%two" ); + em.persist( be2 ); + BasicEntity be3 = new BasicEntity( 3, "Product\"three" ); + em.persist( be3 ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> session.createMutationQuery( "delete from BasicEntity" ).executeUpdate() + ); + } + + @Test + public void testDefaultEscapeBackslash(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query q = session.createQuery( + "from BasicEntity be where be.data like ?1", + BasicEntity.class + ).setParameter( 1, "%\\%" ); + List l = q.getResultList(); + assertEquals( 1, l.size() ); + assertEquals( 1, l.get( 0 ).getId() ); + } ); + } + + @Test + public void testDefaultEscapeBackslashLiteral(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query q = session.createQuery( + "from BasicEntity be where be.data like '%\\%'", + BasicEntity.class + ); + List l = q.getResultList(); + assertEquals( 1, l.size() ); + assertEquals( 1, l.get( 0 ).getId() ); + } ); + } + + @Test + public void testDefaultEscapeNoResults(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query q = session.createQuery( + "from BasicEntity be where be.data like ?1", + BasicEntity.class + ).setParameter( 1, "%\\\"%" ); + List l = q.getResultList(); + assertEquals( 0, l.size() ); + } ); + } + + @Test + public void testExplicitEscapeLiteralBackslash(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query q = session.createQuery( + "from BasicEntity be where be.data like ?1 escape '\\'", + BasicEntity.class + ).setParameter( 1, "%\\%%" ); + List l = q.getResultList(); + assertEquals( 1, l.size() ); + assertEquals( 2, l.get( 0 ).getId() ); + } ); + } + + @Test + public void testExplicitEscapeLiteralOtherChar(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query q = session.createQuery( + "from BasicEntity be where be.data like ?1 escape '#'", + BasicEntity.class + ).setParameter( 1, "%#%%" ); + List l = q.getResultList(); + assertEquals( 1, l.size() ); + assertEquals( 2, l.get( 0 ).getId() ); + } ); + } +}