FIX: correct behavior of "startsWith, "contains" etc (#1031)

* FIX: correct behavior of "startsWith" if string contains "like"-wildcards

* Removed likeClasues with disabled escaping in database platforms as bind values are properly escaped now.
This commit is contained in:
Roland Praml
2017-06-19 22:21:26 +12:00
committed by Rob Bygrave
parent a1f95bf25e
commit bc74d35651
13 changed files with 162 additions and 15 deletions
+4 -4
View File
@@ -6,22 +6,22 @@ package io.ebean;
public enum LikeType {
/**
* You need to put in your own wildcards.
* You need to put in your own wildcards - no escaping is performed.
*/
RAW,
/**
* The % wildcard is added to the end of the search word.
* The % wildcard is added to the end of the search word and search word is escaped.
*/
STARTS_WITH,
/**
* The % wildcard is added to the beginning of the search word.
* The % wildcard is added to the beginning of the search word and search word is escaped.
*/
ENDS_WITH,
/**
* The % wildcard is added to the beginning and end of the search word.
* The % wildcard is added to the beginning and end of the search word and search word is escaped.
*/
CONTAINS,
@@ -174,6 +174,8 @@ public class DatabasePlatform {
protected boolean supportsNativeIlike;
protected SqlExceptionTranslator exceptionTranslator = new SqlCodeTranslator();
protected char[] specialLikeCharacters = { '%', '_' };
/**
* Instantiates a new database platform.
@@ -617,4 +619,37 @@ public class DatabasePlatform {
logger.error("Error closing resultSet", e);
}
}
/**
* Escapes the like string for this DB-Platform
*/
public String escapeLikeString(String value) {
StringBuilder sb = null;
for (int i = 0; i < value.length(); i++) {
char ch = value.charAt(i);
boolean escaped = false;
for (char escapeChar: specialLikeCharacters) {
if (ch == escapeChar) {
if (sb == null) {
sb = new StringBuilder(value.substring(0, i));
}
escapeLikeCharacter(escapeChar, sb);
escaped = true;
break;
}
}
if (!escaped && sb != null) {
sb.append(ch);
}
}
if (sb == null) {
return value;
} else {
return sb.toString();
}
}
protected void escapeLikeCharacter(char ch, StringBuilder sb) {
sb.append('\\').append(ch);
}
}
@@ -40,10 +40,6 @@ public class H2Platform extends DatabasePlatform {
this.dbIdentity.setSupportsSequence(true);
this.dbIdentity.setSupportsIdentity(true);
// like ? escape'' not working in the latest version H2 so just using no
// escape clause for now noting that backslash is an escape char for like in H2
this.likeClause = "like ?";
dbTypeMap.put(DbType.UUID, new DbPlatformType("uuid", false));
}
@@ -30,7 +30,6 @@ public class MySqlPlatform extends DatabasePlatform {
super();
this.platform = Platform.MYSQL;
this.useExtraTransactionOnIterateSecondaryQueries = true;
this.likeClause = "like ? escape''";
this.selectCountWithAlias = true;
this.dbEncrypt = new MySqlDbEncrypt();
this.platformDdl = new MySqlDdl(this);
@@ -28,7 +28,6 @@ public class PostgresPlatform extends DatabasePlatform {
super();
this.platform = Platform.POSTGRES;
this.supportsNativeIlike = true;
this.likeClause = "like ? escape''";
this.selectCountWithAlias = true;
this.blobDbType = Types.LONGVARBINARY;
this.clobDbType = Types.VARCHAR;
@@ -41,6 +41,8 @@ public class SqlServerPlatform extends DatabasePlatform {
this.openQuote = "[";
this.closeQuote = "]";
this.specialLikeCharacters = new char[] { '%', '_', '[' };
this.likeClause = "like ? COLLATE Latin1_General_BIN";
booleanDbType = Types.INTEGER;
dbTypeMap.put(DbType.BOOLEAN, new DbPlatformType("bit default 0"));
@@ -63,4 +65,9 @@ public class SqlServerPlatform extends DatabasePlatform {
}
@Override
protected void escapeLikeCharacter(char ch, StringBuilder sb) {
sb.append('[').append(ch).append(']');
}
}
@@ -65,4 +65,9 @@ public interface SpiExpressionRequest {
* Append a DB Like clause.
*/
void appendLike();
/**
* Escapes a string to use it as exact match in Like clause.
*/
String escapeLikeString(String value);
}
@@ -96,6 +96,14 @@ public final class OrmQueryRequest<T> extends BeanRequest implements BeanQueryRe
public String getDBLikeClause() {
return ebeanServer.getDatabasePlatform().getLikeClause();
}
/**
* Return the database platform escaped like string.
*/
@Override
public String escapeLikeString(String value) {
return ebeanServer.getDatabasePlatform().escapeLikeString(value);
}
@Override
public void executeSecondaryQueries(boolean forEach) {
@@ -123,6 +123,11 @@ public interface SpiOrmQueryRequest<T> extends DocQueryRequest<T> {
* Return the Database platform like clause.
*/
String getDBLikeClause();
/**
* Escapes a string to use it as exact match in Like clause.
*/
String escapeLikeString(String value);
/**
* Mark the underlying transaction as not being query only.
@@ -93,6 +93,11 @@ public class DefaultExpressionRequest implements SpiExpressionRequest {
sql.append(" ");
}
@Override
public String escapeLikeString(String value) {
return queryRequest.escapeLikeString(value);
}
/**
* Increments the parameter index and returns that value.
*/
@@ -34,8 +34,7 @@ class LikeExpression extends AbstractValueExpression {
String encryptKey = prop.getBeanProperty().getEncryptKey().getStringValue();
request.addBindEncryptKey(encryptKey);
}
String bindValue = getValue(strValue(), caseInsensitive, type);
String bindValue = getValue(strValue(), caseInsensitive, type, request);
request.addBindValue(bindValue);
}
@@ -92,13 +91,15 @@ class LikeExpression extends AbstractValueExpression {
return strValue().equals(that.strValue());
}
private static String getValue(String value, boolean caseInsensitive, LikeType type) {
private static String getValue(String value, boolean caseInsensitive, LikeType type, SpiExpressionRequest request) {
if (caseInsensitive) {
value = value.toLowerCase();
}
if (type == LikeType.RAW) {
return value;
}
value = request.escapeLikeString(value);
switch (type) {
case RAW:
return value;
case STARTS_WITH:
return value + "%";
case ENDS_WITH:
@@ -78,4 +78,9 @@ public class TDSpiExpressionRequest implements SpiExpressionRequest {
public void appendLike() {
}
@Override
public String escapeLikeString(String value) {
return value;
}
}
@@ -0,0 +1,82 @@
package org.tests.query.other;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import org.tests.model.basic.Customer;
import org.tests.model.basic.ResetBasicData;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class TestLikeEscaping extends BaseTestCase {
@Test
public void testLikeEscaping() {
ResetBasicData.reset();
// Mysql, PgSql, H2 special chars: "_" "%"
// MsSql special chars: "_" "%" "[" AND different Quoting!
Ebean.save(ResetBasicData.createCustomer("Paul % Percentage", "*Star", "[none]", 0, null));
Ebean.save(ResetBasicData.createCustomer("(none)", "More * Star", "[none]", 0, null));
Ebean.save(ResetBasicData.createCustomer("Paul %% Doublepercentage", "|Pipeway", "[other]", 1, null));
Ebean.save(ResetBasicData.createCustomer("_Udo Underscore", "|Pipeway", "[other]", 1, null));
assertThat(Ebean.find(Customer.class)
.where().contains("name", "Paul %%").findCount()
).isEqualTo(1);
assertThat(Ebean.find(Customer.class)
.where().startsWith("name", "_").findCount()
).isEqualTo(1);
assertThat(Ebean.find(Customer.class)
.where().startsWith("name", "_u").findCount()
).isEqualTo(0);
assertThat(Ebean.find(Customer.class)
.where().istartsWith("name", "_U").findCount()
).isEqualTo(1);
assertThat(Ebean.find(Customer.class)
.where().startsWith("shippingAddress.line1", "|p").findCount()
).isEqualTo(0);
assertThat(Ebean.find(Customer.class)
.where().startsWith("shippingAddress.line1", "|P").findCount()
).isEqualTo(2);
assertThat(Ebean.find(Customer.class)
.where().endsWith("billingAddress.line1", "]").findCount()
).isEqualTo(4);
assertThat(Ebean.find(Customer.class)
.where().endsWith("billingAddress.line1", "[none]").findCount()
).isEqualTo(2);
assertThat(Ebean.find(Customer.class)
.where().startsWith("billingAddress.line1", "[none]").findCount()
).isEqualTo(2);
assertThat(Ebean.find(Customer.class)
.where().contains("billingAddress.line1", "[none]").findCount()
).isEqualTo(2);
assertThat(Ebean.find(Customer.class)
.where().contains("shippingAddress.line1", "*").findCount()
).isEqualTo(2);
assertThat(Ebean.find(Customer.class)
.where().startsWith("shippingAddress.line1", "*").findCount()
).isEqualTo(1);
assertThat(Ebean.find(Customer.class)
.where().endsWith("shippingAddress.line1", "*").findCount()
).isEqualTo(0);
}
}