diff --git a/pom.xml b/pom.xml index f6f8034d1..99d20c71a 100644 --- a/pom.xml +++ b/pom.xml @@ -219,7 +219,7 @@ com.h2database h2 - 1.4.193 + 1.4.196 provided diff --git a/src/main/java/io/ebean/event/BeanQueryRequest.java b/src/main/java/io/ebean/event/BeanQueryRequest.java index 4ba1e395e..ce327e67c 100644 --- a/src/main/java/io/ebean/event/BeanQueryRequest.java +++ b/src/main/java/io/ebean/event/BeanQueryRequest.java @@ -24,4 +24,13 @@ public interface BeanQueryRequest { */ Query getQuery(); + /** + * Return true if multi-value binding using Array or Table Values is supported. + */ + boolean isMultiValueIdSupported(); + + /** + * Return true if multi-value binding is supported for this value type. + */ + boolean isMultiValueSupported(Class> valueType); } diff --git a/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java b/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java index eaa2acb49..88b3881bd 100644 --- a/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java +++ b/src/main/java/io/ebeaninternal/api/LoadBeanRequest.java @@ -103,7 +103,8 @@ public class LoadBeanRequest extends LoadRequest { idList.add(desc.getId(bean)); } - if (!idList.isEmpty()) { + + if (!desc.isMultiValueIdSupported() && !idList.isEmpty()) { int extraIds = batchSize - batch.size(); if (extraIds > 0) { // for performance make up the Id's to the batch size diff --git a/src/main/java/io/ebeaninternal/api/LoadManyRequest.java b/src/main/java/io/ebeaninternal/api/LoadManyRequest.java index f73af11aa..d92ce3222 100644 --- a/src/main/java/io/ebeaninternal/api/LoadManyRequest.java +++ b/src/main/java/io/ebeaninternal/api/LoadManyRequest.java @@ -107,11 +107,13 @@ public class LoadManyRequest extends LoadRequest { for (BeanCollection> bc : batch) { idList.add(many.getParentId(bc.getOwnerBean())); } - int extraIds = batchSize - batch.size(); - if (extraIds > 0) { - Object firstId = idList.get(0); - for (int i = 0; i < extraIds; i++) { - idList.add(firstId); + if (!many.getTargetDescriptor().isMultiValueIdSupported()) { + int extraIds = batchSize - batch.size(); + if (extraIds > 0) { + Object firstId = idList.get(0); + for (int i = 0; i < extraIds; i++) { + idList.add(firstId); + } } } diff --git a/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java b/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java index 09487e833..0f8078c9d 100644 --- a/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java @@ -70,4 +70,9 @@ public interface SpiExpressionRequest { * Escapes a string to use it as exact match in Like clause. */ String escapeLikeString(String value); + + /** + * Append IN expression taking into account platform and type support for Multi-value. + */ + void appendInExpression(boolean not, Object[] bindValues); } diff --git a/src/main/java/io/ebeaninternal/server/core/BeanRequest.java b/src/main/java/io/ebeaninternal/server/core/BeanRequest.java index 909ba0ecf..f3a3e5d2a 100644 --- a/src/main/java/io/ebeaninternal/server/core/BeanRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/BeanRequest.java @@ -12,7 +12,7 @@ import org.slf4j.LoggerFactory; */ public abstract class BeanRequest { - private static final Logger log = LoggerFactory.getLogger(BeanRequest.class); + static final Logger log = LoggerFactory.getLogger(BeanRequest.class); /** * The server processing the request. diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultBeanLoader.java b/src/main/java/io/ebeaninternal/server/core/DefaultBeanLoader.java index 9bc4b2ab1..828bfd361 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultBeanLoader.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultBeanLoader.java @@ -182,7 +182,6 @@ public class DefaultBeanLoader { } } - /** * Load a batch of beans for +query or +lazy loading. */ diff --git a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index 68e8f1a21..3b46d59c1 100644 --- a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -7,9 +7,9 @@ import io.ebean.cache.ServerCacheManager; import io.ebean.config.ExternalTransactionManager; import io.ebean.config.ProfilingConfig; import io.ebean.config.ServerConfig; +import io.ebean.config.SlowQueryListener; import io.ebean.config.dbplatform.DatabasePlatform; import io.ebean.config.dbplatform.DbHistorySupport; -import io.ebean.config.SlowQueryListener; import io.ebean.event.changelog.ChangeLogListener; import io.ebean.event.changelog.ChangeLogPrepare; import io.ebean.event.changelog.ChangeLogRegister; @@ -43,6 +43,9 @@ import io.ebeaninternal.server.deploy.parse.DeployUtil; import io.ebeaninternal.server.expression.DefaultExpressionFactory; import io.ebeaninternal.server.persist.Binder; import io.ebeaninternal.server.persist.DefaultPersister; +import io.ebeaninternal.server.persist.platform.H2MultiValueBind; +import io.ebeaninternal.server.persist.platform.MultiValueBind; +import io.ebeaninternal.server.persist.platform.PostgresMultiValueBind; import io.ebeaninternal.server.query.CQueryEngine; import io.ebeaninternal.server.query.DefaultOrmQueryEngine; import io.ebeaninternal.server.query.DefaultRelationalQueryEngine; @@ -122,6 +125,8 @@ public class InternalConfiguration { */ private final List plugins = new ArrayList<>(); + private final MultiValueBind multiValueBind; + public InternalConfiguration(ClusterManager clusterManager, SpiCacheManager cacheManager, SpiBackgroundExecutor backgroundExecutor, ServerConfig serverConfig, BootupClasses bootupClasses) { @@ -138,6 +143,7 @@ public class InternalConfiguration { this.expressionFactory = initExpressionFactory(serverConfig, databasePlatform); this.typeManager = new DefaultTypeManager(serverConfig, bootupClasses); + this.multiValueBind = createMultiValueBind(databasePlatform.getPlatform()); this.deployInherit = new DeployInherit(bootupClasses); this.deployCreateProperties = new DeployCreateProperties(typeManager); @@ -245,9 +251,9 @@ public class InternalConfiguration { DbHistorySupport historySupport = databasePlatform.getHistorySupport(); if (historySupport == null) { - return new Binder(typeManager, 0, false, jsonHandler, dataTimeZone); + return new Binder(typeManager, 0, false, jsonHandler, dataTimeZone, multiValueBind); } - return new Binder(typeManager, historySupport.getBindCount(), historySupport.isStandardsBased(), jsonHandler, dataTimeZone); + return new Binder(typeManager, historySupport.getBindCount(), historySupport.isStandardsBased(), jsonHandler, dataTimeZone, multiValueBind); } /** @@ -267,6 +273,21 @@ public class InternalConfiguration { } } + private MultiValueBind createMultiValueBind(Platform platform) { + switch (platform) { + case POSTGRES: + return new PostgresMultiValueBind(); +// case H2: +// return new H2MultiValueBind(); +// case SQLSERVER: +// return new SqlServerTvpMultiValueHelp(); +// case ORACLE: +// return new OracleTvpMultiValueHelp(); + default: + return new MultiValueBind(); + } + } + public SpiJsonContext createJsonContext(SpiEbeanServer server) { return new DJsonContext(server, jsonFactory, typeManager); } @@ -280,7 +301,7 @@ public class InternalConfiguration { } public OrmQueryEngine createOrmQueryEngine() { - return new DefaultOrmQueryEngine(cQueryEngine); + return new DefaultOrmQueryEngine(cQueryEngine, binder); } public Persister createPersister(SpiEbeanServer server) { @@ -478,4 +499,11 @@ public class InternalConfiguration { } return listener; } + + /** + * Return the platform specific MultiValue bind support. + */ + public MultiValueBind getMultiValueBind() { + return multiValueBind; + } } diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryEngine.java index 76c5875c8..aeda6a8ad 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryEngine.java @@ -62,4 +62,9 @@ public interface OrmQueryEngine { * Translate the SQLException to a specific persistence exception type if possible. */ PersistenceException translate(OrmQueryRequest request, String bindLog, String sql, SQLException e); + + /** + * Return true if multi-value bind is supported for this type (and current platform). + */ + boolean isMultiValueSupported(Class> valueType); } diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index 6dff33aec..e51516a24 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -89,6 +89,16 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe return queryEngine.translate(this, bindLog, sql, e); } + @Override + public boolean isMultiValueIdSupported() { + return beanDescriptor.isMultiValueIdSupported(); + } + + @Override + public boolean isMultiValueSupported(Class> valueType) { + return queryEngine.isMultiValueSupported(valueType); + } + /** * Mark the transaction as not being query only. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 11c559c50..3469bb0c5 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -49,6 +49,7 @@ import io.ebeaninternal.server.core.InternString; import io.ebeaninternal.server.core.PersistRequest; import io.ebeaninternal.server.core.PersistRequestBean; import io.ebeaninternal.server.deploy.id.IdBinder; +import io.ebeaninternal.server.deploy.id.IdBinderSimple; import io.ebeaninternal.server.deploy.id.ImportedId; import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyLists; @@ -116,6 +117,8 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { private final short profileBeanId; + private final boolean multiValueSupported; + public enum EntityType { ORM, EMBEDDED, VIEW, SQL, DOC } @@ -402,6 +405,7 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { public BeanDescriptor(BeanDescriptorMap owner, DeployBeanDescriptor deploy) { this.owner = owner; + this.multiValueSupported = owner.isMultiValueSupported(); this.serverName = owner.getServerName(); this.entityType = deploy.getEntityType(); this.properties = deploy.getProperties(); @@ -935,9 +939,7 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { sb.append(inClause); DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString()); - for (Object anIdList : idList) { - idBinder.bindId(delete, anIdList); - } + idBinder.addIdInBindValues(delete, idList); return delete; } @@ -1661,7 +1663,7 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { * Return a raw expression for 'where parent id in ...' clause. */ public String getParentIdInExpr(int parentIdSize, String rawWhere) { - String inClause = idBinder.getIdInValueExpr(parentIdSize); + String inClause = idBinder.getIdInValueExpr(false, parentIdSize); return idBinder.isIdInExpandedForm() ? inClause : rawWhere + inClause; } @@ -1672,6 +1674,20 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { return idBinder; } + /** + * Return true if this bean type has a simple single Id property. + */ + public boolean isSimpleId() { + return idBinder instanceof IdBinderSimple; + } + + /** + * Return true if this type has a simple Id and the platform supports mutli-value binding. + */ + public boolean isMultiValueIdSupported() { + return multiValueSupported && isSimpleId(); + } + /** * Return the sql for binding an id. This is the columns with table alias that * make up the id. diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 75cecdcef..72d29e20d 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -45,6 +45,7 @@ import io.ebeaninternal.server.deploy.parse.DeployInherit; import io.ebeaninternal.server.deploy.parse.DeployUtil; import io.ebeaninternal.server.deploy.parse.ReadAnnotations; import io.ebeaninternal.server.deploy.parse.TransientProperties; +import io.ebeaninternal.server.persist.platform.MultiValueBind; import io.ebeaninternal.server.properties.BeanPropertiesReader; import io.ebeaninternal.server.properties.BeanPropertyAccess; import io.ebeaninternal.server.properties.EnhanceBeanPropertyAccess; @@ -131,6 +132,8 @@ public class BeanDescriptorManager implements BeanDescriptorMap { private final DocStoreFactory docStoreFactory; + private final MultiValueBind multiValueBind; + private int entityBeanCount; private final boolean updateChangesOnly; @@ -201,7 +204,8 @@ public class BeanDescriptorManager implements BeanDescriptorMap { this.dataSource = serverConfig.getDataSource(); this.encryptKeyManager = serverConfig.getEncryptKeyManager(); this.databasePlatform = serverConfig.getDatabasePlatform(); - this.idBinderFactory = new IdBinderFactory(databasePlatform.isIdInExpandedForm()); + this.multiValueBind = config.getMultiValueBind(); + this.idBinderFactory = new IdBinderFactory(databasePlatform.isIdInExpandedForm(), multiValueBind); this.eagerFetchLobs = serverConfig.isEagerFetchLobs(); this.asOfViewSuffix = getAsOfViewSuffix(databasePlatform, serverConfig); @@ -252,6 +256,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap { return (historySupport == null) ? serverConfig.getAsOfViewSuffix() : historySupport.getVersionsBetweenSuffix(serverConfig.getAsOfViewSuffix()); } + @Override + public boolean isMultiValueSupported() { + return multiValueBind.isSupported(); + } + @Override public ServerConfig getServerConfig() { return serverConfig; diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java index d01664bc2..2cb357a2e 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorMap.java @@ -36,6 +36,11 @@ public interface BeanDescriptorMap { */ NamingConvention getNamingConvention(); + /** + * Return true if multiple values can be bound as Array or Table Value and hence share the same query plan. + */ + boolean isMultiValueSupported(); + /** * Return the BeanDescriptor for a given class. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanFkeyProperty.java b/src/main/java/io/ebeaninternal/server/deploy/BeanFkeyProperty.java index c2545d06f..c8e8b3a8a 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanFkeyProperty.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanFkeyProperty.java @@ -137,7 +137,7 @@ public final class BeanFkeyProperty implements ElPropertyValue { * Returns null as not an AssocOne. */ @Override - public String getAssocIdInValueExpr(int size) { + public String getAssocIdInValueExpr(boolean not, int size) { return null; } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index fd78177a7..c2a720481 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -909,7 +909,7 @@ public class BeanProperty implements ElPropertyValue, Property { } @Override - public String getAssocIdInValueExpr(int size) { + public String getAssocIdInValueExpr(boolean not, int size) { // Returns null as not an AssocOne. return null; } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index fd7024ac6..897d1ab8c 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -18,6 +18,7 @@ import io.ebeaninternal.server.deploy.id.ImportedId; import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; import io.ebeaninternal.server.el.ElPropertyChainBuilder; import io.ebeaninternal.server.el.ElPropertyValue; +import io.ebeaninternal.server.persist.MultiValueWrapper; import io.ebeaninternal.server.query.SqlBeanLoad; import io.ebeaninternal.server.text.json.ReadJson; import io.ebeaninternal.server.text.json.SpiJsonWriter; @@ -366,8 +367,11 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { // Flatten the bind values if needed (embeddedId) List bindValues = getBindParentIds(parentIds); - - query.where().raw(expr, bindValues.toArray()); + if (descriptor.isSimpleId()) { + query.where().raw(expr, new MultiValueWrapper(bindValues)); + } else { + query.where().raw(expr, bindValues.toArray()); + } } private List findIdsByParentIdList(List parentIdList, Transaction t, ArrayList excludeDetailIds) { @@ -383,10 +387,12 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { } EbeanServer server = getBeanDescriptor().getEbeanServer(); - Query> q = server.find(getPropertyType()) - .where() - .raw(expr, bindValues.toArray()) - .query(); + Query> q = server.find(getPropertyType()); + if (descriptor.isSimpleId()) { + q.where().raw(expr, new MultiValueWrapper(bindValues)); + } else { + q.where().raw(expr, bindValues.toArray()); + } if (excludeDetailIds != null && !excludeDetailIds.isEmpty()) { Expression idIn = q.getExpressionFactory().idIn(excludeDetailIds); @@ -405,10 +411,13 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { sb.append(inClause); DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString()); - for (Object aParentIdist : parentIdist) { - bindWhereParendId(delete, aParentIdist); + if (exportedProperties.length == 1) { + bindWhereParendId(delete, new MultiValueWrapper(parentIdist)); + } else { + for (Object aParentIdist : parentIdist) { + bindWhereParendId(delete, aParentIdist); + } } - return delete; } @@ -430,9 +439,11 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { private String buildInClauseBinding(int size, String bindProto) { + if (descriptor.isSimpleId()) { + return descriptor.getIdBinder().getIdInValueExpr(false, size); + } StringBuilder sb = new StringBuilder(10 + (size * (bindProto.length() + 1))); sb.append(" in"); - sb.append(" ("); for (int i = 0; i < size; i++) { if (i > 0) { @@ -534,8 +545,8 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc { * Return the logical id value expression taking into account embedded id's. */ @Override - public String getAssocIdInValueExpr(int size) { - return targetDescriptor.getIdBinder().getIdInValueExpr(size); + public String getAssocIdInValueExpr(boolean not, int size) { + return targetDescriptor.getIdBinder().getIdInValueExpr(not, size); } /** diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index f4679cb19..fa845bad7 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -207,7 +207,7 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { StringBuilder sb = new StringBuilder(100); sb.append(deleteByParentIdInSql); - String inClause = targetIdBinder.getIdInValueExpr(parentIdist.size()); + String inClause = targetIdBinder.getIdInValueExpr(false, parentIdist.size()); sb.append(inClause); DefaultSqlUpdate delete = new DefaultSqlUpdate(sb.toString()); @@ -256,7 +256,7 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { private List findIdsByParentIdList(List parentIdList, Transaction t) { String rawWhere = deriveWhereParentIdSql(true); - String inClause = targetIdBinder.getIdInValueExpr(parentIdList.size()); + String inClause = targetIdBinder.getIdInValueExpr(false, parentIdList.size()); String expr = rawWhere + inClause; @@ -445,8 +445,8 @@ public class BeanPropertyAssocOne extends BeanPropertyAssoc { * Return the logical id value expression taking into account embedded id's. */ @Override - public String getAssocIdInValueExpr(int size) { - return targetDescriptor.getIdBinder().getIdInValueExpr(size); + public String getAssocIdInValueExpr(boolean not, int size) { + return targetDescriptor.getIdBinder().getIdInValueExpr(not, size); } /** diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java index 95212c98a..766f9eb60 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinder.java @@ -12,6 +12,7 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.Collection; import java.util.List; /** @@ -134,7 +135,15 @@ public interface IdBinder { */ void bindId(DefaultSqlUpdate sqlUpdate, Object value); - void addIdInBindValue(SpiExpressionRequest request, Object value); + /** + * Binds multiple id value to an update. + */ + void addIdInBindValues(DefaultSqlUpdate sqlUpdate, Collection> ids); + + /** + * Binds multiple id value to a request. + */ + void addIdInBindValues(SpiExpressionRequest request, Collection> ids); /** * Return the sql for binding the id using an IN clause. @@ -144,7 +153,7 @@ public interface IdBinder { /** * Return the binding expression (like "?" or "(?,?)")for the Id. */ - String getIdInValueExpr(int size); + String getIdInValueExpr(boolean not, int size); /** * Same as getIdInValueExpr but for delete by id. diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java index caf0fcbe1..5013b2d9e 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmbedded.java @@ -1,6 +1,7 @@ package io.ebeaninternal.server.deploy.id; import io.ebean.bean.EntityBean; +import io.ebean.util.SplitName; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.server.core.DefaultSqlUpdate; import io.ebeaninternal.server.deploy.BeanDescriptor; @@ -8,13 +9,13 @@ import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; import io.ebeaninternal.server.deploy.DbReadContext; import io.ebeaninternal.server.deploy.DbSqlContext; -import io.ebean.util.SplitName; import io.ebeaninternal.server.type.DataBind; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -163,20 +164,13 @@ public final class IdBinderEmbedded implements IdBinder { return props; } - @Override - public void addIdInBindValue(SpiExpressionRequest request, Object value) { - for (BeanProperty prop : props) { - request.addBindValue(prop.getValue((EntityBean) value)); - } - } - @Override public String getIdInValueExprDelete(int size) { if (size <= 0) { throw new IndexOutOfBoundsException("The size must be at least 1"); } if (!idInExpandedForm) { - return getIdInValueExpr(size); + return getIdInValueExpr(false, size); } StringBuilder sb = new StringBuilder(); @@ -201,12 +195,14 @@ public final class IdBinderEmbedded implements IdBinder { } @Override - public String getIdInValueExpr(int size) { + public String getIdInValueExpr(boolean not, int size) { if (size <= 0) { throw new IndexOutOfBoundsException("The size must be at least 1"); } StringBuilder sb = new StringBuilder(); - + if (not) { + sb.append(" not"); + } if (!idInExpandedForm) { sb.append(" in"); } @@ -294,6 +290,22 @@ public final class IdBinderEmbedded implements IdBinder { } } + @Override + public void addIdInBindValues(DefaultSqlUpdate sqlUpdate, Collection> values) { + for (Object value : values) { + bindId(sqlUpdate, value); + } + } + + @Override + public void addIdInBindValues(SpiExpressionRequest request, Collection> values) { + for (Object value : values) { + for (BeanProperty prop : props) { + request.addBindValue(prop.getValue((EntityBean) value)); + } + } + } + @Override public Object readData(DataInput dataInput) throws IOException { diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java index 03ff6ecb0..01052b076 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderEmpty.java @@ -12,6 +12,7 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.Collection; import java.util.List; /** @@ -85,18 +86,13 @@ public final class IdBinderEmpty implements IdBinder { return null; } - @Override - public void addIdInBindValue(SpiExpressionRequest request, Object value) { - - } - @Override public String getIdInValueExprDelete(int size) { - return getIdInValueExpr(size); + return getIdInValueExpr(false, size); } @Override - public String getIdInValueExpr(int size) { + public String getIdInValueExpr(boolean not, int size) { return ""; } @@ -135,6 +131,16 @@ public final class IdBinderEmpty implements IdBinder { } + @Override + public void addIdInBindValues(DefaultSqlUpdate sqlUpdate, Collection> ids) { + + } + + @Override + public void addIdInBindValues(SpiExpressionRequest request, Collection> ids) { + + } + @Override public void loadIgnore(DbReadContext ctx) { } diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderFactory.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderFactory.java index 00793db0d..d5140f589 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderFactory.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderFactory.java @@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy.id; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; +import io.ebeaninternal.server.persist.platform.MultiValueBind; /** * Creates the appropriate IdConvertSet depending on the type of Id property(s). @@ -12,8 +13,11 @@ public class IdBinderFactory { private final boolean idInExpandedForm; - public IdBinderFactory(boolean idInExpandedForm) { + private final MultiValueBind multiValueBind; + + public IdBinderFactory(boolean idInExpandedForm, MultiValueBind multiValueBind) { this.idInExpandedForm = idInExpandedForm; + this.multiValueBind = multiValueBind; } /** @@ -29,7 +33,7 @@ public class IdBinderFactory { if (id.isEmbedded()) { return new IdBinderEmbedded(idInExpandedForm, (BeanPropertyAssocOne>) id); } else { - return new IdBinderSimple(id); + return new IdBinderSimple(id, multiValueBind); } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java index c87eee837..a4da8e3f2 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java +++ b/src/main/java/io/ebeaninternal/server/deploy/id/IdBinderSimple.java @@ -7,6 +7,8 @@ import io.ebeaninternal.server.core.InternString; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.DbReadContext; import io.ebeaninternal.server.deploy.DbSqlContext; +import io.ebeaninternal.server.persist.MultiValueWrapper; +import io.ebeaninternal.server.persist.platform.MultiValueBind; import io.ebeaninternal.server.type.DataBind; import io.ebeaninternal.server.type.ScalarType; @@ -14,6 +16,8 @@ import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -27,14 +31,18 @@ public final class IdBinderSimple implements IdBinder { private final Class> expectedType; + private final MultiValueBind multiValueBind; + @SuppressWarnings("rawtypes") private final ScalarType scalarType; - public IdBinderSimple(BeanProperty idProperty) { + + public IdBinderSimple(BeanProperty idProperty, MultiValueBind multiValueBind) { this.idProperty = idProperty; this.scalarType = idProperty.getScalarType(); this.expectedType = idProperty.getPropertyType(); bindIdSql = InternString.intern(idProperty.getDbColumn() + " = ? "); + this.multiValueBind = multiValueBind; } @Override @@ -125,28 +133,29 @@ public final class IdBinderSimple implements IdBinder { @Override public String getIdInValueExprDelete(int size) { - return getIdInValueExpr(size); + return getIdInValueExpr(false, size); } @Override - public String getIdInValueExpr(int size) { + public String getIdInValueExpr(boolean not, int size) { if (size <= 0) { throw new IndexOutOfBoundsException("The size must be at least 1"); } - StringBuilder sb = new StringBuilder(2 * size + 10); - sb.append(" in"); - sb.append(" (?"); - for (int i = 1; i < size; i++) { - sb.append(",?"); - } - sb.append(") "); - return sb.toString(); + return multiValueBind.getInExpression(not, scalarType, size); } @Override - public void addIdInBindValue(SpiExpressionRequest request, Object value) { - value = convertSetId(value, null); - request.addBindValue(value); + public void addIdInBindValues(DefaultSqlUpdate sqlUpdate, Collection> ids) { + sqlUpdate.addParameter(new MultiValueWrapper(ids)); + } + + @Override + public void addIdInBindValues(SpiExpressionRequest request, Collection> values) { + List copy = new ArrayList<>(values); + for (int i = 0; i < copy.size(); i++) { + copy.set(i, convertSetId(copy.get(i), null)); + } + request.addBindValue(new MultiValueWrapper(copy)); } @Override diff --git a/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java b/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java index 125c71dde..032570fc5 100644 --- a/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java +++ b/src/main/java/io/ebeaninternal/server/el/ElPropertyChain.java @@ -192,8 +192,8 @@ public class ElPropertyChain implements ElPropertyValue { } @Override - public String getAssocIdInValueExpr(int size) { - return lastElPropertyValue.getAssocIdInValueExpr(size); + public String getAssocIdInValueExpr(boolean not, int size) { + return lastElPropertyValue.getAssocIdInValueExpr(not, size); } @Override diff --git a/src/main/java/io/ebeaninternal/server/el/ElPropertyValue.java b/src/main/java/io/ebeaninternal/server/el/ElPropertyValue.java index 7f59fcc19..bbae84850 100644 --- a/src/main/java/io/ebeaninternal/server/el/ElPropertyValue.java +++ b/src/main/java/io/ebeaninternal/server/el/ElPropertyValue.java @@ -14,7 +14,7 @@ public interface ElPropertyValue extends ElPropertyDeploy, ExpressionPath { /** * Return the logical id value expression taking into account embedded id's. */ - String getAssocIdInValueExpr(int size); + String getAssocIdInValueExpr(boolean not, int size); /** * Return the logical id in expression taking into account embedded id's. diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java index e15ab261a..6f800cfbf 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java @@ -160,4 +160,8 @@ public class DefaultExpressionRequest implements SpiExpressionRequest { return bindValues; } + @Override + public void appendInExpression(boolean not, Object[] bindValues) { + append(binder.getInExpression(not, bindValues)); + } } diff --git a/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java b/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java index 436be9d54..a8a5197b0 100644 --- a/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.expression; +import io.ebean.event.BeanQueryRequest; import io.ebeaninternal.api.ManyWhereJoins; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; @@ -18,10 +19,17 @@ public class IdInExpression extends NonPrepareExpression { private final Collection> idCollection; + private boolean multiValueIdSupported; + public IdInExpression(Collection> idCollection) { this.idCollection = idCollection; } + @Override + public void prepareExpression(BeanQueryRequest> request) { + multiValueIdSupported = request.isMultiValueIdSupported(); + } + @Override public String nestedPath(BeanDescriptor> desc) { return null; @@ -49,10 +57,7 @@ public class IdInExpression extends NonPrepareExpression { DefaultExpressionRequest r = (DefaultExpressionRequest) request; BeanDescriptor> descriptor = r.getBeanDescriptor(); IdBinder idBinder = descriptor.getIdBinder(); - - for (Object id : idCollection) { - idBinder.addIdInBindValue(request, id); - } + idBinder.addIdInBindValues(request, idCollection); } /** @@ -67,7 +72,7 @@ public class IdInExpression extends NonPrepareExpression { request.append("1=0"); // append false for this stage } else { request.append(descriptor.getIdBinder().getBindIdInSql(null)); - String inClause = idBinder.getIdInValueExpr(idCollection.size()); + String inClause = idBinder.getIdInValueExpr(false, idCollection.size()); request.append(inClause); } } @@ -82,7 +87,7 @@ public class IdInExpression extends NonPrepareExpression { request.append("1=0"); // append false for this stage } else { request.append(descriptor.getIdBinderInLHSSql()); - String inClause = idBinder.getIdInValueExpr(idCollection.size()); + String inClause = idBinder.getIdInValueExpr(false, idCollection.size()); request.append(inClause); } } @@ -92,7 +97,12 @@ public class IdInExpression extends NonPrepareExpression { */ @Override public void queryPlanHash(StringBuilder builder) { - builder.append("IdIn[").append("?").append(idCollection.size()).append("]"); + builder.append("IdIn[?"); + if (!multiValueIdSupported) { + // query plan specific to the number of parameters in the IN clause + builder.append(idCollection.size()); + } + builder.append("]"); } @Override diff --git a/src/main/java/io/ebeaninternal/server/expression/InExpression.java b/src/main/java/io/ebeaninternal/server/expression/InExpression.java index 3409e0ce0..58c479058 100644 --- a/src/main/java/io/ebeaninternal/server/expression/InExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/InExpression.java @@ -5,11 +5,13 @@ import io.ebean.event.BeanQueryRequest; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.server.el.ElPropertyValue; +import io.ebeaninternal.server.persist.MultiValueWrapper; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; class InExpression extends AbstractExpression { @@ -20,6 +22,8 @@ class InExpression extends AbstractExpression { private Object[] bindValues; + private boolean multiValueSupported; + InExpression(String propertyName, Collection> sourceValues, boolean not) { super(propertyName); this.sourceValues = sourceValues; @@ -43,6 +47,9 @@ class InExpression extends AbstractExpression { @Override public void prepareExpression(BeanQueryRequest> request) { bindValues = values(); + if (bindValues.length > 0) { + multiValueSupported = request.isMultiValueSupported((bindValues[0]).getClass()); + } } @Override @@ -58,19 +65,24 @@ class InExpression extends AbstractExpression { prop = null; } - for (Object bindValue : bindValues) { - if (prop == null) { - request.addBindValue(bindValue); - - } else { + if (prop == null) { + if (bindValues.length > 0) { + // if we have no property, we wrap them in a multi value wrapper. + // later the binder will decide, which bind strategy to use. + request.addBindValue(new MultiValueWrapper(Arrays.asList(bindValues))); + } + } else { + List idList = new ArrayList<>(); + for (Object bindValue : bindValues) { // extract the id values from the bean Object[] ids = prop.getAssocIdValues((EntityBean) bindValue); if (ids != null) { - for (Object id : ids) { - request.addBindValue(id); - } + Collections.addAll(idList, ids); } } + if (!idList.isEmpty()) { + request.addBindValue(new MultiValueWrapper(idList)); + } } } @@ -90,23 +102,12 @@ class InExpression extends AbstractExpression { if (prop != null) { request.append(prop.getAssocIdInExpr(propName)); - String inClause = prop.getAssocIdInValueExpr(bindValues.length); - if (not) { - request.append(" not"); - } + String inClause = prop.getAssocIdInValueExpr(not, bindValues.length); request.append(inClause); } else { request.append(propName); - if (not) { - request.append(" not"); - } - request.append(" in (?"); - for (int i = 1; i < bindValues.length; i++) { - request.append(", ").append("?"); - } - - request.append(" ) "); + request.appendInExpression(not, bindValues); } } @@ -121,7 +122,12 @@ class InExpression extends AbstractExpression { builder.append("In["); } builder.append(propName); - builder.append(" ?").append(bindValues.length).append("]"); + builder.append(" ?"); + if (!multiValueSupported) { + // query plan specific to the number of parameters in the IN clause + builder.append(bindValues.length); + } + builder.append("]"); } @Override diff --git a/src/main/java/io/ebeaninternal/server/persist/Binder.java b/src/main/java/io/ebeaninternal/server/persist/Binder.java index 945495483..34a3f8f2c 100644 --- a/src/main/java/io/ebeaninternal/server/persist/Binder.java +++ b/src/main/java/io/ebeaninternal/server/persist/Binder.java @@ -5,6 +5,7 @@ import io.ebeaninternal.api.BindParams; import io.ebeaninternal.server.core.DbExpressionHandler; import io.ebeaninternal.server.core.Message; import io.ebeaninternal.server.core.timezone.DataTimeZone; +import io.ebeaninternal.server.persist.platform.MultiValueBind; import io.ebeaninternal.server.type.DataBind; import io.ebeaninternal.server.type.ScalarType; import io.ebeaninternal.server.type.TypeManager; @@ -19,6 +20,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -38,17 +40,20 @@ public class Binder { private final DataTimeZone dataTimeZone; + private final MultiValueBind multiValueBind; + /** * Set the PreparedStatement with which to bind variables to. */ public Binder(TypeManager typeManager, int asOfBindCount, boolean asOfStandardsBased, - DbExpressionHandler dbExpressionHandler, DataTimeZone dataTimeZone) { + DbExpressionHandler dbExpressionHandler, DataTimeZone dataTimeZone, MultiValueBind multiValueBind) { this.typeManager = typeManager; this.asOfBindCount = asOfBindCount; this.asOfStandardsBased = asOfStandardsBased; this.dbExpressionHandler = dbExpressionHandler; this.dataTimeZone = dataTimeZone; + this.multiValueBind = multiValueBind; } /** @@ -168,6 +173,26 @@ public class Binder { } } + /** + * Return true if MultiValue binding is supported for the given type. + */ + public boolean isMultiValueSupported(Class> cls) { + try { + ScalarType> scalarType = getScalarType(cls); + return multiValueBind.isTypeSupported(scalarType.getJdbcType()); + } catch (PersistenceException e) { + return false; + } + } + + private ScalarType> getScalarType(Class> clazz) { + ScalarType> type = typeManager.getScalarType(clazz); + if (type == null) { + throw new PersistenceException("No ScalarType registered for " + clazz); + } + return type; + } + /** * Bind an Object with unknown data type. */ @@ -178,15 +203,19 @@ public class Binder { bindObject(dataBind, null, Types.OTHER); return null; + } else if (value instanceof MultiValueWrapper) { + MultiValueWrapper wrapper = (MultiValueWrapper) value; + Collection> values = wrapper.getValues(); + + ScalarType> type = getScalarType(wrapper.getType()); + int dbType = type.getJdbcType(); + // let the multiValueBind decide what to do with the value + multiValueBind.bindMultiValues(dataBind, values, type, one -> bindObject(dataBind, one, dbType)); + return values; + } else { - - ScalarType> type = typeManager.getScalarType(value.getClass()); - if (type == null) { - // the type is not registered with the TypeManager. - String msg = "No ScalarType registered for " + value.getClass(); - throw new PersistenceException(msg); - - } else if (!type.isJdbcNative()) { + ScalarType> type = getScalarType(value.getClass()); + if (!type.isJdbcNative()) { // convert to a JDBC native type value = type.toJdbcType(value); } @@ -197,6 +226,14 @@ public class Binder { } } + /** + * Return the SQL in clause taking into account Multi-value support. + */ + public String getInExpression(boolean not, Object[] bindValues) { + ScalarType> type = getScalarType(bindValues[0].getClass()); + return multiValueBind.getInExpression(not, type, bindValues.length); + } + /** * bind a single value. * diff --git a/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java b/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java new file mode 100644 index 000000000..4eaee90ed --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java @@ -0,0 +1,40 @@ +package io.ebeaninternal.server.persist; + +import java.util.Collection; + +/** + * Wraps the multi values that are used for "property in (...)" queries + * @author Roland Praml, FOCONIS AG + */ +public class MultiValueWrapper { + private final Collection> values; + private Class> type; + + public MultiValueWrapper(Collection> values) { + this.values = values; + this.type = values.iterator().next().getClass(); + } + + public Collection> getValues() { + return values; + } + + public Class> getType() { + return type; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Array[" + values.size() + "]={"); + for (Object value : values) { + sb.append(value).append(','); + if (sb.length() > 50) { + sb.append("...}"); + return sb.toString(); + } + } + sb.setLength(sb.length() - 1); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java new file mode 100644 index 000000000..0079ca957 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java @@ -0,0 +1,93 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebean.config.dbplatform.ExtraDbTypes; +import io.ebeaninternal.server.type.DataBind; +import io.ebeaninternal.server.type.ScalarType; + +import java.sql.SQLException; +import java.util.Collection; + +import static java.sql.Types.BIGINT; +import static java.sql.Types.BIT; +import static java.sql.Types.BOOLEAN; +import static java.sql.Types.CHAR; +import static java.sql.Types.DATE; +import static java.sql.Types.DECIMAL; +import static java.sql.Types.DOUBLE; +import static java.sql.Types.FLOAT; +import static java.sql.Types.INTEGER; +import static java.sql.Types.NCHAR; +import static java.sql.Types.NUMERIC; +import static java.sql.Types.NVARCHAR; +import static java.sql.Types.REAL; +import static java.sql.Types.SMALLINT; +import static java.sql.Types.TIMESTAMP; +import static java.sql.Types.TIMESTAMP_WITH_TIMEZONE; +import static java.sql.Types.TIME_WITH_TIMEZONE; +import static java.sql.Types.TINYINT; +import static java.sql.Types.VARCHAR; + +/** + * Base MultiValueBind for platform specific support. + */ +abstract class AbstractMultiValueBind extends MultiValueBind { + + @Override + public boolean isSupported() { + return true; + } + + @Override + public boolean isTypeSupported(int jdbcType) { + return getArrayType(jdbcType) != null; + } + + @Override + public void bindMultiValues(DataBind dataBind, Collection> values, ScalarType> type, BindOne bindOne) throws SQLException { + String arrayType = getArrayType(type.getJdbcType()); + if (arrayType == null) { + super.bindMultiValues(dataBind, values, type, bindOne); + } else { + dataBind.setArray(arrayType, toArray(values, type)); + } + } + + protected String getArrayType(int dbType) { + switch(dbType) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case DECIMAL: // TODO: we have no info about precision here + case NUMERIC: + return "bigint"; + case REAL: + case FLOAT: + case DOUBLE: + return "float"; + case BIT: + case BOOLEAN: + return "bit"; + case DATE: + return "date"; + case TIMESTAMP: + case TIME_WITH_TIMEZONE: + case TIMESTAMP_WITH_TIMEZONE: + return "timestamp"; + //case LONGVARCHAR: + //case CLOB: + case CHAR: + case VARCHAR: + //case LONGNVARCHAR: + //case NCLOB: + case NCHAR: + case NVARCHAR: + return "varchar"; + case ExtraDbTypes.UUID: // Db Native UUID + return "varchar"; + + default: + return null; + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java new file mode 100644 index 000000000..8cf279a9e --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebeaninternal.server.type.ScalarType; + +/** + * Multi value binder that uses SqlServers Table-value parameters + * @author Roland Praml, FOCONIS AG + * + */ +public class H2MultiValueBind extends AbstractMultiValueBind { + + @Override + public String getInExpression(boolean not, ScalarType> type, int size) { + String arrayType = getArrayType(type.getJdbcType()); + if (arrayType == null) { + return super.getInExpression(not, type, size); + } else { + StringBuilder sb = new StringBuilder(50); + sb.append(" in (select * from table(x ").append(arrayType).append(" = ?)) "); + return sb.toString(); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java new file mode 100644 index 000000000..7df88ebde --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java @@ -0,0 +1,70 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebeaninternal.server.type.DataBind; +import io.ebeaninternal.server.type.ScalarType; + +import java.sql.SQLException; +import java.util.Collection; + +/** + * Default implementation for multi value help. + */ +public class MultiValueBind { + + + @FunctionalInterface + public interface BindOne { + void bind(Object value) throws SQLException; + } + + protected Object[] toArray(Collection> values, ScalarType> type) { + Object[] array = new Object[values.size()]; + int i = 0; + for (Object value : values) { + array[i++] = type.toJdbcType(value); + } + return array; + } + + /** + * Defaults to not supported and using a bind value per element. + */ + public boolean isSupported() { + return false; + } + + /** + * Defaults to not supported and using a bind value per element. + */ + public boolean isTypeSupported(int jdbcType) { + return false; + } + + /** + * Default for multi values. They are appended one by one. + */ + public void bindMultiValues(DataBind dataBind, Collection> values, ScalarType> type, BindOne bindOne) throws SQLException { + for (Object value : values) { + if (!type.isJdbcNative()) { + value = type.toJdbcType(value); + } + bindOne.bind(value); + } + } + + /** + * Appends the 'in' expression to the request. Must add leading & trailing space! + */ + public String getInExpression(boolean not, ScalarType> type, int size) { + StringBuilder sb = new StringBuilder(); + if (not) { + sb.append(" not"); + } + sb.append(" in (?"); + for (int i = 1; i < size; i++) { + sb.append(", ").append("?"); + } + sb.append(" ) "); + return sb.toString(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java new file mode 100644 index 000000000..ac8141a5b --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java @@ -0,0 +1,25 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebean.config.dbplatform.ExtraDbTypes; +import io.ebeaninternal.server.type.ScalarType; + +/** + * Multi value binder that uses Postgres Array. + */ +public class PostgresMultiValueBind extends AbstractMultiValueBind { + + @Override + public String getInExpression(boolean not, ScalarType> type, int size) { + int dbType = type.getJdbcType(); + if (dbType == ExtraDbTypes.UUID) { + return (not) ? " != all(?::uuid[])" : " = any(?::uuid[])"; + } + String arrayType = getArrayType(dbType); + if (arrayType == null) { + return super.getInExpression(not, type, size); + } else { + return (not) ? " != all(?)" : " = any(?)"; + } + } + +} diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java index 51f53eb55..de0b5cc1c 100644 --- a/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java @@ -331,7 +331,7 @@ public class CQueryEngine { * deemed to be a be a paging query - check that the order by contains the id * property to ensure unique row ordering for predicable paging but only in * case, this is not a distinct query - * + * * @param request */ private void prepareForPaging(OrmQueryRequest request) { diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java index 01d81a454..a327f3882 100644 --- a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -10,6 +10,7 @@ import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.server.core.OrmQueryEngine; import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.persist.Binder; import javax.persistence.PersistenceException; import java.sql.SQLException; @@ -28,11 +29,14 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { */ private final CQueryEngine queryEngine; + private final Binder binder; + /** * Create the Finder. */ - public DefaultOrmQueryEngine(CQueryEngine queryEngine) { + public DefaultOrmQueryEngine(CQueryEngine queryEngine, Binder binder) { this.queryEngine = queryEngine; + this.binder = binder; } @Override @@ -40,6 +44,11 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { return queryEngine.translate(request, bindLog, sql, e); } + @Override + public boolean isMultiValueSupported(Class> cls) { + return binder.isMultiValueSupported(cls); + } + /** * Flushes the jdbc batch by default unless explicitly turned off on the transaction. */ diff --git a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java index 88e060e85..314996b56 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java @@ -2,12 +2,12 @@ package io.ebeaninternal.server.querydefn; import io.ebean.OrderBy; import io.ebean.Query; -import io.ebeaninternal.server.rawsql.SpiRawSql; import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.CQueryPlanKey; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.deploy.TableJoin; +import io.ebeaninternal.server.rawsql.SpiRawSql; /** * Query plan key for ORM queries. @@ -109,6 +109,10 @@ class OrmQueryPlanKey implements CQueryPlanKey { return planHash; } + public String toString() { + return description + " maxRows:" + maxRows + " firstRow:" + firstRow + " rawSqlKey:" + rawSqlKey + " planHash:" + planHash; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/test/java/io/ebean/BaseTestCase.java b/src/test/java/io/ebean/BaseTestCase.java index d05d97ce3..09b2100bf 100644 --- a/src/test/java/io/ebean/BaseTestCase.java +++ b/src/test/java/io/ebean/BaseTestCase.java @@ -3,15 +3,20 @@ package io.ebean; import io.ebean.annotation.Platform; import io.ebean.util.StringHelper; import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.core.HelpCreateQueryRequest; +import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.deploy.BeanDescriptor; -import org.tests.model.basic.Country; import org.avaje.agentloader.AgentLoader; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.tests.model.basic.Country; import java.sql.Types; +import static org.assertj.core.api.Assertions.assertThat; + @RunWith(ConditionalTestRunner.class) public abstract class BaseTestCase { @@ -81,7 +86,7 @@ public abstract class BaseTestCase { public boolean isDb2() { return Platform.DB2 == platform(); } - + public boolean isPostgres() { return Platform.POSTGRES == platform(); } @@ -127,4 +132,31 @@ public abstract class BaseTestCase { .setLoadBeanCache(true) .findList(); } + + /** + * Platform specific IN clause assert. + */ + protected void platformAssertIn(String sql, String containsIn) { + if (isPostgres()) { + assertThat(sql).contains(containsIn+" = any("); + } else { + assertThat(sql).contains(containsIn+" in "); + } + // H2 contains("where t0.name in (select * from table(x varchar = ?)"); + } + + /** + * Platform specific NOT IN clause assert. + */ + protected void platformAssertNotIn(String sql, String containsIn) { + if (isPostgres()) { + assertThat(sql).contains(containsIn+" != all("); + } else { + assertThat(sql).contains(containsIn+" not in "); + } + } + + protected OrmQueryRequest createQueryRequest(SpiQuery.Type type, Query query, Transaction t) { + return HelpCreateQueryRequest.create(server(), type, query, t); + } } diff --git a/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java b/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java index 1de9a7d55..b7c285afb 100644 --- a/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java +++ b/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java @@ -1,8 +1,8 @@ package io.ebean; -import org.tests.model.basic.EBasicVer; import org.ebeantest.LoggedSqlCollector; import org.junit.Test; +import org.tests.model.basic.EBasicVer; import java.util.ArrayList; import java.util.List; @@ -29,7 +29,7 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + platformAssertIn(loggedSql.get(0), "delete from e_basicver where id "); } @Test @@ -55,7 +55,7 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { } List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + platformAssertIn(loggedSql.get(0), "delete from e_basicver where id "); } @Test @@ -75,7 +75,7 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + platformAssertIn(loggedSql.get(0), "delete from e_basicver where id "); } @@ -102,7 +102,7 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { } List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + platformAssertIn(loggedSql.get(0), "delete from e_basicver where id "); } private List beans(int count) { diff --git a/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java b/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java new file mode 100644 index 000000000..805ca2982 --- /dev/null +++ b/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java @@ -0,0 +1,15 @@ +package io.ebeaninternal.server.core; + +import io.ebean.EbeanServer; +import io.ebean.Query; +import io.ebean.Transaction; +import io.ebeaninternal.api.SpiQuery; + +public class HelpCreateQueryRequest { + + public static OrmQueryRequest create(EbeanServer server, SpiQuery.Type type, Query query, Transaction t) { + + DefaultServer defaultServer = (DefaultServer) server; + return (OrmQueryRequest)defaultServer.createQueryRequest(type, query, t); + } +} diff --git a/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java index 253fd0851..44aa34dff 100644 --- a/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java +++ b/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java @@ -28,7 +28,7 @@ public class IdInExpressionTest extends BaseExpressionTest { } @Test - public void isSameByPlan_when_diffBindCount() { + public void isSameByPlan_when_diffBindCount_notPrepared() { different(exp(10), exp(10, 20)); } diff --git a/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java index b2a4c17b8..e5fc5cab2 100644 --- a/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java +++ b/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java @@ -1,6 +1,11 @@ package io.ebeaninternal.server.expression; +import io.ebean.EbeanServer; +import io.ebean.Query; +import io.ebean.Transaction; +import io.ebean.event.BeanQueryRequest; import org.junit.Test; +import org.tests.model.basic.Customer; import java.util.ArrayList; import java.util.Arrays; @@ -10,6 +15,19 @@ import static org.assertj.core.api.StrictAssertions.assertThat; public class InExpressionTest extends BaseExpressionTest { + /** + * Request with Multi-Value support. + */ + private TDQueryRequest multi() { + return MULTI_VALUE; + } + + /** + * Request with NO Multi-Value support. + */ + private TDQueryRequest noMulti() { + return NO_MULTI_VALUE; + } @Test public void queryPlanHash_given_diffPropertyName_should_differentPlanHash() throws Exception { @@ -19,8 +37,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, false); InExpression ex2 = new InExpression("bar", values, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); different(ex1, ex2); } @@ -34,12 +52,25 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values1, false); InExpression ex2 = new InExpression("foo", values2, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); - + ex1.prepareExpression(noMulti()); + ex2.prepareExpression(noMulti()); different(ex1, ex2); } + @Test + public void queryPlanHash_given_diffBindCount_withMultiSupport_samePlanHash() throws Exception { + + List values1 = values(42, 92); + List values2 = values(42, 92, 82); + + InExpression ex1 = new InExpression("foo", values1, false); + InExpression ex2 = new InExpression("foo", values2, false); + + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); + same(ex1, ex2); + } + @Test public void queryPlanHash_given_diffNotFlag_should_differentPlanHash() throws Exception { @@ -48,8 +79,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, true); InExpression ex2 = new InExpression("foo", values, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); different(ex1, ex2); } @@ -62,8 +93,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, true); InExpression ex2 = new InExpression("foo", values, true); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); same(ex1, ex2); } @@ -78,7 +109,13 @@ public class InExpressionTest extends BaseExpressionTest { private InExpression exp(String propName, boolean not, Object... values) { InExpression ex = new InExpression(propName, Arrays.asList(values), not); - ex.prepareExpression(null); + ex.prepareExpression(multi()); + return ex; + } + + private InExpression expNoMulti(String propName, boolean not, Object... values) { + InExpression ex = new InExpression(propName, Arrays.asList(values), not); + ex.prepareExpression(noMulti()); return ex; } @@ -103,13 +140,15 @@ public class InExpressionTest extends BaseExpressionTest { @Test public void isSameByPlan_when_diffBind_same() { - different(exp("a", false, 10), exp("a", false, 10, 20)); + same(exp("a", false, 10), exp("a", false, 10, 20)); + different(expNoMulti("a", false, 10), expNoMulti("a", false, 10, 20)); } @Test public void isSameByPlan_when_diffBindCount() { - different(exp("a", false, 10), exp("a", false, 10, 20)); + same(exp("a", false, 10), exp("a", false, 10, 20)); + different(expNoMulti("a", false, 10), expNoMulti("a", false, 10, 20)); } @Test @@ -142,4 +181,42 @@ public class InExpressionTest extends BaseExpressionTest { assertThat(exp("a", false, 10, "ABC").isSameByBind(exp("a", false, 10, "ABC", 30))).isFalse(); } + + private static final TDQueryRequest MULTI_VALUE= new TDQueryRequest<>(true); + private static final TDQueryRequest NO_MULTI_VALUE = new TDQueryRequest<>(false); + + static class TDQueryRequest implements BeanQueryRequest { + + final boolean supported; + + TDQueryRequest(boolean supported) { + this.supported = supported; + } + + @Override + public EbeanServer getEbeanServer() { + return null; + } + + @Override + public Transaction getTransaction() { + return null; + } + + @Override + public Query getQuery() { + return null; + } + + @Override + public boolean isMultiValueIdSupported() { + return supported; + } + + @Override + public boolean isMultiValueSupported(Class> valueType) { + return supported; + } + } + } diff --git a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java index 55641a1a8..e60bb4e73 100644 --- a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java +++ b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java @@ -78,9 +78,14 @@ public class TDSpiExpressionRequest implements SpiExpressionRequest { public void appendLike() { } - + @Override public String escapeLikeString(String value) { return value; } + + @Override + public void appendInExpression(boolean not, Object[] bindValues) { + + } } diff --git a/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java b/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java index 03f043a8c..ad13e69c8 100644 --- a/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java +++ b/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java @@ -4,9 +4,9 @@ import io.ebean.BaseTestCase; import io.ebean.Ebean; import io.ebean.Query; import io.ebeaninternal.api.SpiQuery; +import org.junit.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.ResetBasicData; -import org.junit.Test; import java.util.Arrays; import java.util.List; @@ -167,7 +167,7 @@ public class EqlParserTest extends BaseTestCase { Query query = parse("where name in ('Rob','Jim')"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test @@ -178,7 +178,7 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test @@ -189,7 +189,7 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test @@ -200,7 +200,7 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test @@ -210,7 +210,7 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("names", Arrays.asList("Baz", "Maz", "Jim")); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test diff --git a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java index 427539c80..a92ccf92a 100644 --- a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java +++ b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java @@ -3,9 +3,11 @@ package io.ebeaninternal.server.querydefn; import io.ebean.BaseTestCase; import io.ebean.Ebean; +import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.core.OrmQueryRequest; +import org.junit.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.Order; -import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -91,7 +93,11 @@ public class DefaultOrmQueryTest extends BaseTestCase { } private void prepare(DefaultOrmQuery> q1, DefaultOrmQuery> q2) { - q1.prepare(null); - q2.prepare(null); + + OrmQueryRequest> r1 = createQueryRequest(SpiQuery.Type.LIST, q1, null); + q1.prepare(r1); + + OrmQueryRequest> r2 = createQueryRequest(SpiQuery.Type.LIST, q2, null); + q2.prepare(r2); } } diff --git a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java index 6246e4212..e2d98199c 100644 --- a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java +++ b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java @@ -79,7 +79,7 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase { // batch lazy loading into cache assertThat(sql).hasSize(2); assertThat(sql.get(0)).contains("from uuone t0 where t0.name like "); - assertThat(sql.get(1)).contains("from uuone t0 where t0.id in (?,"); + platformAssertIn(sql.get(1), "from uuone t0 where t0.id"); statistics = beanCache.getStatistics(true); assertThat(statistics.getSize()).isGreaterThan(3); diff --git a/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java b/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java index d01d2ef78..ffde77d3b 100644 --- a/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java +++ b/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java @@ -36,7 +36,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + platformAssertIn(secondaryQuery, " (t0.order_id)"); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } @@ -63,7 +63,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + platformAssertIn(secondaryQuery, " (t0.order_id)"); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } @@ -103,7 +103,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + platformAssertIn(secondaryQuery, " (t0.order_id)"); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } diff --git a/src/test/java/org/tests/batchload/TestSecondaryQueries.java b/src/test/java/org/tests/batchload/TestSecondaryQueries.java index 99881043c..fbe1891df 100644 --- a/src/test/java/org/tests/batchload/TestSecondaryQueries.java +++ b/src/test/java/org/tests/batchload/TestSecondaryQueries.java @@ -36,7 +36,8 @@ public class TestSecondaryQueries extends BaseTestCase { assertThat(sql).hasSize(2); assertThat(trimSql(sql.get(0), 2)).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0"); - assertThat(trimSql(sql.get(1), 2)).contains("select t0.id, t0.name from o_customer t0 where t0.id in"); + assertThat(trimSql(sql.get(1), 2)).contains("select t0.id, t0.name from o_customer t0 where t0.id"); + platformAssertIn(sql.get(1), " where t0.id"); } @Test @@ -71,7 +72,8 @@ public class TestSecondaryQueries extends BaseTestCase { sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(trimSql(sql.get(0), 1)).contains("select t0.id, t0.name from o_customer t0 where t0.id in"); + assertThat(trimSql(sql.get(0), 1)).contains("select t0.id, t0.name from o_customer t0 where t0.id"); + platformAssertIn(sql.get(0), " where t0.id"); } @Test @@ -123,7 +125,8 @@ public class TestSecondaryQueries extends BaseTestCase { assertThat(generatedSql).contains("from o_customer t0 where t0.id = ?"); assertEquals(2, sql.size()); - assertThat(sql.get(1)).contains("from contact t0 where (t0.customer_id) in (?)"); + assertThat(sql.get(1)).contains("from contact t0 where (t0.customer_id) "); + platformAssertIn(sql.get(1), " where (t0.customer_id)"); } @@ -179,7 +182,8 @@ public class TestSecondaryQueries extends BaseTestCase { // from o_order_detail t0 // where (t0.order_id) in (?,?,?,?,?) ; --bind(1,4,1,1,1) - assertThat(ordSecondarySql).contains(" from o_order_detail t0 where t0.id > 0 and (t0.order_id) in (?"); + assertThat(ordSecondarySql).contains(" from o_order_detail t0 where t0.id > 0 and (t0.order_id) "); + platformAssertIn(ordSecondarySql, "and (t0.order_id)"); } } diff --git a/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java b/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java index 97c53d326..bb27d3c5a 100644 --- a/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java +++ b/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java @@ -118,9 +118,9 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); if (isPlatformBooleanNative()) { - assertThat(sql.get(0)).contains("update cover set deleted=true where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=true where id "); } else { - assertThat(sql.get(0)).contains("update cover set deleted=1 where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=1 where id "); } } @@ -144,9 +144,9 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); if (isPlatformBooleanNative()) { - assertThat(sql.get(0)).contains("update cover set deleted=true where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=true where id"); } else { - assertThat(sql.get(0)).contains("update cover set deleted=1 where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=1 where id"); } } @@ -162,7 +162,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + platformAssertIn(sql.get(0), "delete from cover where id "); } @Test @@ -178,7 +178,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + platformAssertIn(sql.get(0), "delete from cover where id "); } @@ -201,7 +201,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + platformAssertIn(sql.get(0), "delete from cover where id "); } @Test diff --git a/src/test/java/org/tests/query/TestQueryFetchManyTwoDeep.java b/src/test/java/org/tests/query/TestQueryFetchManyTwoDeep.java index fd449e8bf..048a6e97c 100644 --- a/src/test/java/org/tests/query/TestQueryFetchManyTwoDeep.java +++ b/src/test/java/org/tests/query/TestQueryFetchManyTwoDeep.java @@ -57,7 +57,8 @@ public class TestQueryFetchManyTwoDeep extends BaseTestCase { //SpiQuery> secondaryQuery = secondaryQueries.get(0); String secondarySql = sql.get(1); - assertThat(secondarySql).contains("from o_order_detail t0 where t0.id > 0 and (t0.order_id) in"); + assertThat(secondarySql).contains("from o_order_detail t0 where t0.id > 0 and (t0.order_id) "); + platformAssertIn(secondarySql, "(t0.order_id)"); // select t0.order_id c0, t0.id c1, t0.order_qty c2, t0.ship_qty c3, t0.unit_price c4, t0.cretime c5, t0.updtime c6, t0.order_id c7, t0.product_id c8 // from o_order_detail t0 diff --git a/src/test/java/org/tests/query/TestQueryFilterMany.java b/src/test/java/org/tests/query/TestQueryFilterMany.java index f34a275e0..dc03f4982 100644 --- a/src/test/java/org/tests/query/TestQueryFilterMany.java +++ b/src/test/java/org/tests/query/TestQueryFilterMany.java @@ -76,9 +76,9 @@ public class TestQueryFilterMany extends BaseTestCase { assertThat(sql).hasSize(3); assertThat(sql.get(0)).contains(" from o_customer t0; --bind()"); - assertThat(sql.get(1)).contains(" from contact t0 where (t0.customer_id) in"); + platformAssertIn(sql.get(1), " from contact t0 where (t0.customer_id)"); assertThat(sql.get(1)).contains(" and t0.first_name is not null"); - assertThat(sql.get(2)).contains(" from contact_note t0 where (t0.contact_id) in"); + platformAssertIn(sql.get(2), " from contact_note t0 where (t0.contact_id)"); assertThat(sql.get(2)).contains(" and lower(t0.title) like"); } } diff --git a/src/test/java/org/tests/query/TestQueryInAssocOne.java b/src/test/java/org/tests/query/TestQueryInAssocOne.java index d95030ac2..9de07f3ff 100644 --- a/src/test/java/org/tests/query/TestQueryInAssocOne.java +++ b/src/test/java/org/tests/query/TestQueryInAssocOne.java @@ -27,7 +27,7 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id in (?"); + platformAssertIn(sql, "where t0.kcustomer_id"); } @@ -44,7 +44,7 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id in (?"); + platformAssertIn(sql, "t0.kcustomer_id"); } @@ -61,6 +61,6 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id not in (?"); + platformAssertNotIn(sql, "t0.kcustomer_id"); } } diff --git a/src/test/java/org/tests/query/TestSubQuery.java b/src/test/java/org/tests/query/TestSubQuery.java index 817555e22..1e3427737 100644 --- a/src/test/java/org/tests/query/TestSubQuery.java +++ b/src/test/java/org/tests/query/TestSubQuery.java @@ -24,6 +24,8 @@ public class TestSubQuery extends BaseTestCase { List productIds = new ArrayList<>(); productIds.add(3); + productIds.add(4); + productIds.add(5); Query sq = Ebean.createQuery(Order.class).select("id").where() .in("details.product.id", productIds).query(); diff --git a/src/test/java/org/tests/query/TestWhereIn.java b/src/test/java/org/tests/query/TestWhereIn.java new file mode 100644 index 000000000..9fa85e82a --- /dev/null +++ b/src/test/java/org/tests/query/TestWhereIn.java @@ -0,0 +1,45 @@ +package org.tests.query; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.Query; +import org.junit.Test; +import org.tests.model.basic.Country; +import org.tests.model.basic.ResetBasicData; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestWhereIn extends BaseTestCase { + + @Test + public void testInVarchar() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Country.class) + .where().in("code", "NZ", "AU") + .query(); + + query.findList(); + if (isPostgres()) { + assertThat(sqlOf(query)).contains(" = any("); + } + + } + + + @Test + public void testNotInVarchar() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Country.class) + .where().notIn("code", "NZ", "SA", "US") + .query(); + + query.findList(); + if (isPostgres()) { + assertThat(sqlOf(query)).contains(" != all("); + } + } +} diff --git a/src/test/java/org/tests/query/other/TestManyLazyLoadingQuery.java b/src/test/java/org/tests/query/other/TestManyLazyLoadingQuery.java index cf20aba40..4070496c1 100644 --- a/src/test/java/org/tests/query/other/TestManyLazyLoadingQuery.java +++ b/src/test/java/org/tests/query/other/TestManyLazyLoadingQuery.java @@ -55,7 +55,8 @@ public class TestManyLazyLoadingQuery extends BaseTestCase { beanProperty.addWhereParentIdIn(query0, parentIds, false); query0.findList(); - assertThat(query0.getGeneratedSql()).contains(" from o_order_detail t0 where (t0.order_id) in ("); + assertThat(query0.getGeneratedSql()).contains(" from o_order_detail t0 where (t0.order_id) "); + platformAssertIn(query0.getGeneratedSql(), "where (t0.order_id)"); } finally { Ebean.endTransaction();
diff --git a/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java b/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java new file mode 100644 index 000000000..4eaee90ed --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/MultiValueWrapper.java @@ -0,0 +1,40 @@ +package io.ebeaninternal.server.persist; + +import java.util.Collection; + +/** + * Wraps the multi values that are used for "property in (...)" queries + * @author Roland Praml, FOCONIS AG + */ +public class MultiValueWrapper { + private final Collection> values; + private Class> type; + + public MultiValueWrapper(Collection> values) { + this.values = values; + this.type = values.iterator().next().getClass(); + } + + public Collection> getValues() { + return values; + } + + public Class> getType() { + return type; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("Array[" + values.size() + "]={"); + for (Object value : values) { + sb.append(value).append(','); + if (sb.length() > 50) { + sb.append("...}"); + return sb.toString(); + } + } + sb.setLength(sb.length() - 1); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java new file mode 100644 index 000000000..0079ca957 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/AbstractMultiValueBind.java @@ -0,0 +1,93 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebean.config.dbplatform.ExtraDbTypes; +import io.ebeaninternal.server.type.DataBind; +import io.ebeaninternal.server.type.ScalarType; + +import java.sql.SQLException; +import java.util.Collection; + +import static java.sql.Types.BIGINT; +import static java.sql.Types.BIT; +import static java.sql.Types.BOOLEAN; +import static java.sql.Types.CHAR; +import static java.sql.Types.DATE; +import static java.sql.Types.DECIMAL; +import static java.sql.Types.DOUBLE; +import static java.sql.Types.FLOAT; +import static java.sql.Types.INTEGER; +import static java.sql.Types.NCHAR; +import static java.sql.Types.NUMERIC; +import static java.sql.Types.NVARCHAR; +import static java.sql.Types.REAL; +import static java.sql.Types.SMALLINT; +import static java.sql.Types.TIMESTAMP; +import static java.sql.Types.TIMESTAMP_WITH_TIMEZONE; +import static java.sql.Types.TIME_WITH_TIMEZONE; +import static java.sql.Types.TINYINT; +import static java.sql.Types.VARCHAR; + +/** + * Base MultiValueBind for platform specific support. + */ +abstract class AbstractMultiValueBind extends MultiValueBind { + + @Override + public boolean isSupported() { + return true; + } + + @Override + public boolean isTypeSupported(int jdbcType) { + return getArrayType(jdbcType) != null; + } + + @Override + public void bindMultiValues(DataBind dataBind, Collection> values, ScalarType> type, BindOne bindOne) throws SQLException { + String arrayType = getArrayType(type.getJdbcType()); + if (arrayType == null) { + super.bindMultiValues(dataBind, values, type, bindOne); + } else { + dataBind.setArray(arrayType, toArray(values, type)); + } + } + + protected String getArrayType(int dbType) { + switch(dbType) { + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + case DECIMAL: // TODO: we have no info about precision here + case NUMERIC: + return "bigint"; + case REAL: + case FLOAT: + case DOUBLE: + return "float"; + case BIT: + case BOOLEAN: + return "bit"; + case DATE: + return "date"; + case TIMESTAMP: + case TIME_WITH_TIMEZONE: + case TIMESTAMP_WITH_TIMEZONE: + return "timestamp"; + //case LONGVARCHAR: + //case CLOB: + case CHAR: + case VARCHAR: + //case LONGNVARCHAR: + //case NCLOB: + case NCHAR: + case NVARCHAR: + return "varchar"; + case ExtraDbTypes.UUID: // Db Native UUID + return "varchar"; + + default: + return null; + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java new file mode 100644 index 000000000..8cf279a9e --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/H2MultiValueBind.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebeaninternal.server.type.ScalarType; + +/** + * Multi value binder that uses SqlServers Table-value parameters + * @author Roland Praml, FOCONIS AG + * + */ +public class H2MultiValueBind extends AbstractMultiValueBind { + + @Override + public String getInExpression(boolean not, ScalarType> type, int size) { + String arrayType = getArrayType(type.getJdbcType()); + if (arrayType == null) { + return super.getInExpression(not, type, size); + } else { + StringBuilder sb = new StringBuilder(50); + sb.append(" in (select * from table(x ").append(arrayType).append(" = ?)) "); + return sb.toString(); + } + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java new file mode 100644 index 000000000..7df88ebde --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/MultiValueBind.java @@ -0,0 +1,70 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebeaninternal.server.type.DataBind; +import io.ebeaninternal.server.type.ScalarType; + +import java.sql.SQLException; +import java.util.Collection; + +/** + * Default implementation for multi value help. + */ +public class MultiValueBind { + + + @FunctionalInterface + public interface BindOne { + void bind(Object value) throws SQLException; + } + + protected Object[] toArray(Collection> values, ScalarType> type) { + Object[] array = new Object[values.size()]; + int i = 0; + for (Object value : values) { + array[i++] = type.toJdbcType(value); + } + return array; + } + + /** + * Defaults to not supported and using a bind value per element. + */ + public boolean isSupported() { + return false; + } + + /** + * Defaults to not supported and using a bind value per element. + */ + public boolean isTypeSupported(int jdbcType) { + return false; + } + + /** + * Default for multi values. They are appended one by one. + */ + public void bindMultiValues(DataBind dataBind, Collection> values, ScalarType> type, BindOne bindOne) throws SQLException { + for (Object value : values) { + if (!type.isJdbcNative()) { + value = type.toJdbcType(value); + } + bindOne.bind(value); + } + } + + /** + * Appends the 'in' expression to the request. Must add leading & trailing space! + */ + public String getInExpression(boolean not, ScalarType> type, int size) { + StringBuilder sb = new StringBuilder(); + if (not) { + sb.append(" not"); + } + sb.append(" in (?"); + for (int i = 1; i < size; i++) { + sb.append(", ").append("?"); + } + sb.append(" ) "); + return sb.toString(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java b/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java new file mode 100644 index 000000000..ac8141a5b --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/persist/platform/PostgresMultiValueBind.java @@ -0,0 +1,25 @@ +package io.ebeaninternal.server.persist.platform; + +import io.ebean.config.dbplatform.ExtraDbTypes; +import io.ebeaninternal.server.type.ScalarType; + +/** + * Multi value binder that uses Postgres Array. + */ +public class PostgresMultiValueBind extends AbstractMultiValueBind { + + @Override + public String getInExpression(boolean not, ScalarType> type, int size) { + int dbType = type.getJdbcType(); + if (dbType == ExtraDbTypes.UUID) { + return (not) ? " != all(?::uuid[])" : " = any(?::uuid[])"; + } + String arrayType = getArrayType(dbType); + if (arrayType == null) { + return super.getInExpression(not, type, size); + } else { + return (not) ? " != all(?)" : " = any(?)"; + } + } + +} diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java index 51f53eb55..de0b5cc1c 100644 --- a/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java @@ -331,7 +331,7 @@ public class CQueryEngine { * deemed to be a be a paging query - check that the order by contains the id * property to ensure unique row ordering for predicable paging but only in * case, this is not a distinct query - * + * * @param request */ private void prepareForPaging(OrmQueryRequest request) { diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java index 01d81a454..a327f3882 100644 --- a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -10,6 +10,7 @@ import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.server.core.OrmQueryEngine; import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.server.persist.Binder; import javax.persistence.PersistenceException; import java.sql.SQLException; @@ -28,11 +29,14 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { */ private final CQueryEngine queryEngine; + private final Binder binder; + /** * Create the Finder. */ - public DefaultOrmQueryEngine(CQueryEngine queryEngine) { + public DefaultOrmQueryEngine(CQueryEngine queryEngine, Binder binder) { this.queryEngine = queryEngine; + this.binder = binder; } @Override @@ -40,6 +44,11 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { return queryEngine.translate(request, bindLog, sql, e); } + @Override + public boolean isMultiValueSupported(Class> cls) { + return binder.isMultiValueSupported(cls); + } + /** * Flushes the jdbc batch by default unless explicitly turned off on the transaction. */ diff --git a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java index 88e060e85..314996b56 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/OrmQueryPlanKey.java @@ -2,12 +2,12 @@ package io.ebeaninternal.server.querydefn; import io.ebean.OrderBy; import io.ebean.Query; -import io.ebeaninternal.server.rawsql.SpiRawSql; import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.CQueryPlanKey; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.deploy.TableJoin; +import io.ebeaninternal.server.rawsql.SpiRawSql; /** * Query plan key for ORM queries. @@ -109,6 +109,10 @@ class OrmQueryPlanKey implements CQueryPlanKey { return planHash; } + public String toString() { + return description + " maxRows:" + maxRows + " firstRow:" + firstRow + " rawSqlKey:" + rawSqlKey + " planHash:" + planHash; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/test/java/io/ebean/BaseTestCase.java b/src/test/java/io/ebean/BaseTestCase.java index d05d97ce3..09b2100bf 100644 --- a/src/test/java/io/ebean/BaseTestCase.java +++ b/src/test/java/io/ebean/BaseTestCase.java @@ -3,15 +3,20 @@ package io.ebean; import io.ebean.annotation.Platform; import io.ebean.util.StringHelper; import io.ebeaninternal.api.SpiEbeanServer; +import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.core.HelpCreateQueryRequest; +import io.ebeaninternal.server.core.OrmQueryRequest; import io.ebeaninternal.server.deploy.BeanDescriptor; -import org.tests.model.basic.Country; import org.avaje.agentloader.AgentLoader; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.tests.model.basic.Country; import java.sql.Types; +import static org.assertj.core.api.Assertions.assertThat; + @RunWith(ConditionalTestRunner.class) public abstract class BaseTestCase { @@ -81,7 +86,7 @@ public abstract class BaseTestCase { public boolean isDb2() { return Platform.DB2 == platform(); } - + public boolean isPostgres() { return Platform.POSTGRES == platform(); } @@ -127,4 +132,31 @@ public abstract class BaseTestCase { .setLoadBeanCache(true) .findList(); } + + /** + * Platform specific IN clause assert. + */ + protected void platformAssertIn(String sql, String containsIn) { + if (isPostgres()) { + assertThat(sql).contains(containsIn+" = any("); + } else { + assertThat(sql).contains(containsIn+" in "); + } + // H2 contains("where t0.name in (select * from table(x varchar = ?)"); + } + + /** + * Platform specific NOT IN clause assert. + */ + protected void platformAssertNotIn(String sql, String containsIn) { + if (isPostgres()) { + assertThat(sql).contains(containsIn+" != all("); + } else { + assertThat(sql).contains(containsIn+" not in "); + } + } + + protected OrmQueryRequest createQueryRequest(SpiQuery.Type type, Query query, Transaction t) { + return HelpCreateQueryRequest.create(server(), type, query, t); + } } diff --git a/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java b/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java index 1de9a7d55..b7c285afb 100644 --- a/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java +++ b/src/test/java/io/ebean/EbeanServer_deleteAllByIdTest.java @@ -1,8 +1,8 @@ package io.ebean; -import org.tests.model.basic.EBasicVer; import org.ebeantest.LoggedSqlCollector; import org.junit.Test; +import org.tests.model.basic.EBasicVer; import java.util.ArrayList; import java.util.List; @@ -29,7 +29,7 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + platformAssertIn(loggedSql.get(0), "delete from e_basicver where id "); } @Test @@ -55,7 +55,7 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { } List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + platformAssertIn(loggedSql.get(0), "delete from e_basicver where id "); } @Test @@ -75,7 +75,7 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + platformAssertIn(loggedSql.get(0), "delete from e_basicver where id "); } @@ -102,7 +102,7 @@ public class EbeanServer_deleteAllByIdTest extends BaseTestCase { } List loggedSql = LoggedSqlCollector.stop(); assertThat(loggedSql).hasSize(1); - assertThat(loggedSql.get(0)).contains("delete from e_basicver where id in (?,?,?)"); + platformAssertIn(loggedSql.get(0), "delete from e_basicver where id "); } private List beans(int count) { diff --git a/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java b/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java new file mode 100644 index 000000000..805ca2982 --- /dev/null +++ b/src/test/java/io/ebeaninternal/server/core/HelpCreateQueryRequest.java @@ -0,0 +1,15 @@ +package io.ebeaninternal.server.core; + +import io.ebean.EbeanServer; +import io.ebean.Query; +import io.ebean.Transaction; +import io.ebeaninternal.api.SpiQuery; + +public class HelpCreateQueryRequest { + + public static OrmQueryRequest create(EbeanServer server, SpiQuery.Type type, Query query, Transaction t) { + + DefaultServer defaultServer = (DefaultServer) server; + return (OrmQueryRequest)defaultServer.createQueryRequest(type, query, t); + } +} diff --git a/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java index 253fd0851..44aa34dff 100644 --- a/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java +++ b/src/test/java/io/ebeaninternal/server/expression/IdInExpressionTest.java @@ -28,7 +28,7 @@ public class IdInExpressionTest extends BaseExpressionTest { } @Test - public void isSameByPlan_when_diffBindCount() { + public void isSameByPlan_when_diffBindCount_notPrepared() { different(exp(10), exp(10, 20)); } diff --git a/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java b/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java index b2a4c17b8..e5fc5cab2 100644 --- a/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java +++ b/src/test/java/io/ebeaninternal/server/expression/InExpressionTest.java @@ -1,6 +1,11 @@ package io.ebeaninternal.server.expression; +import io.ebean.EbeanServer; +import io.ebean.Query; +import io.ebean.Transaction; +import io.ebean.event.BeanQueryRequest; import org.junit.Test; +import org.tests.model.basic.Customer; import java.util.ArrayList; import java.util.Arrays; @@ -10,6 +15,19 @@ import static org.assertj.core.api.StrictAssertions.assertThat; public class InExpressionTest extends BaseExpressionTest { + /** + * Request with Multi-Value support. + */ + private TDQueryRequest multi() { + return MULTI_VALUE; + } + + /** + * Request with NO Multi-Value support. + */ + private TDQueryRequest noMulti() { + return NO_MULTI_VALUE; + } @Test public void queryPlanHash_given_diffPropertyName_should_differentPlanHash() throws Exception { @@ -19,8 +37,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, false); InExpression ex2 = new InExpression("bar", values, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); different(ex1, ex2); } @@ -34,12 +52,25 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values1, false); InExpression ex2 = new InExpression("foo", values2, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); - + ex1.prepareExpression(noMulti()); + ex2.prepareExpression(noMulti()); different(ex1, ex2); } + @Test + public void queryPlanHash_given_diffBindCount_withMultiSupport_samePlanHash() throws Exception { + + List values1 = values(42, 92); + List values2 = values(42, 92, 82); + + InExpression ex1 = new InExpression("foo", values1, false); + InExpression ex2 = new InExpression("foo", values2, false); + + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); + same(ex1, ex2); + } + @Test public void queryPlanHash_given_diffNotFlag_should_differentPlanHash() throws Exception { @@ -48,8 +79,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, true); InExpression ex2 = new InExpression("foo", values, false); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); different(ex1, ex2); } @@ -62,8 +93,8 @@ public class InExpressionTest extends BaseExpressionTest { InExpression ex1 = new InExpression("foo", values, true); InExpression ex2 = new InExpression("foo", values, true); - ex1.prepareExpression(null); - ex2.prepareExpression(null); + ex1.prepareExpression(multi()); + ex2.prepareExpression(multi()); same(ex1, ex2); } @@ -78,7 +109,13 @@ public class InExpressionTest extends BaseExpressionTest { private InExpression exp(String propName, boolean not, Object... values) { InExpression ex = new InExpression(propName, Arrays.asList(values), not); - ex.prepareExpression(null); + ex.prepareExpression(multi()); + return ex; + } + + private InExpression expNoMulti(String propName, boolean not, Object... values) { + InExpression ex = new InExpression(propName, Arrays.asList(values), not); + ex.prepareExpression(noMulti()); return ex; } @@ -103,13 +140,15 @@ public class InExpressionTest extends BaseExpressionTest { @Test public void isSameByPlan_when_diffBind_same() { - different(exp("a", false, 10), exp("a", false, 10, 20)); + same(exp("a", false, 10), exp("a", false, 10, 20)); + different(expNoMulti("a", false, 10), expNoMulti("a", false, 10, 20)); } @Test public void isSameByPlan_when_diffBindCount() { - different(exp("a", false, 10), exp("a", false, 10, 20)); + same(exp("a", false, 10), exp("a", false, 10, 20)); + different(expNoMulti("a", false, 10), expNoMulti("a", false, 10, 20)); } @Test @@ -142,4 +181,42 @@ public class InExpressionTest extends BaseExpressionTest { assertThat(exp("a", false, 10, "ABC").isSameByBind(exp("a", false, 10, "ABC", 30))).isFalse(); } + + private static final TDQueryRequest MULTI_VALUE= new TDQueryRequest<>(true); + private static final TDQueryRequest NO_MULTI_VALUE = new TDQueryRequest<>(false); + + static class TDQueryRequest implements BeanQueryRequest { + + final boolean supported; + + TDQueryRequest(boolean supported) { + this.supported = supported; + } + + @Override + public EbeanServer getEbeanServer() { + return null; + } + + @Override + public Transaction getTransaction() { + return null; + } + + @Override + public Query getQuery() { + return null; + } + + @Override + public boolean isMultiValueIdSupported() { + return supported; + } + + @Override + public boolean isMultiValueSupported(Class> valueType) { + return supported; + } + } + } diff --git a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java index 55641a1a8..e60bb4e73 100644 --- a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java +++ b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java @@ -78,9 +78,14 @@ public class TDSpiExpressionRequest implements SpiExpressionRequest { public void appendLike() { } - + @Override public String escapeLikeString(String value) { return value; } + + @Override + public void appendInExpression(boolean not, Object[] bindValues) { + + } } diff --git a/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java b/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java index 03f043a8c..ad13e69c8 100644 --- a/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java +++ b/src/test/java/io/ebeaninternal/server/grammer/EqlParserTest.java @@ -4,9 +4,9 @@ import io.ebean.BaseTestCase; import io.ebean.Ebean; import io.ebean.Query; import io.ebeaninternal.api.SpiQuery; +import org.junit.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.ResetBasicData; -import org.junit.Test; import java.util.Arrays; import java.util.List; @@ -167,7 +167,7 @@ public class EqlParserTest extends BaseTestCase { Query query = parse("where name in ('Rob','Jim')"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test @@ -178,7 +178,7 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test @@ -189,7 +189,7 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test @@ -200,7 +200,7 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("two", "Bar"); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test @@ -210,7 +210,7 @@ public class EqlParserTest extends BaseTestCase { query.setParameter("names", Arrays.asList("Baz", "Maz", "Jim")); query.findList(); - assertThat(query.getGeneratedSql()).contains("where t0.name in (?, ?, ? )"); + platformAssertIn(query.getGeneratedSql(),"where t0.name"); } @Test diff --git a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java index 427539c80..a92ccf92a 100644 --- a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java +++ b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java @@ -3,9 +3,11 @@ package io.ebeaninternal.server.querydefn; import io.ebean.BaseTestCase; import io.ebean.Ebean; +import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.server.core.OrmQueryRequest; +import org.junit.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.Order; -import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -91,7 +93,11 @@ public class DefaultOrmQueryTest extends BaseTestCase { } private void prepare(DefaultOrmQuery> q1, DefaultOrmQuery> q2) { - q1.prepare(null); - q2.prepare(null); + + OrmQueryRequest> r1 = createQueryRequest(SpiQuery.Type.LIST, q1, null); + q1.prepare(r1); + + OrmQueryRequest> r2 = createQueryRequest(SpiQuery.Type.LIST, q2, null); + q2.prepare(r2); } } diff --git a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java index 6246e4212..e2d98199c 100644 --- a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java +++ b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java @@ -79,7 +79,7 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase { // batch lazy loading into cache assertThat(sql).hasSize(2); assertThat(sql.get(0)).contains("from uuone t0 where t0.name like "); - assertThat(sql.get(1)).contains("from uuone t0 where t0.id in (?,"); + platformAssertIn(sql.get(1), "from uuone t0 where t0.id"); statistics = beanCache.getStatistics(true); assertThat(statistics.getSize()).isGreaterThan(3); diff --git a/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java b/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java index d01d2ef78..ffde77d3b 100644 --- a/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java +++ b/src/test/java/org/tests/batchload/TestQueryJoinToAssocOne.java @@ -36,7 +36,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + platformAssertIn(secondaryQuery, " (t0.order_id)"); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } @@ -63,7 +63,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + platformAssertIn(secondaryQuery, " (t0.order_id)"); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } @@ -103,7 +103,7 @@ public class TestQueryJoinToAssocOne extends BaseTestCase { String secondaryQuery = trimSql(loggedSql.get(1), 1); assertThat(secondaryQuery).contains("select t0.order_id, t0.id,"); assertThat(secondaryQuery).contains(" from o_order_detail t0 left join o_product t1"); - assertThat(secondaryQuery).contains(" (t0.order_id) in (?"); + platformAssertIn(secondaryQuery, " (t0.order_id)"); assertThat(secondaryQuery).contains(" order by t0.order_id, t0.id"); } diff --git a/src/test/java/org/tests/batchload/TestSecondaryQueries.java b/src/test/java/org/tests/batchload/TestSecondaryQueries.java index 99881043c..fbe1891df 100644 --- a/src/test/java/org/tests/batchload/TestSecondaryQueries.java +++ b/src/test/java/org/tests/batchload/TestSecondaryQueries.java @@ -36,7 +36,8 @@ public class TestSecondaryQueries extends BaseTestCase { assertThat(sql).hasSize(2); assertThat(trimSql(sql.get(0), 2)).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0"); - assertThat(trimSql(sql.get(1), 2)).contains("select t0.id, t0.name from o_customer t0 where t0.id in"); + assertThat(trimSql(sql.get(1), 2)).contains("select t0.id, t0.name from o_customer t0 where t0.id"); + platformAssertIn(sql.get(1), " where t0.id"); } @Test @@ -71,7 +72,8 @@ public class TestSecondaryQueries extends BaseTestCase { sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(trimSql(sql.get(0), 1)).contains("select t0.id, t0.name from o_customer t0 where t0.id in"); + assertThat(trimSql(sql.get(0), 1)).contains("select t0.id, t0.name from o_customer t0 where t0.id"); + platformAssertIn(sql.get(0), " where t0.id"); } @Test @@ -123,7 +125,8 @@ public class TestSecondaryQueries extends BaseTestCase { assertThat(generatedSql).contains("from o_customer t0 where t0.id = ?"); assertEquals(2, sql.size()); - assertThat(sql.get(1)).contains("from contact t0 where (t0.customer_id) in (?)"); + assertThat(sql.get(1)).contains("from contact t0 where (t0.customer_id) "); + platformAssertIn(sql.get(1), " where (t0.customer_id)"); } @@ -179,7 +182,8 @@ public class TestSecondaryQueries extends BaseTestCase { // from o_order_detail t0 // where (t0.order_id) in (?,?,?,?,?) ; --bind(1,4,1,1,1) - assertThat(ordSecondarySql).contains(" from o_order_detail t0 where t0.id > 0 and (t0.order_id) in (?"); + assertThat(ordSecondarySql).contains(" from o_order_detail t0 where t0.id > 0 and (t0.order_id) "); + platformAssertIn(ordSecondarySql, "and (t0.order_id)"); } } diff --git a/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java b/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java index 97c53d326..bb27d3c5a 100644 --- a/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java +++ b/src/test/java/org/tests/model/onetoone/album/DeleteById_SoftDelete_Tests.java @@ -118,9 +118,9 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); if (isPlatformBooleanNative()) { - assertThat(sql.get(0)).contains("update cover set deleted=true where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=true where id "); } else { - assertThat(sql.get(0)).contains("update cover set deleted=1 where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=1 where id "); } } @@ -144,9 +144,9 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); if (isPlatformBooleanNative()) { - assertThat(sql.get(0)).contains("update cover set deleted=true where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=true where id"); } else { - assertThat(sql.get(0)).contains("update cover set deleted=1 where id in (?,?)"); + assertThat(sql.get(0)).contains("update cover set deleted=1 where id"); } } @@ -162,7 +162,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + platformAssertIn(sql.get(0), "delete from cover where id "); } @Test @@ -178,7 +178,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + platformAssertIn(sql.get(0), "delete from cover where id "); } @@ -201,7 +201,7 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase { List sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(sql.get(0)).contains("delete from cover where id in (?,?)"); + platformAssertIn(sql.get(0), "delete from cover where id "); } @Test diff --git a/src/test/java/org/tests/query/TestQueryFetchManyTwoDeep.java b/src/test/java/org/tests/query/TestQueryFetchManyTwoDeep.java index fd449e8bf..048a6e97c 100644 --- a/src/test/java/org/tests/query/TestQueryFetchManyTwoDeep.java +++ b/src/test/java/org/tests/query/TestQueryFetchManyTwoDeep.java @@ -57,7 +57,8 @@ public class TestQueryFetchManyTwoDeep extends BaseTestCase { //SpiQuery> secondaryQuery = secondaryQueries.get(0); String secondarySql = sql.get(1); - assertThat(secondarySql).contains("from o_order_detail t0 where t0.id > 0 and (t0.order_id) in"); + assertThat(secondarySql).contains("from o_order_detail t0 where t0.id > 0 and (t0.order_id) "); + platformAssertIn(secondarySql, "(t0.order_id)"); // select t0.order_id c0, t0.id c1, t0.order_qty c2, t0.ship_qty c3, t0.unit_price c4, t0.cretime c5, t0.updtime c6, t0.order_id c7, t0.product_id c8 // from o_order_detail t0 diff --git a/src/test/java/org/tests/query/TestQueryFilterMany.java b/src/test/java/org/tests/query/TestQueryFilterMany.java index f34a275e0..dc03f4982 100644 --- a/src/test/java/org/tests/query/TestQueryFilterMany.java +++ b/src/test/java/org/tests/query/TestQueryFilterMany.java @@ -76,9 +76,9 @@ public class TestQueryFilterMany extends BaseTestCase { assertThat(sql).hasSize(3); assertThat(sql.get(0)).contains(" from o_customer t0; --bind()"); - assertThat(sql.get(1)).contains(" from contact t0 where (t0.customer_id) in"); + platformAssertIn(sql.get(1), " from contact t0 where (t0.customer_id)"); assertThat(sql.get(1)).contains(" and t0.first_name is not null"); - assertThat(sql.get(2)).contains(" from contact_note t0 where (t0.contact_id) in"); + platformAssertIn(sql.get(2), " from contact_note t0 where (t0.contact_id)"); assertThat(sql.get(2)).contains(" and lower(t0.title) like"); } } diff --git a/src/test/java/org/tests/query/TestQueryInAssocOne.java b/src/test/java/org/tests/query/TestQueryInAssocOne.java index d95030ac2..9de07f3ff 100644 --- a/src/test/java/org/tests/query/TestQueryInAssocOne.java +++ b/src/test/java/org/tests/query/TestQueryInAssocOne.java @@ -27,7 +27,7 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id in (?"); + platformAssertIn(sql, "where t0.kcustomer_id"); } @@ -44,7 +44,7 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id in (?"); + platformAssertIn(sql, "t0.kcustomer_id"); } @@ -61,6 +61,6 @@ public class TestQueryInAssocOne extends BaseTestCase { String sql = query.getGeneratedSql(); assertThat(sql).contains("join o_customer t1 on t1.id = t0.kcustomer_id"); - assertThat(sql).contains("t0.kcustomer_id not in (?"); + platformAssertNotIn(sql, "t0.kcustomer_id"); } } diff --git a/src/test/java/org/tests/query/TestSubQuery.java b/src/test/java/org/tests/query/TestSubQuery.java index 817555e22..1e3427737 100644 --- a/src/test/java/org/tests/query/TestSubQuery.java +++ b/src/test/java/org/tests/query/TestSubQuery.java @@ -24,6 +24,8 @@ public class TestSubQuery extends BaseTestCase { List productIds = new ArrayList<>(); productIds.add(3); + productIds.add(4); + productIds.add(5); Query sq = Ebean.createQuery(Order.class).select("id").where() .in("details.product.id", productIds).query(); diff --git a/src/test/java/org/tests/query/TestWhereIn.java b/src/test/java/org/tests/query/TestWhereIn.java new file mode 100644 index 000000000..9fa85e82a --- /dev/null +++ b/src/test/java/org/tests/query/TestWhereIn.java @@ -0,0 +1,45 @@ +package org.tests.query; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.Query; +import org.junit.Test; +import org.tests.model.basic.Country; +import org.tests.model.basic.ResetBasicData; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestWhereIn extends BaseTestCase { + + @Test + public void testInVarchar() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Country.class) + .where().in("code", "NZ", "AU") + .query(); + + query.findList(); + if (isPostgres()) { + assertThat(sqlOf(query)).contains(" = any("); + } + + } + + + @Test + public void testNotInVarchar() { + + ResetBasicData.reset(); + + Query query = Ebean.find(Country.class) + .where().notIn("code", "NZ", "SA", "US") + .query(); + + query.findList(); + if (isPostgres()) { + assertThat(sqlOf(query)).contains(" != all("); + } + } +} diff --git a/src/test/java/org/tests/query/other/TestManyLazyLoadingQuery.java b/src/test/java/org/tests/query/other/TestManyLazyLoadingQuery.java index cf20aba40..4070496c1 100644 --- a/src/test/java/org/tests/query/other/TestManyLazyLoadingQuery.java +++ b/src/test/java/org/tests/query/other/TestManyLazyLoadingQuery.java @@ -55,7 +55,8 @@ public class TestManyLazyLoadingQuery extends BaseTestCase { beanProperty.addWhereParentIdIn(query0, parentIds, false); query0.findList(); - assertThat(query0.getGeneratedSql()).contains(" from o_order_detail t0 where (t0.order_id) in ("); + assertThat(query0.getGeneratedSql()).contains(" from o_order_detail t0 where (t0.order_id) "); + platformAssertIn(query0.getGeneratedSql(), "where (t0.order_id)"); } finally { Ebean.endTransaction();