Skip to content

Commit

Permalink
HHH-15736 Handle backslash escapes in like patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
mbladel committed Dec 12, 2022
1 parent a5525a9 commit 254724d
Show file tree
Hide file tree
Showing 13 changed files with 539 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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;
Expand Down Expand Up @@ -209,7 +210,17 @@ protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lo
}
}
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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;
Expand All @@ -51,7 +52,11 @@ public MariaDBLegacyDialect(DatabaseVersion version) {
}

public MariaDBLegacyDialect(DialectResolutionInfo info) {
super( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) );
super(
createVersion( info ),
getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ),
getNoBackslashEscapes( info.getDatabaseMetadata() )
);
registerKeywords( info );
}

Expand Down Expand Up @@ -193,4 +198,26 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D

return super.buildIdentifierHelper( builder, dbMetaData );
}

protected void appendLikeLiteral(SqlAppender appender, String literal) {
appender.appendSql( '\'' );
for ( int i = 0; i < literal.length(); i++ ) {
final char c = literal.charAt( i );
switch ( c ) {
case '\'':
appender.appendSql( '\'' );
break;
case '\\':
if ( !isNoBackslashEscapesEnabled() ) {
appender.appendSql( "\\\\\\" );
}
else {
appender.appendSql( '\\' );
}
break;
}
appender.appendSql( c );
}
appender.appendSql( '\'' );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,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.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;
Expand Down Expand Up @@ -157,6 +160,75 @@ else if ( expression instanceof Summarization ) {
}
}

@Override
public void visitLikePredicate(LikePredicate likePredicate) {
if ( likePredicate.isCaseSensitive() ) {
likePredicate.getMatchExpression().accept( this );
if ( likePredicate.isNegated() ) {
appendSql( " not" );
}
appendSql( " like " );
escapeLikePattern( likePredicate.getPattern(), likePredicate.getEscapeCharacter() );
}
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 );
escapeLikePattern( likePredicate.getPattern(), likePredicate.getEscapeCharacter() );
appendSql( CLOSE_PARENTHESIS );
}
if ( likePredicate.getEscapeCharacter() != null ) {
appendSql( " escape " );
likePredicate.getEscapeCharacter().accept( this );
}
}

protected void escapeLikePattern(Expression pattern, Expression escapeCharacter) {
// 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
// See https://mariadb.com/kb/en/like/#description
if ( pattern instanceof Literal ) {
Object literalValue = ( (Literal) pattern ).getLiteralValue();
if ( literalValue == null ) {
pattern.accept( this );
}
else {
getDialect().appendLikeLiteral( this, literalValue.toString() );
}
}
else {
// replace(<value>, '\\', '\\\\')
appendSql( "replace" );
appendSql( OPEN_PARENTHESIS );
pattern.accept( this );
String replaceParams = "'\\\\','\\\\\\\\'";
if ( getDialect().isNoBackslashEscapesEnabled() ) {
replaceParams = replaceParams.replace( "\\\\", "\\" );
}
appendSql( "," + replaceParams );
appendSql( CLOSE_PARENTHESIS );
}
}
}

@Override
public boolean supportsRowValueConstructorSyntaxInSet() {
return false;
Expand All @@ -178,6 +250,11 @@ protected boolean supportsDistinctFromPredicate() {
return true;
}

@Override
public MariaDBLegacyDialect getDialect() {
return (MariaDBLegacyDialect) super.getDialect();
}

private boolean supportsWindowFunctions() {
return getDialect().getVersion().isSameOrAfter( 10, 2 );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Arrays;

import org.hibernate.LockOptions;
import org.hibernate.PessimisticLockException;
Expand Down Expand Up @@ -141,22 +142,29 @@ public Size resolveSize(
private final int maxVarcharLength;
private final int maxVarbinaryLength;

private final boolean noBackslashEscapesEnabled;

public MySQLLegacyDialect() {
this( DatabaseVersion.make( 5, 0 ) );
}

public MySQLLegacyDialect(DatabaseVersion version) {
this( version, 4 );
this( version, 4, false );
}

public MySQLLegacyDialect(DatabaseVersion version, int bytesPerCharacter) {
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 ),
getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ),
getNoBackslashEscapes( info.getDatabaseMetadata() )
);
registerKeywords( info );
}

Expand Down Expand Up @@ -396,6 +404,22 @@ protected static int getCharacterSetBytesPerCharacter(DatabaseMetaData databaseM
return 4;
}

protected static boolean getNoBackslashEscapes(DatabaseMetaData databaseMetaData) {
// See https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html
try (java.sql.Statement s = databaseMetaData.getConnection().createStatement() ) {
final ResultSet rs = s.executeQuery( "SELECT @@sql_mode" );
if ( rs.next() ) {
final String sqlMode = rs.getString( 1 );
final String expectedValue = "no_backslash_escapes";
return sqlMode.toLowerCase().contains( expectedValue );
}
}
catch (SQLException ex) {
// Ignore
}
return false;
}

private static int maxVarbinaryLength(DatabaseVersion version) {
return version.isBefore( 5 ) ? 255 : 65_535;
}
Expand Down Expand Up @@ -430,6 +454,10 @@ public int getMaxVarbinaryLength() {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,6 +17,7 @@
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;
Expand Down Expand Up @@ -139,6 +141,17 @@ else if ( expression instanceof Summarization ) {
}
}

@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 ( !( (MySQLLegacyDialect) getDialect() ).isNoBackslashEscapesEnabled() && likePredicate.getEscapeCharacter() == null ) {
appendSql( " escape ''" );
}
}

@Override
public boolean supportsRowValueConstructorSyntaxInSet() {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
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;
Expand Down Expand Up @@ -244,7 +245,17 @@ protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lo
}
}
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand Down Expand Up @@ -56,7 +57,11 @@ public MariaDBDialect(DatabaseVersion version) {
}

public MariaDBDialect(DialectResolutionInfo info) {
super( createVersion( info ), getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ) );
super(
createVersion( info ),
getCharacterSetBytesPerCharacter( info.getDatabaseMetadata() ),
getNoBackslashEscapes( info.getDatabaseMetadata() )
);
registerKeywords( info );
}

Expand Down Expand Up @@ -238,4 +243,26 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D

return super.buildIdentifierHelper( builder, dbMetaData );
}

protected void appendLikeLiteral(SqlAppender appender, String literal) {
appender.appendSql( '\'' );
for ( int i = 0; i < literal.length(); i++ ) {
final char c = literal.charAt( i );
switch ( c ) {
case '\'':
appender.appendSql( '\'' );
break;
case '\\':
if ( !isNoBackslashEscapesEnabled() ) {
appender.appendSql( "\\\\\\" );
}
else {
appender.appendSql( '\\' );
}
break;
}
appender.appendSql( c );
}
appender.appendSql( '\'' );
}
}

0 comments on commit 254724d

Please sign in to comment.