diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index f3d4ab5c9..32bf47913 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -409,6 +409,11 @@ public class ServerConfig { */ private boolean expressionEqualsWithNullAsNoop; + /** + * Set to true to use native ILIKE expression (if support by datasbase platform / like Postgres). + */ + private boolean expressionNativeIlike; + private String jodaLocalTimeMode; /** @@ -2391,6 +2396,7 @@ public class ServerConfig { changeLogIncludeInserts = p.getBoolean("changeLogIncludeInserts", changeLogIncludeInserts); expressionEqualsWithNullAsNoop = p.getBoolean("expressionEqualsWithNullAsNoop", expressionEqualsWithNullAsNoop); + expressionNativeIlike = p.getBoolean("expressionNativeIlike", expressionNativeIlike); dataTimeZone = p.get("dataTimeZone", dataTimeZone); asOfViewSuffix = p.get("asOfViewSuffix", asOfViewSuffix); @@ -2531,6 +2537,20 @@ public class ServerConfig { this.expressionEqualsWithNullAsNoop = expressionEqualsWithNullAsNoop; } + /** + * Return true if native ILIKE expression should be used if supported by the database platform (e.g. Postgres). + */ + public boolean isExpressionNativeIlike() { + return expressionNativeIlike; + } + + /** + * Set to true to use native ILIKE expression if supported by the database platform (e.g. Postgres). + */ + public void setExpressionNativeIlike(boolean expressionNativeIlike) { + this.expressionNativeIlike = expressionNativeIlike; + } + /** * Return true if L2 cache is disabled. */ diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java index a07651803..748d70976 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java @@ -170,6 +170,8 @@ public class DatabasePlatform { */ protected int maxConstraintNameLength = 60; + protected boolean supportsNativeIlike; + /** * Instantiates a new database platform. */ @@ -191,6 +193,13 @@ public class DatabasePlatform { return name; } + /** + * Return true if this database platform supports native ILIKE expression. + */ + public boolean isSupportsNativeIlike() { + return supportsNativeIlike; + } + /** * Return the maximum table name length. *

diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java index 17a537988..0654dfc50 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/PostgresPlatform.java @@ -20,6 +20,7 @@ public class PostgresPlatform extends DatabasePlatform { public PostgresPlatform() { super(); this.name = "postgres"; + this.supportsNativeIlike = true; this.likeClause = "like ? escape''"; this.selectCountWithAlias = true; this.blobDbType = Types.LONGVARBINARY; diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java index 373eba960..6cd87afd8 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/InternalConfiguration.java @@ -126,8 +126,9 @@ public class InternalConfiguration { this.cacheManager = cacheManager; this.serverConfig = serverConfig; this.bootupClasses = bootupClasses; - this.expressionFactory = new DefaultExpressionFactory(serverConfig.isExpressionEqualsWithNullAsNoop()); + DatabasePlatform databasePlatform = serverConfig.getDatabasePlatform(); + this.expressionFactory = initExpressionFactory(serverConfig, databasePlatform); this.typeManager = new DefaultTypeManager(serverConfig, bootupClasses); this.deployOrmXml = new DeployOrmXml(); @@ -140,13 +141,20 @@ public class InternalConfiguration { Map asOfTableMapping = beanDescriptorManager.deploy(); Map draftTableMap = beanDescriptorManager.getDraftTableMap(); - DatabasePlatform databasePlatform = serverConfig.getDatabasePlatform(); - this.dataTimeZone = initDataTimeZone(); this.binder = getBinder(typeManager, databasePlatform, dataTimeZone); this.cQueryEngine = new CQueryEngine(databasePlatform, binder, asOfTableMapping, serverConfig.getAsOfSysPeriod(), draftTableMap); } + /** + * Create and return the ExpressionFactory based on configuration and database platform. + */ + private ExpressionFactory initExpressionFactory(ServerConfig serverConfig, DatabasePlatform databasePlatform) { + + boolean nativeIlike = serverConfig.isExpressionNativeIlike() && databasePlatform.isSupportsNativeIlike(); + return new DefaultExpressionFactory(serverConfig.isExpressionEqualsWithNullAsNoop(), nativeIlike); + } + private DocStoreFactory initDocStoreFactory(DocStoreFactory service) { return service == null ? new NoneDocStoreFactory() : service; } @@ -324,10 +332,6 @@ public class InternalConfiguration { return cQueryEngine; } - public ClusterManager getClusterManager() { - return clusterManager; - } - public SpiBackgroundExecutor getBackgroundExecutor() { return backgroundExecutor; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java index 84db696b3..87465295f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactory.java @@ -28,10 +28,13 @@ public class DefaultExpressionFactory implements SpiExpressionFactory { private static final Object[] EMPTY_ARRAY = new Object[] {}; + private final boolean nativeIlike; + private final boolean equalsWithNullAsNoop; - public DefaultExpressionFactory(boolean equalsWithNullAsNoop) { + public DefaultExpressionFactory(boolean equalsWithNullAsNoop, boolean nativeIlike) { this.equalsWithNullAsNoop = equalsWithNullAsNoop; + this.nativeIlike = nativeIlike; } public ExpressionFactory createExpressionFactory(){ @@ -242,7 +245,11 @@ public class DefaultExpressionFactory implements SpiExpressionFactory { * a lower() function to make the expression case insensitive. */ public Expression ilike(String propertyName, String value) { - return new LikeExpression(propertyName, value, true, LikeType.RAW); + if (nativeIlike) { + return new NativeILikeExpression(propertyName, value); + } else { + return new LikeExpression(propertyName, value, true, LikeType.RAW); + } } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/NativeILikeExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/NativeILikeExpression.java new file mode 100644 index 000000000..5526fd145 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/NativeILikeExpression.java @@ -0,0 +1,79 @@ +package com.avaje.ebeaninternal.server.expression; + +import com.avaje.ebean.LikeType; +import com.avaje.ebeaninternal.api.HashQueryPlanBuilder; +import com.avaje.ebeaninternal.api.SpiExpression; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; +import com.avaje.ebeaninternal.server.el.ElPropertyValue; + +import java.io.IOException; + +class NativeILikeExpression extends AbstractExpression { + + private final String val; + + NativeILikeExpression(String propertyName, String value) { + super(propertyName); + this.val = value; + } + + @Override + public void writeDocQuery(DocQueryContext context) throws IOException { + context.writeLike(propName, val, LikeType.RAW, true); + } + + @Override + public void addBindValues(SpiExpressionRequest request) { + + ElPropertyValue prop = getElProp(request); + if (prop != null && prop.isDbEncrypted()) { + // bind the key as well as the value + String encryptKey = prop.getBeanProperty().getEncryptKey().getStringValue(); + request.addBindEncryptKey(encryptKey); + } + request.addBindValue(val); + } + + @Override + public void addSql(SpiExpressionRequest request) { + + String pname = propName; + ElPropertyValue prop = getElProp(request); + if (prop != null && prop.isDbEncrypted()) { + pname = prop.getBeanProperty().getDecryptProperty(propName); + } + + request.append(pname).append(" ilike ? "); + } + + /** + * Based on caseInsensitive and the property name. + */ + @Override + public void queryPlanHash(HashQueryPlanBuilder builder) { + builder.add(NativeILikeExpression.class).add(propName); + builder.bind(1); + } + + @Override + public int queryBindHash() { + return val.hashCode(); + } + + @Override + public boolean isSameByPlan(SpiExpression other) { + if (!(other instanceof NativeILikeExpression)) { + return false; + } + + NativeILikeExpression that = (NativeILikeExpression) other; + return this.propName.equals(that.propName); + } + + @Override + public boolean isSameByBind(SpiExpression other) { + NativeILikeExpression that = (NativeILikeExpression) other; + return val.equals(that.val); + } + +} diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactoryTest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactoryTest.java index 0b048117a..c19788624 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactoryTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionFactoryTest.java @@ -8,10 +8,26 @@ import static org.assertj.core.api.StrictAssertions.assertThat; public class DefaultExpressionFactoryTest { + @Test + public void testLowerILike() throws Exception { + + DefaultExpressionFactory factory = new DefaultExpressionFactory(false, false); + Expression expression = factory.ilike("name", "foo"); + assertThat(expression).isInstanceOf(LikeExpression.class); + } + + @Test + public void testNativeILike() throws Exception { + + DefaultExpressionFactory factory = new DefaultExpressionFactory(false, true); + Expression expression = factory.ilike("name", "foo"); + assertThat(expression).isInstanceOf(NativeILikeExpression.class); + } + @Test public void testEq() throws Exception { - DefaultExpressionFactory factory = new DefaultExpressionFactory(false); + DefaultExpressionFactory factory = new DefaultExpressionFactory(false, false); Expression expression = factory.eq("name", null); assertThat(expression).isInstanceOf(NullExpression.class); } @@ -19,7 +35,7 @@ public class DefaultExpressionFactoryTest { @Test public void testNe() throws Exception { - DefaultExpressionFactory factory = new DefaultExpressionFactory(false); + DefaultExpressionFactory factory = new DefaultExpressionFactory(false, false); Expression expression = factory.ne("name", null); assertThat(expression).isInstanceOf(NullExpression.class); } @@ -27,7 +43,7 @@ public class DefaultExpressionFactoryTest { @Test public void testIeq() throws Exception { - DefaultExpressionFactory factory = new DefaultExpressionFactory(false); + DefaultExpressionFactory factory = new DefaultExpressionFactory(false, false); Expression expression = factory.ieq("name", null); assertThat(expression).isInstanceOf(NullExpression.class); } @@ -35,7 +51,7 @@ public class DefaultExpressionFactoryTest { @Test public void testEq_with_equalsWithNullAsNoop() throws Exception { - DefaultExpressionFactory factory = new DefaultExpressionFactory(true); + DefaultExpressionFactory factory = new DefaultExpressionFactory(true, false); Expression expression = factory.eq("name", null); assertThat(expression).isInstanceOf(NoopExpression.class); } @@ -43,7 +59,7 @@ public class DefaultExpressionFactoryTest { @Test public void testNe_with_equalsWithNullAsNoop() throws Exception { - DefaultExpressionFactory factory = new DefaultExpressionFactory(true); + DefaultExpressionFactory factory = new DefaultExpressionFactory(true, false); Expression expression = factory.ne("name", null); assertThat(expression).isInstanceOf(NoopExpression.class); } @@ -51,7 +67,7 @@ public class DefaultExpressionFactoryTest { @Test public void testIeq_with_equalsWithNullAsNoop() throws Exception { - DefaultExpressionFactory factory = new DefaultExpressionFactory(true); + DefaultExpressionFactory factory = new DefaultExpressionFactory(true, false); Expression expression = factory.ieq("name", null); assertThat(expression).isInstanceOf(NoopExpression.class); } diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionListTest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionListTest.java index a5a688fd8..14e095c6c 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionListTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionListTest.java @@ -10,7 +10,7 @@ public class DefaultExpressionListTest { DefaultExpressionList exp() { - return new DefaultExpressionList(null, new DefaultExpressionFactory(true), null); + return new DefaultExpressionList(null, new DefaultExpressionFactory(true, true), null); } DefaultExpressionList spi(ExpressionList list) { diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/JunctionExpressionTest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/JunctionExpressionTest.java index 26610a0c4..fcd789555 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/expression/JunctionExpressionTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/JunctionExpressionTest.java @@ -16,7 +16,7 @@ public class JunctionExpressionTest { DefaultExpressionList exp(Expression... expressions) { - DefaultExpressionList list = new DefaultExpressionList(null, new DefaultExpressionFactory(true), null); + DefaultExpressionList list = new DefaultExpressionList(null, new DefaultExpressionFactory(true, false), null); for (Expression ex : expressions) { list.add(ex); } diff --git a/src/test/java/com/avaje/ebeaninternal/server/querydefn/TestQueryLanguage.java b/src/test/java/com/avaje/ebeaninternal/server/querydefn/TestQueryLanguage.java index 239ef2698..88714a5c5 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/querydefn/TestQueryLanguage.java +++ b/src/test/java/com/avaje/ebeaninternal/server/querydefn/TestQueryLanguage.java @@ -89,7 +89,7 @@ public class TestQueryLanguage extends BaseTestCase { BeanDescriptor desc = server.getBeanDescriptor(Order.class); DefaultOrmQuery qry = new DefaultOrmQuery(desc, server, - new DefaultExpressionFactory(false), (String) null); + new DefaultExpressionFactory(false, false), (String) null); p.assign(qry); return qry; diff --git a/src/test/java/com/avaje/tests/query/sqlquery/TestNativeILikeExpression.java b/src/test/java/com/avaje/tests/query/sqlquery/TestNativeILikeExpression.java new file mode 100644 index 000000000..ff07ddf86 --- /dev/null +++ b/src/test/java/com/avaje/tests/query/sqlquery/TestNativeILikeExpression.java @@ -0,0 +1,44 @@ +package com.avaje.tests.query.sqlquery; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.Query; +import com.avaje.ebean.plugin.SpiServer; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.ResetBasicData; +import org.junit.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestNativeILikeExpression extends BaseTestCase { + + @Test + public void test() { + + boolean expectNative = isExpectNative(); + + ResetBasicData.reset(); + + Query query = Ebean.find(Customer.class) + .where().ilike("name", "rob") + .query(); + + List list = query.findList(); + + if (expectNative) { + assertThat(query.getGeneratedSql()).contains(" from o_customer t0 where t0.name ilike ?"); + assertThat(list).isNotEmpty(); + } + } + + private boolean isExpectNative() { + + SpiServer pluginApi = server().getPluginApi(); + boolean expressionNativeIlike = pluginApi.getServerConfig().isExpressionNativeIlike(); + String platformName = pluginApi.getDatabasePlatform().getName(); + + return expressionNativeIlike && platformName.equalsIgnoreCase("postgres"); + } +} diff --git a/src/test/resources/ebean.properties b/src/test/resources/ebean.properties index 524fdff6b..e5fa48cef 100644 --- a/src/test/resources/ebean.properties +++ b/src/test/resources/ebean.properties @@ -23,7 +23,7 @@ ebean.persistBatch=NONE ebean.persistBatchOnCascade=ALL ebean.currentUserProvider=com.avaje.ebean.MyCurrentUserProvider - +#ebean.expressionNativeIlike=true #ebean.jsonInclude=NON_NULL #ebean.cluster.type=mcast