diff --git a/pom.xml b/pom.xml index e82c576dfc..b1dc69de3c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 4.2.0-SNAPSHOT + 4.2.0-2325-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index f830419609..52d741fca3 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-relational-parent - 4.2.0-SNAPSHOT + 4.2.0-2325-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 00919c3360..f10c31ae37 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -4,7 +4,7 @@ 4.0.0 spring-data-jdbc - 4.2.0-SNAPSHOT + 4.2.0-2325-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -13,7 +13,7 @@ org.springframework.data spring-data-relational-parent - 4.2.0-SNAPSHOT + 4.2.0-2325-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/DialectResolver.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/DialectResolver.java index affe4351fe..92b3cac23b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/DialectResolver.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/DialectResolver.java @@ -255,6 +255,11 @@ public Escaper getLikeEscaper() { return delegate.getLikeEscaper(); } + @Override + public Escaper getStringLiteralEscaper() { + return delegate.getStringLiteralEscaper(); + } + @Override public LimitClause limit() { return delegate.limit(); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/EscapingParameterSourceUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/EscapingParameterSourceUnitTests.java index 6fb34f0a45..12805de6fc 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/EscapingParameterSourceUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/EscapingParameterSourceUnitTests.java @@ -35,7 +35,7 @@ class EscapingParameterSourceUnitTests { MapSqlParameterSource delegate = new MapSqlParameterSource(); - Escaper escaper = Escaper.of('x'); + Escaper escaper = Escaper.escapeLikeWith('x'); EscapingParameterSource escapingParameterSource = new EscapingParameterSource(delegate, escaper); @Nested diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 273937bbfc..8f52baef37 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -717,8 +717,8 @@ void createsQueryByEmbeddedObject() throws Exception { .contains(TABLE + ".\"USER_STREET\" = :user_street", // " AND ", // TABLE + ".\"USER_CITY\" = :user_city"); - assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("user_street")).isEqualTo("Hello"); - assertThat(query.getParameterSource(Escaper.DEFAULT).getValue("user_city")).isEqualTo("World"); + assertThat(query.getParameterSource(Escaper.ANSI_LIKE_ESCAPER).getValue("user_street")).isEqualTo("Hello"); + assertThat(query.getParameterSource(Escaper.ANSI_LIKE_ESCAPER).getValue("user_city")).isEqualTo("World"); } @Test // GH-2187 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAssert.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAssert.java index cf1a24c55c..d54e08ed6e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAssert.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAssert.java @@ -101,7 +101,7 @@ public QueryAssert contains(String expected) { */ public QueryAssert hasBindValue(String key, Object value) { - SqlParameterSource parameterSource = actual.getParameterSource(Escaper.DEFAULT); + SqlParameterSource parameterSource = actual.getParameterSource(Escaper.ANSI_LIKE_ESCAPER); Assertions.assertThat(parameterSource.getValue(key)) .describedAs("Parameter source [%s] shouldn contain value [%s] for key [%s]", parameterSource, value, key) .isEqualTo(value); diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 386bb211d4..8543c61255 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -4,7 +4,7 @@ 4.0.0 spring-data-r2dbc - 4.2.0-SNAPSHOT + 4.2.0-2325-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -13,7 +13,7 @@ org.springframework.data spring-data-relational-parent - 4.2.0-SNAPSHOT + 4.2.0-2325-SNAPSHOT diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java index e1b5465924..9f5770331e 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java @@ -500,7 +500,7 @@ private Escaper getEscaper(Comparator comparator) { return dialect.getLikeEscaper(); } - return Escaper.DEFAULT; + return Escaper.ANSI_LIKE_ESCAPER; } /** diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java index f026eadc08..928920e446 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java @@ -141,7 +141,7 @@ private Collection getAssignments(SqlIdentifier columnName, @Nullabl } else if (value instanceof ValueFunction valueFunction) { - mappedValue = valueFunction.map(v -> convertValue(v, propertyField.getTypeHint())).apply(Escaper.DEFAULT); + mappedValue = valueFunction.map(v -> convertValue(v, propertyField.getTypeHint())).apply(Escaper.ANSI_LIKE_ESCAPER); if (mappedValue == null) { return List.of(Assignments.value(column, SQL.nullLiteral())); diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 2be7b8fe9a..c7f663b9d1 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -4,7 +4,7 @@ 4.0.0 spring-data-relational - 4.2.0-SNAPSHOT + 4.2.0-2325-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -12,7 +12,7 @@ org.springframework.data spring-data-relational-parent - 4.2.0-SNAPSHOT + 4.2.0-2325-SNAPSHOT diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java index 83b92bc3ba..8a7297a47b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java @@ -136,7 +136,20 @@ default boolean supportsSingleQueryLoading() { * @since 2.0 */ default Escaper getLikeEscaper() { - return Escaper.DEFAULT; + return Escaper.ANSI_LIKE_ESCAPER; + } + + /** + * Returns the {@link Escaper} used for escaping the content of string literals. + *

+ * The default follows the SQL standard of doubling a single quote ({@code '} → {@code ''}). Dialects that + * require a different strategy must override this method. + * + * @return the {@link Escaper} used for string literal escaping. + * @since 4.2 + */ + default Escaper getStringLiteralEscaper() { + return Escaper.ANSI_LITERAL_ESCAPER; } /** diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Escaper.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Escaper.java index c17f81e259..0a18892fac 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Escaper.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Escaper.java @@ -15,9 +15,9 @@ */ package org.springframework.data.relational.core.dialect; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; +import java.util.HashSet; +import java.util.Set; import org.jspecify.annotations.Nullable; @@ -28,35 +28,74 @@ * @author Roman Chigvintsev * @author Mark Paluch * @author Alexander Tochin + * @author Jens Schauder * @since 2.0 */ public class Escaper { - public static final Escaper DEFAULT = Escaper.of('\\'); + public static final Escaper ANSI_LIKE_ESCAPER = Escaper.escapeLikeWith('\\'); + public static final Escaper ANSI_LITERAL_ESCAPER = Escaper.of('\'', Set.of("'")); + + /** + * @deprecated since 4.2, use {@link #ANSI_LIKE_ESCAPER} instead. + */ + @Deprecated + public static final Escaper DEFAULT = ANSI_LIKE_ESCAPER; private final char escapeCharacter; - private final List toReplace; + private final Set toReplace; - private Escaper(char escapeCharacter, List toReplace) { + private Escaper(char escapeCharacter, Set toReplace) { + + this.escapeCharacter = escapeCharacter; + this.toReplace = new HashSet<>(toReplace); + } + /** + * Creates a new instance of this class with the given escape character escaping the {@code LIKE} special characters + * {@code _} and {@code %}. + * + * @param escapeCharacter escape character + * @return new instance of {@link Escaper}. + * @throws IllegalArgumentException if the escape character is one of the special characters ('_' and '%') + */ + public static Escaper escapeLikeWith(char escapeCharacter) { + + Set toReplace = Set.of("_", "%"); if (toReplace.contains(Character.toString(escapeCharacter))) { throw new IllegalArgumentException( - String.format("'%s' and cannot be used as escape character as it should be replaced", escapeCharacter)); + String.format("'%s' cannot be used as escape character as it should be replaced", escapeCharacter)); } - this.escapeCharacter = escapeCharacter; - this.toReplace = toReplace; + return Escaper.of(escapeCharacter, toReplace); } /** - * Creates a new instance of this class with the given escape character. + * Creates a new instance of this class with the given escape character escaping the {@code LIKE} special characters + * {@code _} and {@code %}. * * @param escapeCharacter escape character * @return new instance of {@link Escaper}. - * @throws IllegalArgumentException if the escape character is one of special characters ('_' and '%') + * @throws IllegalArgumentException if the escape character is one of the special characters ('_' and '%') + * @deprecated since 4.2, use {@link #escapeLikeWith(char)} instead. */ + @Deprecated public static Escaper of(char escapeCharacter) { - return new Escaper(escapeCharacter, Arrays.asList("_", "%")); + return escapeLikeWith(escapeCharacter); + } + + /** + * Creates a new instance of this class with the given escape character and the characters to be escaped. In contrast + * to {@link #of(char)} the {@code escapeCharacter} may itself be part of {@code toReplace}; this is the standard SQL + * way of escaping a single quote inside a string literal by doubling it ({@code '} → {@code ''}). + * + * @param escapeCharacter escape character. + * @param toReplace characters/char sequences that should be escaped. + * @return new instance of {@link Escaper}. + * @since 4.1 + */ + public static Escaper of(char escapeCharacter, Set toReplace) { + return new Escaper(escapeCharacter, toReplace); } /** @@ -67,7 +106,7 @@ public static Escaper of(char escapeCharacter) { */ public Escaper withRewriteFor(String... chars) { - List toReplace = new ArrayList<>(this.toReplace.size() + chars.length); + HashSet toReplace = new HashSet<>(this.toReplace.size() + chars.length); toReplace.addAll(this.toReplace); toReplace.addAll(Arrays.asList(chars)); @@ -84,8 +123,8 @@ public char getEscapeCharacter() { } /** - * Escapes all special like characters ({@code _}, {@code %}) using the configured escape character. - * Escape character itself is also escaped. + * Escapes all special like characters ({@code _}, {@code %}) using the configured escape character. Escape character + * itself is also escaped. * * @param value value to be escaped * @return escaped value @@ -99,6 +138,12 @@ public char getEscapeCharacter() { String escapeCharString = String.valueOf(escapeCharacter); String escapedValue = value.replace(escapeCharString, escapeCharString.repeat(2)); for (String character : toReplace) { + + // the escape character was already doubled in the step above; doubling it again would be wrong + if (character.equals(escapeCharString)) { + continue; + } + escapedValue = escapedValue.replace(character, escapeCharacter + character); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java index f19e7c5f15..c5d40442ad 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java @@ -17,6 +17,8 @@ import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.Set; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; @@ -119,6 +121,14 @@ public Position getClausePosition() { private static final Collection CONVERTERS = Arrays.asList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE, NumberToBooleanConverter.INSTANCE); + /** + * MySQL (and MariaDB) interpret the backslash as an escape character inside string literals by default. Therefore + * both the backslash itself and the single quote have to be escaped with a leading backslash ({@code \} → + * {@code \\}, {@code '} → {@code \'}). This differs from the SQL standard of doubling the single quote. Note that + * this is not correct if the server runs with the {@code NO_BACKSLASH_ESCAPES} SQL mode. + */ + private static final Escaper STRING_LITERAL_ESCAPER = Escaper.of('\\', Set.of("'", "\\")); + private final IdentifierProcessing identifierProcessing; protected MySqlDialect() { @@ -168,6 +178,11 @@ public OrderByNullPrecedence orderByNullHandling() { return OrderByNullPrecedence.NONE; } + @Override + public Escaper getStringLiteralEscaper() { + return STRING_LITERAL_ESCAPER; + } + @Override public UpsertRenderContext getUpsertRenderContext() { return UpsertRenderContexts.MYSQL; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/RenderContextFactory.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/RenderContextFactory.java index e518847f15..e4fbadbad1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/RenderContextFactory.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/RenderContextFactory.java @@ -126,6 +126,11 @@ public UpsertRenderContext getUpsertRenderContext() { return upsertRenderContext; } + @Override + public Escaper getStringLiteralEscaper() { + return renderingDialect.getStringLiteralEscaper(); + } + @Override public Dialect getDialect() { return renderingDialect; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java index 93cd64d964..4bbeda4e13 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java @@ -120,7 +120,7 @@ public LockClause lock() { @Override public Escaper getLikeEscaper() { - return Escaper.DEFAULT.withRewriteFor("[", "]"); + return Escaper.ANSI_LIKE_ESCAPER.withRewriteFor("[", "]"); } @Override diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java index 686a8f0775..31aba6c9d4 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Criteria.java @@ -572,7 +572,7 @@ private static String renderValue(@Nullable Object value) { } if (value instanceof ValueFunction) { - value = ((ValueFunction) value).apply(Escaper.DEFAULT); + value = ((ValueFunction) value).apply(Escaper.ANSI_LIKE_ESCAPER); } if (value != null) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index e74abba426..d761497862 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -125,8 +125,13 @@ Delegation enterMatched(Expression segment) { CaseExpressionVisitor visitor = new CaseExpressionVisitor(context); partRenderer = visitor; return Delegation.delegateTo(visitor); + } else if (segment instanceof StringLiteral stringLiteral) { + + CharSequence content = stringLiteral.getContent(); + String escaped = context.getStringLiteralEscaper().escape(content == null ? null : content.toString()); + value = "'" + (escaped == null ? "" : escaped) + "'"; } else { - // works for literals and just and possibly more + // works for `just` and possibly more value = segment.toString(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java index 4575cb4e72..cfbd5fe022 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java @@ -15,6 +15,9 @@ */ package org.springframework.data.relational.core.sql.render; +import java.util.Set; + +import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.dialect.InsertRenderContext; import org.springframework.data.relational.core.sql.IdentifierProcessing; @@ -60,4 +63,15 @@ public interface RenderContext { */ UpsertRenderContext getUpsertRenderContext(); + /** + * Returns the {@link Escaper} used for escaping the content of string literals. Defaults to the SQL standard of + * doubling a single quote ({@code '} → {@code ''}). + * + * @return the {@link Escaper} used for string literal escaping. + * @since 4.1 + */ + default Escaper getStringLiteralEscaper() { + return Escaper.of('\'', Set.of("'")); + } + } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/EscaperUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/EscaperUnitTests.java index a47f437db4..b158c7e915 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/EscaperUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/EscaperUnitTests.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.*; +import java.util.Set; + import org.junit.jupiter.api.Test; /** @@ -25,63 +27,95 @@ * @author Roman Chigvintsev * @author Mark Paluch * @author Alexander Tochin + * @author Jens Schauder */ public class EscaperUnitTests { @Test // DATAJDBC-514 public void ignoresNulls() { - assertThat((Escaper.DEFAULT.escape(null))).isNull(); + assertThat((Escaper.ANSI_LIKE_ESCAPER.escape(null))).isNull(); } @Test // DATAJDBC-514 public void ignoresEmptyString() { - assertThat(Escaper.DEFAULT.escape("")).isEmpty(); + assertThat(Escaper.ANSI_LIKE_ESCAPER.escape("")).isEmpty(); } @Test // DATAJDBC-514 public void ignoresBlankString() { - assertThat(Escaper.DEFAULT.escape(" ")).isEqualTo(" "); + assertThat(Escaper.ANSI_LIKE_ESCAPER.escape(" ")).isEqualTo(" "); } @Test // DATAJDBC-514 public void throwsExceptionWhenEscapeCharacterIsUnderscore() { - assertThatIllegalArgumentException().isThrownBy(() -> Escaper.of('_')); + assertThatIllegalArgumentException().isThrownBy(() -> Escaper.escapeLikeWith('_')); } @Test // DATAJDBC-514 public void throwsExceptionWhenEscapeCharacterIsPercent() { - assertThatIllegalArgumentException().isThrownBy(() -> Escaper.of('%')); + assertThatIllegalArgumentException().isThrownBy(() -> Escaper.escapeLikeWith('%')); } @Test // DATAJDBC-514 public void escapesUnderscoresUsingDefaultEscapeCharacter() { - assertThat(Escaper.DEFAULT.escape("_test_")).isEqualTo("\\_test\\_"); + assertThat(Escaper.ANSI_LIKE_ESCAPER.escape("_test_")).isEqualTo("\\_test\\_"); } @Test // DATAJDBC-514 public void escapesPercentsUsingDefaultEscapeCharacter() { - assertThat(Escaper.DEFAULT.escape("%test%")).isEqualTo("\\%test\\%"); + assertThat(Escaper.ANSI_LIKE_ESCAPER.escape("%test%")).isEqualTo("\\%test\\%"); } @Test // DATAJDBC-514 public void escapesSpecialCharactersUsingCustomEscapeCharacter() { - assertThat(Escaper.of('$').escape("_%")).isEqualTo("$_$%"); + assertThat(Escaper.escapeLikeWith('$').escape("_%")).isEqualTo("$_$%"); } @Test // DATAJDBC-514 public void escapesAdditionalCharacters() { - assertThat(Escaper.DEFAULT.withRewriteFor("[", "]").escape("Hello Wo[Rr]ld")).isEqualTo("Hello Wo\\[Rr\\]ld"); + assertThat(Escaper.ANSI_LIKE_ESCAPER.withRewriteFor("[", "]").escape("Hello Wo[Rr]ld")).isEqualTo("Hello Wo\\[Rr\\]ld"); } @Test // GH-2182 public void escapesCharactersUsingDefaultEscapeCharacter() { - assertThat(Escaper.DEFAULT.escape("%te\\st_")).isEqualTo("\\%te\\\\st\\_"); + assertThat(Escaper.ANSI_LIKE_ESCAPER.escape("%te\\st_")).isEqualTo("\\%te\\\\st\\_"); } @Test // GH-2182 public void escapesCharactersUsingCustomEscapeCharacter() { - assertThat(Escaper.DEFAULT.escape("%te\\st_")).isEqualTo("\\%te\\\\st\\_"); - assertThat(Escaper.of('$').escape("%te$st_")).isEqualTo("$%te$$st$_"); + assertThat(Escaper.ANSI_LIKE_ESCAPER.escape("%te\\st_")).isEqualTo("\\%te\\\\st\\_"); + assertThat(Escaper.escapeLikeWith('$').escape("%te$st_")).isEqualTo("$%te$$st$_"); + } + + @Test // GH-2325 + public void allowsEscapeCharacterToBePartOfCharactersToReplace() { + assertThatNoException().isThrownBy(() -> Escaper.of('\'', Set.of("'"))); + } + + @Test // GH-2325 + public void doublesSingleQuoteWhenEscapeCharacterIsSingleQuote() { + + Escaper escaper = Escaper.of('\'', Set.of("'")); + + assertThat(escaper.escape("O'Brien")).isEqualTo("O''Brien"); + assertThat(escaper.escape("''")).isEqualTo("''''"); + } + + @Test // GH-2325 + public void doesNotEscapeCharactersThatAreNotConfiguredToBeReplaced() { + + Escaper escaper = Escaper.of('\'', Set.of("'")); + + assertThat(escaper.escape("63%_is h_p")).isEqualTo("63%_is h_p"); + } + + @Test // GH-2325 + public void escapeCharacterIsHandledExactlyOnceWhenPartOfCharactersToReplace() { + + // step 1 doubles the escape character; step 2 must skip it so it is not doubled again + Escaper escaper = Escaper.of('\'', Set.of("'", "%")); + + assertThat(escaper.escape("a'b%c")).isEqualTo("a''b'%c"); } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectUnitTests.java index e97a44ca97..0d3c952260 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectUnitTests.java @@ -83,4 +83,14 @@ public void shouldRenderLock() { assertThat(lock.getLock(new LockOptions(LockMode.PESSIMISTIC_READ, from))).isEqualTo("LOCK IN SHARE MODE"); assertThat(lock.getClausePosition()).isEqualTo(LockClause.Position.AFTER_ORDER_BY); } + + @Test // GH-2325 + public void escapesStringLiteralsUsingBackslash() { + + Escaper escaper = new MySqlDialect().getStringLiteralEscaper(); + + assertThat(escaper.escape("O'Brien")).isEqualTo("O\\'Brien"); + assertThat(escaper.escape("back\\slash")).isEqualTo("back\\\\slash"); + assertThat(escaper.escape("50%_of it")).isEqualTo("50%_of it"); + } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java index 06251c891d..b0a437df80 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ExpressionVisitorUnitTests.java @@ -58,6 +58,8 @@ static List expressionsWithOutAliasGetRendered() { return asList( // fixture("String literal", SQL.literalOf("one"), "'one'"), // + fixture("String literal with single quote", SQL.literalOf("O'Brien"), "'O''Brien'"), // + fixture("String literal with only special LIKE characters", SQL.literalOf("50%_"), "'50%_'"), // fixture("Numeric literal", SQL.literalOf(23L), "23"), // fixture("Boolean literal", SQL.literalOf(true), "TRUE"), // fixture("Just", SQL.literalOf(Expressions.just("just an arbitrary String")), "just an arbitrary String"), // diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/ParameterMetadataProviderUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/ParameterMetadataProviderUnitTests.java index b5d99edfd6..0e18fc3043 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/ParameterMetadataProviderUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/ParameterMetadataProviderUnitTests.java @@ -46,7 +46,7 @@ public void shouldCreateValueFunctionForContains() throws Exception { assertThat(metadata.value()).isInstanceOf(ValueFunction.class); ValueFunction function = (ValueFunction) metadata.value(); - assertThat(function.apply(Escaper.DEFAULT)).isEqualTo("%hell\\%o%"); + assertThat(function.apply(Escaper.ANSI_LIKE_ESCAPER)).isEqualTo("%hell\\%o%"); } @Test // DATAJDBC-514 @@ -56,7 +56,7 @@ public void shouldCreateValueFunctionForStartingWith() throws Exception { assertThat(metadata.value()).isInstanceOf(ValueFunction.class); ValueFunction function = (ValueFunction) metadata.value(); - assertThat(function.apply(Escaper.DEFAULT)).isEqualTo("hell\\%o%"); + assertThat(function.apply(Escaper.ANSI_LIKE_ESCAPER)).isEqualTo("hell\\%o%"); } @Test // DATAJDBC-514 diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/RelationalExampleMapperTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/RelationalExampleMapperTests.java index c9ffe73ee0..9384d51f3a 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/RelationalExampleMapperTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/repository/query/RelationalExampleMapperTests.java @@ -195,7 +195,7 @@ void queryByExampleShouldEscapeSpecialCharactersInStringMatching() { assertThat(query.getCriteria().get().getGroup().get(0).getValue()).isInstanceOf(ValueFunction.class) .satisfies(it -> { - assertThat(((ValueFunction) it).apply(Escaper.of('!'))).isEqualTo("%!%"); + assertThat(((ValueFunction) it).apply(Escaper.escapeLikeWith('!'))).isEqualTo("%!%"); }); }