Compare commits

..
Author SHA1 Message Date
robin.bygrave 56894400f4 Fix tests TestInsertCheckUnique for Oracle and SQL Server 2026-07-14 09:35:25 +12:00
robin.bygrave 32931d6db0 #3848 Fix regression for Sql Server and Oracle with exists() introduced in 18.2.0
Fix #3848: exists() generates invalid scalar SQL on SQL Server and Oracle

Query.exists() generated `select exists(<subquery>)`, which is valid on
Postgres/H2/MySQL but rejected by SQL Server and Oracle since both only
support EXISTS as a predicate, not as a directly selectable scalar boolean
expression.                                                                                                                                                                              ┃
                                                                                                                                                                                            ┃
Add DatabasePlatform.existsWithCaseWhen/existsFromClause capability flags
and use them in CQueryBuilder.wrapSelectExists() to generate
`select case when exists(<subquery>) then 1 else 0 end` on platforms that                                                                                                                ┃
need it, with an additional ` from dual` suffix for Oracle (which requires                                                                                                               ┃
a FROM clause on every select). CQueryExists reads the boolean result via                                                                                                                ┃
ResultSet.getBoolean(1), which correctly interprets the resulting 0/1 int.
2026-07-14 09:15:41 +12:00
Rob Bygraveandrobin.bygrave 73c9ff8d80 Throw exception on duplicate Database registration (#3838) (#3847)
* Throw exception on duplicate Database registration (#3838)

DatabaseFactory.create() with register(true) previously returned the
existing Database instance when another instance was already registered
under the same name (added in #3759). This silently caused two
independently created Database instances (e.g. with different
DataSourceConfig.url values) to collide/share state, since the
second "instance" was actually the first one in disguise.

Change this to throw an IllegalStateException instead, forcing
callers to either use a unique DatabaseConfig name or setRegister(false)
when the Database is not intended to be registered/looked up by name.

* Also handle deregistration on shutdown

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-14 00:34:49 +12:00
Rob Bygraveandrobin.bygrave bdbe743f56 #3275: Fix NPE when update/delete with @IdClass mapping (#3846)
## Root cause:

For @IdClass composite keys, the "id" is cached only in EntityBeanIntercept.ownerId(), populated only when reading from DB or during insert derivation. A fresh bean instance passed straight to DB.update()/DB.delete() had this null, causing an NPE in BindableIdEmbedded.dmlBind().

## Fix:

dmlBind() now defensively derives the id (reusing the existing insert-time derivation logic, extracted into a shared deriveId() helper) when it's null but derivable — covering both update and delete, since they share the same DML bind path.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 23:10:54 +12:00
Rob Bygraveandrobin.bygrave ed2026b858 #3110: Fix cache key with enum type (#3845)
## Root cause:

Finder.byId() on a @Cache entity converts the id to a String cache key via IdBinder.cacheKey() = scalarType.format(value). For enums, format() returns .name() (e.g. "APPROVED"), not the db-mapped value. On a  cache hit, BeanDescriptorCacheHelp.loadBeanDirect() converts that cache-key string back via desc.convertId() → IdBinderSimple.convertId() → scalarType.toBeanType(), which expects a db value (e.g. "2" for @EnumValue("2")),  not the enum's Java name — causing Integer.parseInt("APPROVED") to crash in EnumToDbIntegerMap.getBeanValue().

## Fix:

IdBinderSimple.convertId() now special-cases String input to use scalarType.parse() (the true inverse of  format()) instead of toBeanType() (the inverse of toJdbcType()/db-value). This mirrors an existing, already-correct pattern in IdBinderEmbedded.convertId(), which already handles String cache-key round-tripping via parse(). Verified  this doesn't affect other id types (Long/Integer/etc.) since parse() and toBeanType() are equivalent for them — the mismatch only exists for Enum-mapped types.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 22:42:16 +12:00
Rob Bygraveandrobin.bygrave 8d21f91f62 #2477: Support @DbArray inside an @Embeddable (#3844)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 21:45:11 +12:00
Rob Bygraveandrobin.bygrave 6d1d58b8c2 #1989: Add findPagedList() support to DtoQuery (when based on orm query) (#3843)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 20:34:25 +12:00
Rob Bygraveandrobin.bygrave a943ee9225 #2263: MySql DDL - generate inline comments for MySql DDL generation (#3842)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 15:56:06 +12:00
Rob Bygraveandrobin.bygrave 94f252f423 #1700: Add support for @OrderColumn on @ManyToMany (#3841)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 15:55:03 +12:00
Rob Bygrave 275f8ad9f9 #2393: @OrderColumn was not maintained for @ElementCollection — no order column populated on insert and no order by on fetch. (#3840)
## Root cause:
AnnotationAssocManys only parsed @OrderColumn inside the @OneToMany branch, and even if parsed, BeanDescriptorManager.checkMappedByOneToMany() returned early for element collections before reaching the order-column setup. The element collection's synthetic descriptor also isn't registered in deployInfoMap, so it couldn't reuse the existing @OneToMany lookup path.

## Fix (in ebean-core):

- BeanDescriptorManager: extracted makeOrderColumn(DeployBeanPropertyAssocMany, DeployBeanDescriptor) so the synthetic order property can be built against an explicit target descriptor, not just one looked up via deployInfoMap.

- AnnotationAssocManys.readElementCollection(): now reads @OrderColumn, sets fetchOrderBy, and builds the order property directly on the element descriptor before it's converted to a runtime BeanDescriptor.

- SaveManyElementCollection: binds a sequential 0-based index as the order column value on insert (element collections delete-then-reinsert the whole collection in list order, so this is a natural fit).

DDL generation and the fetch order by needed no changes - both already generalize over any BeanDescriptor's order column property once it exists.

Added EcolPerson / TestElementCollectionOrderColumn covering insert population, order preserved on reload, and order by present on fetch-join SQL.

Known limitation: @OrderColumn on a Map-based element collection (SaveManyElementCollectionMap) is not wired up - not part of this issue and not a standard JPA combination (Maps use @MapKeyColumn).
2026-07-13 15:11:49 +12:00
Rob Bygrave 9ac3bd71f6 Issue #2840: @DbJson/@DbJsonB Map<String,Object> fields ignored mutationDetection and always used ModifyAware-wrapper dirty checking — so NONE (and HASH) had no effect, unlike plain Object-typed @DbJson fields which correctly honored these modes. (#3839)
Root cause: The built-in ScalarTypeJsonMap/List/Set types (used for Map<String,Object>, simple List/Set) hard-coded mutable()=true and isDirty() to always do a ModifyAware check, regardless of the configured MutationDetection. Only SOURCE accidentally worked because it happened to route through BeanPropertyJsonMapper.

 Fix (in ebean-core, io.ebeaninternal.server.type):

 - ScalarTypeJsonValue now derives mutable (false only for NONE) and keepSource/jsonMapper() (true for HASH/SOURCE) from the property's MutationDetection, instead of a single hard-coded keepSource boolean.

- ScalarTypeJsonMap, ScalarTypeJsonMapEnum, ScalarTypeJsonList, ScalarTypeJsonSet, and ScalarTypeJsonCollectionValue now accept/pass through MutationDetection instead of a raw boolean.

- DefaultTypeManager.dbJsonType() passes the property's own mutationDetection() straight through for these collection types (per your choice: DEFAULT still always means ModifyAware for collections — only an explicit NONE/HASH/SOURCE annotation changes behavior; the DatabaseConfig-wide default is not applied to these types).

- @DbArray fallback call sites (PlatformArrayTypeJsonList/Set) updated to pass MutationDetection.DEFAULT, preserving unchanged behavior.
2026-07-13 10:24:25 +12:00
Rob Bygraveandrobin.bygrave bb46c88db1 Fix findVersions() unbound bind parameters on sql2011 history platforms (#3837)
On standards-based platforms (MariaDB, SQL Server, Oracle, DB2, HANA) the root table always generates a 'for system_time between ? and ?' clause for TemporalMode.VERSIONS, but bind values were only supplied  for findVersionsBetween(), leaving findVersions() with 2 unbound placeholders and shifted bind positions ("Parameter at position N is not set").

 Match the bind guard to the SQL generation condition and default start/end to epoch/now when not explicit.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-07 21:05:22 +12:00
Rob Bygraveandrobin.bygrave 28e6108315 Bump mysql driver to mysql-connector-j (#3836)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-07 10:17:09 +12:00
robin.bygrave c20d1cdf9b Fix CI test execution with redis reused across ebean-redis and ebean-redisson modules 2026-07-07 10:09:46 +12:00
robin.bygrave 007409d21c Bump postgresql to 42.7.11 2026-07-07 09:21:14 +12:00
Rob Bygrave 8b61069b3f #3441 Handle type coercion in hits against bean cache (#3835) 2026-07-06 22:04:09 +12:00
Rob BygraveandRoland Praml a82dcb2509 Fix #3041 check uniqueness cache skipclean (#3834)
* checkUniqueness supports queryCache and skipClean

* Keep the DB.checkUniqueness() as per original (not overload there)

---------

Co-authored-by: Roland Praml <roland.praml@foconis.de>
2026-07-06 22:03:52 +12:00
Rob BygraveandRoland Praml 08bb170cb2 Fix #3156 inherited property access (#3833)
* Properties of inherited models can be used on different child models

* Adjust typos only

---------

Co-authored-by: Roland Praml <roland.praml@foconis.de>
2026-07-06 21:08:35 +12:00
98 changed files with 2129 additions and 253 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
<properties>
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
<postgres.jdbc.version>42.7.11</postgres.jdbc.version>
</properties>
<dependencies>
+1 -1
View File
@@ -101,7 +101,7 @@ Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.8</version>
<version>42.7.11</version>
</dependency>
```
+4
View File
@@ -457,6 +457,10 @@ public final class DB {
/**
* Same as {@link #checkUniqueness(Object)} but with given transaction.
* <p>
* For control over query cache use and whether to skip the check when the bean's unique
* properties are unchanged, use {@link Database#checkUniqueness(Object, Transaction, boolean, boolean)}
* via {@link #getDefault()} instead.
*/
public static Set<Property> checkUniqueness(Object bean, Transaction transaction) {
return getDefault().checkUniqueness(bean, transaction);
+11 -2
View File
@@ -1078,12 +1078,21 @@ public interface Database {
* @param bean The entity bean to check uniqueness on
* @return a set of Properties if constraint validation was detected or empty list.
*/
Set<Property> checkUniqueness(Object bean);
default Set<Property> checkUniqueness(Object bean) {
return checkUniqueness(bean, null, false, true);
}
/**
* Same as {@link #checkUniqueness(Object)}. but with given transaction.
*/
Set<Property> checkUniqueness(Object bean, Transaction transaction);
default Set<Property> checkUniqueness(Object bean, Transaction transaction) {
return checkUniqueness(bean, transaction, false, true);
}
/**
* Same as {@link #checkUniqueness(Object)}. but with given transaction and extended search options.
*/
Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean);
/**
* Marks the entity bean as dirty.
@@ -62,9 +62,10 @@ public interface DatabaseBuilder {
/**
* Build and return the Database instance.
* <p>
* When {@link #setRegister(boolean)} is set to true, and a database with the same
* name is already registered, this may return the existing registered database
* rather than creating a new one.
* When {@link #setRegister(boolean)} is set to true (the default), and a database
* with the same name is already registered, this throws an {@link IllegalStateException}.
* Use a unique name, or use {@link #setRegister(boolean)} with {@code false} if the
* Database instance is not intended to be registered/looked up by name.
*/
Database build();
@@ -7,8 +7,6 @@ import jakarta.persistence.PersistenceException;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.System.Logger.Level.WARNING;
/**
* Low-level factory for creating {@link Database} instances.
* <p>
@@ -81,10 +79,10 @@ public final class DatabaseFactory {
// We're explicitly creating a database to be registered, so avoid
// triggering DbContext static initialisation to auto-create a default one.
DbPrimary.setSkip(true);
Database existing = DbContext.getInstance().getRegistered(name);
if (existing != null) {
EbeanVersion.log.log(WARNING, "Using existing database with name:{0}", name);
return existing;
if (DbContext.getInstance().contains(name)) {
throw new IllegalStateException("A Database with name [" + name + "] is already registered."
+ " Use a unique DatabaseConfig name, or set DatabaseConfig.setRegister(false)"
+ " if this Database instance is not intended to be registered/looked up by name.");
}
}
Database server = createInternal(config);
@@ -123,6 +121,24 @@ public final class DatabaseFactory {
}
}
/**
* Remove the registration of this Database.
* <p>
* This is invoked when a Database is shutdown so that its registered name
* becomes available again for a subsequently created Database with the same name.
*/
public static void unregister(Database server) {
lock.lock();
try {
DbContext.getInstance().deregister(server);
if (server.name().equals(defaultServerName)) {
defaultServerName = null;
}
} finally {
lock.unlock();
}
}
/**
* Shutdown gracefully all Database instances cleaning up any resources as required.
* <p>
@@ -4,7 +4,6 @@ import io.ebean.config.BeanNotEnhancedException;
import io.ebean.datasource.DataSourceConfigurationException;
import jakarta.persistence.PersistenceException;
import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
@@ -77,9 +76,8 @@ final class DbContext {
return defaultDatabase;
}
@Nullable
Database getRegistered(String name) {
return concMap.get(name);
boolean contains(String name) {
return concMap.containsKey(name);
}
/**
@@ -122,6 +120,27 @@ final class DbContext {
registerWithName(server.name(), server, isDefault);
}
/**
* Remove the registration for this Database (typically on shutdown) so that
* its name becomes available again for a subsequently created Database.
* <p>
* Only removes the registration if it currently maps to this exact instance
* (avoids removing a different Database subsequently registered with the same name).
*/
void deregister(Database server) {
lock.lock();
try {
String name = server.name();
concMap.remove(name, server);
syncMap.remove(name, server);
if (defaultDatabase == server) {
defaultDatabase = null;
}
} finally {
lock.unlock();
}
}
private void registerWithName(String name, Database server, boolean isDefault) {
lock.lock();
try {
@@ -246,4 +246,41 @@ public interface DtoQuery<T> extends CancelableQuery {
*/
DtoQuery<T> usingMaster(boolean useMaster);
/**
* Return a PagedList for this query using firstRow and maxRows.
* <p>
* The benefit of using this over findList() is that it provides functionality to get the
* total row count etc.
* <p>
* If maxRows is not set on the query prior to calling findPagedList() then a
* PersistenceException is thrown.
* <p>
* This is only supported for a DtoQuery that is derived from an ORM query via
* {@link Query#asDto(Class)} / {@link ExpressionList#asDto(Class)}. It is not supported
* for a DtoQuery based on raw SQL (e.g. via {@link Database#findDto(Class, String)}) as
* there is no query structure available from which to derive a matching row count query -
* a PersistenceException is thrown in that case.
* <pre>{@code
*
* PagedList<OrderDto> pagedList =
* DB.find(Order.class)
* .where().eq("status", Order.Status.NEW)
* .orderBy().asc("id")
* .setFirstRow(50)
* .setMaxRows(20)
* .asDto(OrderDto.class)
* .findPagedList();
*
* // fetch the total row count in the background
* pagedList.loadCount();
*
* List<OrderDto> orders = pagedList.getList();
* int totalRowCount = pagedList.getTotalCount();
*
* }</pre>
*
* @return The PagedList
*/
PagedList<T> findPagedList();
}
@@ -162,6 +162,18 @@ public class DatabasePlatform {
protected boolean selectCountWithAlias;
protected boolean selectCountWithColumnAlias;
/**
* Set true for platforms where {@code exists(...)} can only be used as a predicate
* and not as a directly selectable scalar boolean expression (e.g. SQL Server, Oracle).
*/
protected boolean existsWithCaseWhen;
/**
* Clause appended after the {@code case when exists(...) then 1 else 0 end} exists query
* for platforms that require a FROM clause on every select (e.g. {@code from dual} on Oracle).
*/
protected String existsFromClause = "";
/**
* If set then use the FORWARD ONLY hint when creating ResultSets for
* findIterate() and findVisit().
@@ -660,6 +672,21 @@ public class DatabasePlatform {
return selectCountWithColumnAlias;
}
/**
* Return true if a scalar boolean {@code exists(...)} expression is not supported
* as a select expression and needs to be wrapped as {@code case when exists(...) then 1 else 0 end}.
*/
public boolean existsWithCaseWhen() {
return existsWithCaseWhen;
}
/**
* Return the clause to append after the exists case-when wrapping (e.g. {@code from dual} on Oracle).
*/
public String existsFromClause() {
return existsFromClause;
}
public String completeSql(String sql, Query<?> query) {
if (query.isForUpdate()) {
@@ -1,6 +1,7 @@
package io.ebean.event;
import io.ebean.Database;
import io.ebean.DatabaseFactory;
import io.ebean.EbeanVersion;
import io.ebean.service.SpiContainer;
@@ -188,6 +189,7 @@ public final class ShutdownManager {
*/
public static void unregisterDatabase(Database server) {
databases.remove(server);
DatabaseFactory.unregister(server);
}
private static class ShutdownHook extends Thread {
+1 -1
View File
@@ -29,7 +29,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.2</version>
<version>42.7.11</version>
<optional>true</optional>
</dependency>
+1 -1
View File
@@ -149,7 +149,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.2</version>
<version>42.7.11</version>
<optional>true</optional>
</dependency>
@@ -15,4 +15,19 @@ public interface SpiQueryManyJoin {
*/
String fetchOrderBy();
/**
* Return true if this many relationship has an order column stored on the
* ManyToMany intersection table (rather than a target descriptor property).
*/
default boolean hasIntersectionOrderColumn() {
return false;
}
/**
* Return the db column name of the ManyToMany intersection table order column (or null).
*/
default String intersectionOrderColumn() {
return null;
}
}
@@ -1242,9 +1242,6 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
}
private <T> int delete(SpiQuery<T> query, boolean permanent) {
if (query.getId() != null && query.whereExpressions() == null && query.descriptor().isSoftDelete()) {
return executeInTrans((txn) -> persister.delete(query.getBeanType(), query.getId(), txn, permanent), query.transaction());
}
SpiOrmQueryRequest<T> request = createQueryRequest(Type.DELETE, query);
try {
request.initTransIfRequired();
@@ -2199,12 +2196,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
}
@Override
public Set<Property> checkUniqueness(Object bean) {
return checkUniqueness(bean, null);
}
@Override
public Set<Property> checkUniqueness(Object bean, @Nullable Transaction transaction) {
public Set<Property> checkUniqueness(Object bean, @Nullable Transaction transaction, boolean useQueryCache, boolean skipClean) {
EntityBean entityBean = checkEntityBean(bean);
BeanDescriptor<?> beanDesc = descriptor(entityBean.getClass());
BeanProperty idProperty = beanDesc.idProperty();
@@ -2216,14 +2208,15 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
if (entityBean._ebean_getIntercept().isNew() && id != null) {
// Primary Key is changeable only on new models - so skip check if we are not new
SpiQuery<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
query.setUseQueryCache(useQueryCache);
query.usingTransaction(transaction);
query.setId(id);
if (findCount(query) > 0) {
if (exists(query)) {
return Collections.singleton(idProperty);
}
}
for (BeanProperty[] props : beanDesc.uniqueProps()) {
Set<Property> ret = checkUniqueness(entityBean, beanDesc, props, transaction);
Set<Property> ret = checkUniqueness(entityBean, beanDesc, props, transaction, useQueryCache, skipClean);
if (ret != null) {
return ret;
}
@@ -2231,13 +2224,34 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
return Collections.emptySet();
}
/**
* Checks, if any property is dirty.
*/
private boolean isAnyPropertyDirty(EntityBean entityBean, BeanProperty[] props) {
if (entityBean._ebean_getIntercept().isNew()) {
return true;
}
for (BeanProperty prop : props) {
if (entityBean._ebean_getIntercept().isDirtyProperty(prop.propertyIndex())) {
return true;
}
}
return false;
}
/**
* Returns a set of properties if saving the bean will violate the unique constraints (defined by given properties).
*/
@Nullable
private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> beanDesc, BeanProperty[] props, @Nullable Transaction transaction) {
private Set<Property> checkUniqueness(EntityBean entityBean, BeanDescriptor<?> beanDesc, BeanProperty[] props, @Nullable Transaction transaction,
boolean useQueryCache, boolean skipClean) {
if (skipClean && !isAnyPropertyDirty(entityBean, props)) {
return null;
}
BeanProperty idProperty = beanDesc.idProperty();
SpiQuery<?> query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory);
query.setUseQueryCache(useQueryCache);
query.usingTransaction(transaction);
ExpressionList<?> exprList = query.where();
if (!entityBean._ebean_getIntercept().isNew()) {
@@ -2251,7 +2265,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
}
exprList.eq(prop.name(), value);
}
if (findCount(query) > 0) {
if (exists(query)) {
Set<Property> ret = new LinkedHashSet<>();
Collections.addAll(ret, props);
return ret;
@@ -43,6 +43,8 @@ import io.ebeaninternal.server.deploy.meta.DeployBeanDescriptor;
import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyLists;
import io.ebeaninternal.server.el.*;
import io.ebeaninternal.server.persist.DeleteMode;
import io.ebeaninternal.server.persist.MultiValueWrapper;
import io.ebeaninternal.server.type.ScalarTypeArray;
import io.ebeaninternal.server.query.*;
import io.ebeaninternal.server.querydefn.DefaultOrmQuery;
import io.ebeaninternal.server.querydefn.OrmQueryDetail;
@@ -753,7 +755,16 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
public void bindElementValue(SqlUpdate insert, Object value) {
EntityBean bean = (EntityBean) value;
for (BeanProperty property : propertiesBaseScalar) {
insert.setParameter(property.getValue(bean));
Object propertyValue = property.getValue(bean);
if (property.isArrayType() && propertyValue instanceof Collection) {
// Bind with the declared element type rather than relying on MultiValueWrapper's default
// constructor which infers the element type from the first value - this fails with a
// NoSuchElementException for an empty collection. See #2477.
Class<?> elementType = ((ScalarTypeArray) property.scalarType()).elementType();
insert.setParameter(new MultiValueWrapper((Collection<?>) propertyValue, elementType));
} else {
insert.setParameter(propertyValue);
}
}
}
@@ -372,14 +372,15 @@ abstract class BeanDescriptorCacheHelp<T> {
* Hit the bean cache with the given ids returning the hits.
*/
BeanCacheResult<T> cacheIdLookup(PersistenceContext context, boolean unmodifiable, Collection<?> ids) {
Set<Object> keys = new HashSet<>(ids.size());
for (Object id : ids) {
keys.add(desc.cacheKey(id));
}
if (ids.isEmpty()) {
return new BeanCacheResult<>();
}
Map<Object, Object> beanDataMap = beanCache().getAll(keys);
// map cacheKey -> original id to support type coercion
Map<Object, Object> keyToOriginalId = new HashMap<>(ids.size());
for (Object id : ids) {
keyToOriginalId.put(desc.cacheKey(id), id);
}
Map<Object, Object> beanDataMap = beanCache().getAll(keyToOriginalId.keySet());
if (beanLog.isLoggable(TRACE)) {
beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet());
}
@@ -387,7 +388,8 @@ abstract class BeanDescriptorCacheHelp<T> {
for (Map.Entry<Object, Object> entry : beanDataMap.entrySet()) {
CachedBeanData cachedBeanData = (CachedBeanData) entry.getValue();
T bean = convertToBean(entry.getKey(), unmodifiable, context, cachedBeanData);
result.add(bean, desc.id(bean));
Object originalId = keyToOriginalId.get(entry.getKey());
result.add(bean, originalId != null ? originalId : desc.id(bean));
}
return result;
}
@@ -907,8 +907,18 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
}
private void makeOrderColumn(DeployBeanPropertyAssocMany<?> oneToMany) {
DeployBeanDescriptor<?> targetDesc = targetDescriptor(oneToMany);
DeployOrderColumn orderColumn = oneToMany.getOrderColumn();
makeOrderColumn(oneToMany, targetDescriptor(oneToMany));
}
/**
* Create and assign the synthetic order column property onto the given target descriptor.
* <p>
* Used for both {@code @OneToMany} (targetDesc looked up via the target entity type) and
* {@code @ElementCollection} (targetDesc is the synthetic element descriptor which is not
* registered in {@code deployInfoMap} and so must be passed in directly).
*/
public void makeOrderColumn(DeployBeanPropertyAssocMany<?> many, DeployBeanDescriptor<?> targetDesc) {
DeployOrderColumn orderColumn = many.getOrderColumn();
final ScalarType<?> scalarType = typeManager.type(Integer.class);
DeployBeanProperty orderProperty = new DeployBeanProperty(targetDesc, Integer.class, scalarType, null);
orderProperty.setName(DeployOrderColumn.LOGICAL_NAME);
@@ -671,6 +671,35 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
setValue(entityBean, tenantId);
}
/**
* By default, getIntercept and setIntercept will check if the passed bean is an instance of the descriptor type.
* <p>
* If the property is not part of the type hierarchy (i.e. is not this property from this descriptor) an
* IllegalArgumentException is thrown.
* <p>
* If inheritance is involved, this method returns false instead of throwing an exception, if the property might
* exist on one of the sibling child beans. This is necessary for getIntercept, as it returns <code>null</code>
* in this case.
*
* @return true if the property can be accessed on the given bean, false if it should be treated as unloaded.
*/
private boolean checkPropertyAccess(EntityBean bean) {
if (bean == null || descriptor.type().isInstance(bean)) { // null = fall through - NPE is caught later.
return true;
}
InheritInfo inheritInfo = descriptor.inheritInfo();
if (inheritInfo == null || inheritInfo.isRoot() || !inheritInfo.getRoot().getType().isInstance(bean)) {
throw new IllegalArgumentException(propertyIncompatibleMsg(bean));
} else {
return false;
}
}
private String propertyIncompatibleMsg(EntityBean bean) {
String beanType = bean == null ? "null" : bean.getClass().getName();
return "Property " + name + " on [" + descriptor + "] is incompatible with type[" + beanType + "]";
}
/**
* Set the value of the property without interception or
* PropertyChangeSupport.
@@ -687,6 +716,9 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
* Set the value of the property.
*/
public void setValueIntercept(EntityBean bean, Object value) {
if (!checkPropertyAccess(bean)) {
throw new IllegalArgumentException(propertyIncompatibleMsg(bean));
}
try {
setter.setIntercept(bean, value);
} catch (Exception ex) {
@@ -798,6 +830,9 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
}
public Object getValueIntercept(EntityBean bean) {
if (!checkPropertyAccess(bean)) {
return null;
}
try {
return getter.getIntercept(bean);
} catch (Exception ex) {
@@ -57,6 +57,12 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
* Flag to indicate that the target has a order column to auto populate.
*/
private final boolean hasOrderColumn;
/**
* For ManyToMany, the db column name of the order column stored on the intersection
* table (null for OneToMany/ElementCollection which use a target descriptor property instead).
*/
private final String intersectionOrderColumn;
private final boolean intersectionOrderColumnNullable;
/**
* Flag to indicate manyToMany relationship.
*/
@@ -95,6 +101,8 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
this.o2mJoinTable = deploy.isO2mJoinTable();
this.hasOrderColumn = deploy.hasOrderColumn();
this.manyToMany = deploy.isManyToMany();
this.intersectionOrderColumn = (manyToMany && hasOrderColumn) ? deploy.getOrderColumn().getName() : null;
this.intersectionOrderColumnNullable = (manyToMany && hasOrderColumn) && deploy.getOrderColumn().isNullable();
this.elementCollection = deploy.isElementCollection();
this.elementDescriptor = deploy.getElementDescriptor();
this.manyType = deploy.getManyType();
@@ -154,6 +162,9 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
embeddedExportedProperties = exportedProperties[0].isEmbedded();
if (fetchOrderBy != null) {
lazyFetchOrderBy = sqlHelp.lazyFetchOrderBy(fetchOrderBy);
} else if (intersectionOrderColumn != null) {
// ManyToMany @OrderColumn - the intersection table is always aliased "int_"
lazyFetchOrderBy = sqlHelp.lazyFetchOrderBy("int_." + intersectionOrderColumn);
}
}
}
@@ -511,6 +522,29 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
return hasOrderColumn;
}
/**
* Return true if this is a ManyToMany with an order column stored on the intersection table.
*/
@Override
public boolean hasIntersectionOrderColumn() {
return intersectionOrderColumn != null;
}
/**
* Return the db column name of the ManyToMany intersection table order column (or null).
*/
@Override
public String intersectionOrderColumn() {
return intersectionOrderColumn;
}
/**
* Return true if the intersection table order column is nullable.
*/
public boolean isIntersectionOrderColumnNullable() {
return intersectionOrderColumnNullable;
}
public boolean isOrphanRemoval() {
return orphanRemoval;
}
@@ -234,6 +234,10 @@ public final class IdBinderSimple implements IdBinder {
@Override
public Object convertId(Object idValue) {
if (!idValue.getClass().equals(expectedType)) {
if (idValue instanceof String) {
// for cacheKey() formatted values
return scalarType.parse((String) idValue);
}
return scalarType.toBeanType(idValue);
}
return idValue;
@@ -89,6 +89,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
ManyToMany manyToMany = get(prop, ManyToMany.class);
if (manyToMany != null) {
readToMany(manyToMany, prop);
OrderColumn orderColumn = get(prop, OrderColumn.class);
if (orderColumn != null) {
// ManyToMany order value is stored on the intersection table (not the target bean)
prop.setOrderColumn(new DeployOrderColumn(orderColumn));
}
}
ElementCollection elementCollection = get(prop, ElementCollection.class);
if (elementCollection != null) {
@@ -177,6 +182,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
if (!elementCollection.targetClass().equals(void.class)) {
prop.setTargetType(elementCollection.targetClass());
}
OrderColumn orderColumn = get(prop, OrderColumn.class);
if (orderColumn != null) {
prop.setOrderColumn(new DeployOrderColumn(orderColumn));
prop.setFetchOrderBy(DeployOrderColumn.LOGICAL_NAME);
}
Column column = prop.getMetaAnnotation(Column.class);
if (column != null) {
prop.setDbColumn(column.name());
@@ -268,6 +278,11 @@ final class AnnotationAssocManys extends AnnotationAssoc {
elementDescriptor.setName(prop.toString());
factory.createUnidirectional(elementDescriptor, prop.getOwningType(), beanTable, prop.getTableJoin());
if (prop.hasOrderColumn()) {
// create the synthetic order property on the element descriptor - the element descriptor
// is not registered in deployInfoMap so this can't go through the usual OneToMany path
factory.makeOrderColumn(prop, elementDescriptor);
}
prop.setElementDescriptor(factory.createElementDescriptor(elementDescriptor, prop.getManyType(), scalar));
}
@@ -91,4 +91,16 @@ public interface ElPropertyDeploy extends SpiQueryManyJoin {
default String fetchOrderBy() {
return beanProperty().fetchOrderBy();
}
@Override
default boolean hasIntersectionOrderColumn() {
BeanProperty prop = beanProperty();
return prop instanceof io.ebeaninternal.server.deploy.BeanPropertyAssocMany
&& prop.hasIntersectionOrderColumn();
}
@Override
default String intersectionOrderColumn() {
return beanProperty().intersectionOrderColumn();
}
}
@@ -256,6 +256,12 @@ final class SaveManyBeans extends SaveManyBase {
}
private void saveAssocManyIntersection(boolean queue) {
if (many.hasIntersectionOrderColumn()) {
// With @OrderColumn the position of every row can change on any add/remove/reorder so
// we always delete all intersection rows and reinsert them in the current list order.
saveAssocManyIntersectionOrdered(queue);
return;
}
final boolean vanillaCollection = !(value instanceof BeanCollection<?>);
if (vanillaCollection || forcedUpdate) {
// delete all intersection rows and then treat all
@@ -340,6 +346,53 @@ final class SaveManyBeans extends SaveManyBase {
transaction.depth(-1);
}
/**
* Save the ManyToMany intersection rows for a property with an {@code @OrderColumn}.
* <p>
* Unlike the standard diff based save (additions/removals), this always deletes all existing
* intersection rows for the parent and reinserts every current entry in list order, binding
* the sequential order index. This is required because a pure reorder (no add/remove) would
* not otherwise be detected/persisted, and there is no per-row place (unlike OneToMany/
* ElementCollection) to compare an existing 'loaded' order against - the order value lives on
* the intersection row, not on the target bean.
*/
private void saveAssocManyIntersectionOrdered(boolean queue) {
if (value == null) {
return;
}
Collection<?> current;
if (value instanceof Map<?, ?>) {
current = ((Map<?, ?>) value).values();
} else if (value instanceof Collection<?>) {
current = (Collection<?>) value;
} else {
throw new PersistenceException("Unhandled ManyToMany type " + value.getClass().getName() + " for " + many.fullName());
}
if (value instanceof BeanCollection<?>) {
BeanCollection<?> manyValue = (BeanCollection<?>) value;
setListenMode(manyValue, many);
manyValue.modifyReset();
}
if (!insertedParent) {
request.preManyToManyUpdate();
persister.deleteManyIntersection(parentBean, many, transaction, publish, queue);
}
String orderColumn = many.intersectionOrderColumn();
transaction.depth(+1);
int position = 0;
for (Object other : current) {
EntityBean otherBean = (EntityBean) other;
if (!many.hasImportedId(otherBean)) {
throw new PersistenceException("ManyToMany bean does not have an Id value? " + otherBean);
}
IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherBean, publish);
intRow.put(orderColumn, position++);
SpiSqlUpdate sqlInsert = intRow.createInsert(server);
persister.executeOrQueue(sqlInsert, transaction, queue, BatchControl.INSERT_QUEUE);
}
transaction.depth(-1);
}
private boolean isChangedProperty() {
return request.isChangedProperty(many.propertyIndex());
}
@@ -44,10 +44,15 @@ final class SaveManyElementCollection extends SaveManyBase {
private void saveCollection() {
SpiSqlUpdate proto = many.insertElementCollection();
Object parentId = request.beanId();
boolean hasOrderColumn = many.hasOrderColumn();
int position = 0;
for (Object value : collection) {
final SpiSqlUpdate sqlInsert = proto.copy();
sqlInsert.setParameter(parentId);
many.bindElementValue(sqlInsert, value);
if (hasOrderColumn) {
sqlInsert.setParameter(position++);
}
persister.addToFlushQueue(sqlInsert, transaction, BatchControl.INSERT_QUEUE);
}
resetModifyState();
@@ -60,6 +60,10 @@ final class BindableIdEmbedded implements BindableId {
@Override
public void dmlBind(BindableRequest request, EntityBean bean) throws SQLException {
EntityBean idValue = (EntityBean) embId.getValue(bean);
if (idValue == null && matches != null) {
// The id (e.g. IdClass) hasn't been derived/cached on this bean yet
idValue = deriveId(bean);
}
for (BeanProperty prop : props) {
Object value = prop.getValue(idValue);
request.bind(value, prop);
@@ -83,13 +87,21 @@ final class BindableIdEmbedded implements BindableId {
@Override
public boolean deriveConcatenatedId(PersistRequestBean<?> persist) {
deriveId(persist.entityBean());
return true;
}
/**
* Derive/build the concatenated id (e.g. IdClass) from the entity's own matching id
* properties, caching it on the bean (via setValueIntercept) for subsequent use.
*/
private EntityBean deriveId(EntityBean bean) {
if (matches == null) {
String m = "No matches for " + embId.fullName() + " the concatenated key columns where not found?"
+ " I expect that the concatenated key was null, and this bean does"
+ " not have ManyToOne assoc beans matching the primary key columns?";
throw new PersistenceException(m);
}
EntityBean bean = persist.entityBean();
// create the new id
EntityBean newId = (EntityBean) embId.createEmbeddedId();
// populate it from the assoc one id values...
@@ -97,7 +109,7 @@ final class BindableIdEmbedded implements BindableId {
match.populate(bean, newId);
}
embId.setValueIntercept(bean, newId);
return true;
return newId;
}
}
@@ -336,7 +336,10 @@ final class CQueryBuilder {
return sql;
}
private String wrapSelectExists(String sql) {
static String wrapSelectExists(String sql, boolean existsWithCaseWhen, String existsFromClause) {
if (existsWithCaseWhen) {
return "select case when exists(" + sql + ") then 1 else 0 end" + existsFromClause;
}
return "select exists(" + sql + ")";
}
@@ -366,7 +369,7 @@ final class CQueryBuilder {
}
SqlLimitResponse s = buildSql("select 1", request, predicates, sqlTree);
String sql = wrapSelectExists(s.getSql());
String sql = wrapSelectExists(s.getSql(), dbPlatform.existsWithCaseWhen(), dbPlatform.existsFromClause());
queryPlan = new CQueryPlan(request, sql, sqlTree.plan(), predicates.logWhereSql());
request.putQueryPlan(queryPlan);
@@ -33,6 +33,13 @@ import static java.lang.System.Logger.Level.WARNING;
*/
public final class CQueryPredicates {
/**
* Default lower bound used for findVersions() (no explicit start/end) on sql2011
* standards based platforms that require actual bind values for the root table's
* 'for system_time between ? and ?' clause.
*/
private static final Timestamp EPOCH = Timestamp.valueOf("1970-01-01 00:00:00");
private final Binder binder;
private final OrmQueryRequest<?> request;
private final SpiQuery<?> query;
@@ -60,6 +67,12 @@ public final class CQueryPredicates {
private String dbOrderBy;
private String dbDistinctOn;
private String dbUpdateClause;
/**
* Set when the many join is a ManyToMany with an intersection table order column - used to
* resolve the literal "${path}dbColumn" marker appended to dbOrderBy in parseTableAlias().
*/
private String intersectionOrderPath;
private String intersectionOrderColumn;
/**
* Includes from where and order by clauses.
*/
@@ -84,10 +97,16 @@ public final class CQueryPredicates {
// bind the update set clause
updateProperties.bind(binder, dataBind);
}
if (query.isVersionsBetween() && binder.isAsOfStandardsBased()) {
if (binder.isAsOfStandardsBased() && query.temporalMode() == SpiQuery.TemporalMode.VERSIONS) {
// sql2011 based versions between timestamp syntax
Timestamp start = query.versionStart();
Timestamp end = query.versionEnd();
if (start == null) {
start = EPOCH;
}
if (end == null) {
end = new Timestamp(System.currentTimeMillis());
}
dataBind.append("between ").append(start).append(" and ").append(end);
binder.bindObject(dataBind, start);
binder.bindObject(dataBind, end);
@@ -239,6 +258,14 @@ public final class CQueryPredicates {
dbHaving = alias.parseWhere(dbHaving);
}
if (dbOrderBy != null) {
if (intersectionOrderColumn != null) {
// resolve the ManyToMany intersection table order column BEFORE the generic
// alias substitution runs, as it needs the "z_" suffixed intersection alias
// rather than the plain target table alias.
String marker = "${" + intersectionOrderPath + "}" + intersectionOrderColumn;
String targetAlias = alias.tableAlias(intersectionOrderPath);
dbOrderBy = dbOrderBy.replace(marker, targetAlias + "z_." + intersectionOrderColumn);
}
dbOrderBy = alias.parse(dbOrderBy);
}
if (dbDistinctOn != null) {
@@ -268,9 +295,18 @@ public final class CQueryPredicates {
}
// check for default ordering on the many property...
SpiQueryManyJoin manyProp = request.manyJoin();
String manyOrderBy = manyProp.fetchOrderBy();
if (manyOrderBy != null) {
orderBy = orderBy + ", " + parser.parse(CQueryBuilder.prefixOrderByFields(manyProp.path(), manyOrderBy));
if (manyProp.hasIntersectionOrderColumn()) {
// ManyToMany with @OrderColumn on the intersection table - the column lives on the
// intersection table (alias "<targetAlias>z_") rather than a normal property path,
// so we append a literal marker that parseTableAlias() resolves directly.
intersectionOrderPath = manyProp.path();
intersectionOrderColumn = manyProp.intersectionOrderColumn();
orderBy = orderBy + ", ${" + intersectionOrderPath + "}" + intersectionOrderColumn;
} else {
String manyOrderBy = manyProp.fetchOrderBy();
if (manyOrderBy != null) {
orderBy = orderBy + ", " + parser.parse(CQueryBuilder.prefixOrderByFields(manyProp.path(), manyOrderBy));
}
}
if (request.isFindById()) {
// only one master bean so should be fine...
@@ -43,6 +43,16 @@ public interface STreePropertyAssocMany extends STreePropertyAssoc {
*/
boolean hasJoinTable();
/**
* Return true if this is a ManyToMany with an order column stored on the intersection table.
*/
boolean hasIntersectionOrderColumn();
/**
* Return the db column name of the ManyToMany intersection table order column (or null).
*/
String intersectionOrderColumn();
/**
* Return the intersection table join.
*/
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.querydefn;
import org.jspecify.annotations.NullMarked;
import io.ebean.DtoQuery;
import io.ebean.PagedList;
import io.ebean.ProfileLocation;
import io.ebean.QueryIterator;
import io.ebean.Transaction;
@@ -11,6 +12,7 @@ import io.ebeaninternal.server.dto.DtoMappingRequest;
import io.ebeaninternal.server.dto.DtoQueryPlan;
import io.ebeaninternal.server.transaction.ExternalJdbcTransaction;
import jakarta.persistence.PersistenceException;
import javax.annotation.Nullable;
import java.sql.Connection;
import java.util.Collection;
@@ -50,6 +52,8 @@ public final class DefaultDtoQuery<T> extends AbstractQuery implements SpiDtoQue
this.useMaster = ormQuery.isUseMaster();
this.label = ormQuery.label();
this.profileLocation = ormQuery.profileLocation();
this.firstRow = ormQuery.getFirstRow();
this.maxRows = ormQuery.getMaxRows();
}
/**
@@ -135,6 +139,24 @@ public final class DefaultDtoQuery<T> extends AbstractQuery implements SpiDtoQue
return server.findDtoList(this);
}
@Override
public PagedList<T> findPagedList() {
if (ormQuery == null) {
throw new PersistenceException("findPagedList() is only supported for a DtoQuery derived from an ORM query");
}
if (maxRows == 0) {
throw new PersistenceException("maxRows must be specified for findPagedList()");
}
// Use an independent copy for the row count query. The ormQuery instance is mutated with
// a (potentially since ended/inactive) implicit transaction when the DTO list is executed,
// so the count query must not share that transaction reference - instead it uses the
// transaction explicitly bound to this DtoQuery (if any), consistent with a plain
// Query.findPagedList().
SpiQuery<?> countQuery = ormQuery.copy();
countQuery.usingTransaction(transaction);
return new DtoPagedList<>(server, this, countQuery);
}
@Nullable
@Override
public T findOne() {
@@ -0,0 +1,146 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.PagedList;
import io.ebeaninternal.api.SpiDtoQuery;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import jakarta.persistence.PersistenceException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.locks.ReentrantLock;
/**
* PagedList implementation for a DtoQuery that is derived from an underlying ORM query
* (created via {@code Query.asDto()}).
* <p>
* The page of DTO beans is fetched via the DtoQuery while the total row count is derived
* from the underlying ORM query (which has the matching where clause, joins etc).
*/
final class DtoPagedList<T> implements PagedList<T> {
private final transient SpiEbeanServer server;
private final transient ReentrantLock lock = new ReentrantLock();
private final SpiDtoQuery<T> dtoQuery;
private final SpiQuery<?> countQuery;
private final int firstRow;
private final int maxRows;
private int totalRowCount = -1;
private Future<Integer> futureRowCount;
private List<T> list;
/**
* Construct with the dto query and the underlying ORM query used to derive the row count.
*/
DtoPagedList(SpiEbeanServer server, SpiDtoQuery<T> dtoQuery, SpiQuery<?> countQuery) {
this.server = server;
this.dtoQuery = dtoQuery;
this.countQuery = countQuery;
this.maxRows = countQuery.getMaxRows();
this.firstRow = countQuery.getFirstRow();
}
@Override
public void loadCount() {
getFutureCount();
}
@Override
public Future<Integer> getFutureCount() {
lock.lock();
try {
if (futureRowCount == null) {
futureRowCount = server.findFutureCount(countQuery);
}
return futureRowCount;
} finally {
lock.unlock();
}
}
@Override
public List<T> getList() {
lock.lock();
try {
if (list == null) {
if (totalRowCount == 0) {
// already count and no rows
list = Collections.emptyList();
} else {
list = server.findDtoList(dtoQuery);
}
}
return list;
} finally {
lock.unlock();
}
}
@Override
public int getPageIndex() {
if (firstRow == 0) {
return 0;
}
return ((firstRow - 1) / maxRows) + 1;
}
@Override
public int getTotalPageCount() {
int rowCount = getTotalCount();
if (rowCount == 0) {
return 0;
} else {
return ((rowCount - 1) / maxRows) + 1;
}
}
@Override
public int getTotalCount() {
lock.lock();
try {
if (totalRowCount > -1) {
return totalRowCount;
}
if (futureRowCount != null) {
try {
// background query already initiated so get it with a wait
totalRowCount = futureRowCount.get();
return totalRowCount;
} catch (Exception e) {
throw new PersistenceException(e);
}
}
// just using foreground thread
totalRowCount = server.findCount(countQuery);
return totalRowCount;
} finally {
lock.unlock();
}
}
@Override
public boolean hasNext() {
return (firstRow + maxRows) < getTotalCount();
}
@Override
public boolean hasPrev() {
return firstRow > 0;
}
@Override
public int getPageSize() {
return maxRows;
}
@Override
public String getDisplayXtoYofZ(String to, String of) {
int first = firstRow + 1;
int last = firstRow + getList().size();
int total = getTotalCount();
return first + to + last + of + total;
}
}
@@ -265,7 +265,7 @@ public final class DefaultTypeManager implements TypeManager {
@Override
public ScalarType<?> dbMapType() {
return hstoreSupport() ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR, false);
return hstoreSupport() ? hstoreType : ScalarTypeJsonMap.typeFor(false, Types.VARCHAR, MutationDetection.DEFAULT);
}
@Override
@@ -322,17 +322,17 @@ public final class DefaultTypeManager implements TypeManager {
}
Type genericType = prop.genericType();
if (type.equals(List.class) && isValueTypeSimple(genericType)) {
return ScalarTypeJsonList.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), keepSource(prop));
return ScalarTypeJsonList.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), collectionMutationDetection(prop));
}
if (type.equals(Set.class) && isValueTypeSimple(genericType)) {
return ScalarTypeJsonSet.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), keepSource(prop));
return ScalarTypeJsonSet.typeFor(postgres, dbType, docType(genericType), prop.isNullable(), collectionMutationDetection(prop));
}
if (type.equals(Map.class) && isBuiltinJsonMap(genericType)) {
Type keyType = TypeReflectHelper.getMapKeyTypeRaw(genericType);
if (isEnumType(keyType)) {
return enumJsonMapType(postgres, dbType, keyType, keepSource(prop));
return enumJsonMapType(postgres, dbType, keyType, collectionMutationDetection(prop));
}
return ScalarTypeJsonMap.typeFor(postgres, dbType, keepSource(prop));
return ScalarTypeJsonMap.typeFor(postgres, dbType, collectionMutationDetection(prop));
}
if (objectMapperPresent && prop.mutationDetection() == MutationDetection.DEFAULT) {
ScalarTypeSet<?> typeSet = typeSets.get(type);
@@ -343,18 +343,25 @@ public final class DefaultTypeManager implements TypeManager {
return createJsonObjectMapperType(prop, dbType, DocPropertyType.OBJECT);
}
private boolean keepSource(DeployProperty prop) {
if (prop.mutationDetection() == MutationDetection.DEFAULT) {
prop.setMutationDetection(jsonManager != null ? jsonManager.mutationDetection() : MutationDetection.NONE);
}
return prop.mutationDetection() == MutationDetection.SOURCE;
/**
* Return the mutation detection mode to use for the built-in JSON collection types
* (Map, List, Set).
* <p>
* Unlike {@code @DbJson} properties handled via the Jackson ObjectMapper, {@code DEFAULT}
* on these collection types is <em>not</em> resolved against the DatabaseConfig wide
* default - it always uses the legacy ModifyAware wrapper based dirty checking. Only an
* explicit {@code NONE}, {@code HASH} or {@code SOURCE} on the property itself switches
* these types away from ModifyAware based checking.
*/
private MutationDetection collectionMutationDetection(DeployProperty prop) {
return prop.mutationDetection();
}
@SuppressWarnings("unchecked")
private ScalarType<?> enumJsonMapType(boolean postgres, int dbType, Type keyType, boolean keepSource) {
private ScalarType<?> enumJsonMapType(boolean postgres, int dbType, Type keyType, MutationDetection mutationDetection) {
Class<? extends Enum<?>> enumClass = asEnumClass(keyType);
ScalarType<? extends Enum<?>> enumScalarType = (ScalarType<? extends Enum<?>>) enumType(enumClass, null);
return ScalarTypeJsonMapEnum.typeFor(postgres, dbType, enumScalarType, keepSource);
return ScalarTypeJsonMapEnum.typeFor(postgres, dbType, enumScalarType, mutationDetection);
}
private DocPropertyType docPropertyType(DeployProperty prop, Class<?> type) {
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.type;
import io.ebean.annotation.MutationDetection;
import io.ebean.core.type.DocPropertyType;
import io.ebean.core.type.ScalarType;
@@ -27,15 +28,14 @@ class PlatformArrayTypeJsonList implements PlatformArrayTypeFactory {
@Override
public ScalarType<?> typeFor(Type valueType, boolean nullable) {
if (valueType.equals(UUID.class)) {
// TODO: keepSource for @DbArray?
return new ScalarTypeJsonList.VarcharWithConverter(DocPropertyType.UUID, nullable, false, ArrayElementConverter.UUID);
return new ScalarTypeJsonList.VarcharWithConverter(DocPropertyType.UUID, nullable, ArrayElementConverter.UUID);
}
return new ScalarTypeJsonList(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, false);
return new ScalarTypeJsonList(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, MutationDetection.DEFAULT);
}
@Override
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
final ArrayElementConverter.EnumConverter converter = new ArrayElementConverter.EnumConverter(scalarType);
return new ScalarTypeJsonList.VarcharWithConverter(scalarType.docType(), nullable, false, converter);
return new ScalarTypeJsonList.VarcharWithConverter(scalarType.docType(), nullable, converter);
}
}
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.type;
import io.ebean.annotation.MutationDetection;
import io.ebean.core.type.DocPropertyType;
import io.ebean.core.type.ScalarType;
@@ -27,15 +28,14 @@ class PlatformArrayTypeJsonSet implements PlatformArrayTypeFactory {
@Override
public ScalarType<?> typeFor(Type valueType, boolean nullable) {
if (valueType.equals(UUID.class)) {
// TODO: keepSource for @DbArray?
return new ScalarTypeJsonSet.VarcharWithConverter(DocPropertyType.UUID, nullable, false, ArrayElementConverter.UUID);
return new ScalarTypeJsonSet.VarcharWithConverter(DocPropertyType.UUID, nullable, ArrayElementConverter.UUID);
}
return new ScalarTypeJsonSet(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, false);
return new ScalarTypeJsonSet(java.sql.Types.VARCHAR, JsonStorage.VARCHAR, docType(valueType), nullable, MutationDetection.DEFAULT);
}
@Override
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
final ArrayElementConverter.EnumConverter converter = new ArrayElementConverter.EnumConverter(scalarType);
return new ScalarTypeJsonSet.VarcharWithConverter(scalarType.docType(), nullable, false, converter);
return new ScalarTypeJsonSet.VarcharWithConverter(scalarType.docType(), nullable, converter);
}
}
@@ -10,4 +10,12 @@ public interface ScalarTypeArray {
*/
String getDbColumnDefn();
/**
* Return the Java type of the individual array elements.
* <p>
* Used to bind the array correctly when the collection value is empty
* and so the element type can't be determined from the collection content.
*/
Class<?> elementType();
}
@@ -45,31 +45,31 @@ class ScalarTypeArrayList extends ScalarTypeArrayBase<List> implements ScalarTyp
try {
String key = valueType + ":" + nullable;
if (valueType.equals(UUID.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID, UUID.class));
}
if (valueType.equals(Long.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG, Long.class));
}
if (valueType.equals(Integer.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER, Integer.class));
}
if (valueType.equals(Float.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "float4", DocPropertyType.DOUBLE, ArrayElementConverter.FLOAT));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "float4", DocPropertyType.DOUBLE, ArrayElementConverter.FLOAT, Float.class));
}
if (valueType.equals(Double.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE, Double.class));
}
if (valueType.equals(BigDecimal.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "decimal", DocPropertyType.DOUBLE, ArrayElementConverter.BIG_DECIMAL));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "decimal", DocPropertyType.DOUBLE, ArrayElementConverter.BIG_DECIMAL, BigDecimal.class));
}
if (valueType.equals(String.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING, String.class));
}
if (valueType.equals(Instant.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "timestamptz", DocPropertyType.TEXT, ArrayElementConverter.INSTANT));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "timestamptz", DocPropertyType.TEXT, ArrayElementConverter.INSTANT, Instant.class));
}
if (valueType.equals(LocalDate.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "date", DocPropertyType.TEXT, ArrayElementConverter.LOCAL_DATE));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayList(nullable, "date", DocPropertyType.TEXT, ArrayElementConverter.LOCAL_DATE, LocalDate.class));
}
throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping");
} finally {
@@ -79,18 +79,24 @@ class ScalarTypeArrayList extends ScalarTypeArrayBase<List> implements ScalarTyp
@Override
public ScalarTypeArrayList typeForEnum(ScalarType<?> scalarType, boolean nullable) {
return new ScalarTypeArrayList(nullable, arrayTypeFor(scalarType), scalarType.docType(), new ArrayElementConverter.EnumConverter(scalarType));
return new ScalarTypeArrayList(nullable, arrayTypeFor(scalarType), scalarType.docType(), new ArrayElementConverter.EnumConverter(scalarType), scalarType.type());
}
}
private final String arrayType;
private final ArrayElementConverter converter;
private final Class<?> elementType;
public ScalarTypeArrayList(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
public ScalarTypeArrayList(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter, Class<?> elementType) {
super(List.class, Types.ARRAY, docPropertyType, nullable);
this.arrayType = arrayType;
this.converter = converter;
this.elementType = elementType;
}
@Override
public Class<?> elementType() {
return elementType;
}
@Override
@@ -40,31 +40,31 @@ final class ScalarTypeArrayListH2 extends ScalarTypeArrayList {
try {
String key = valueType + ":" + nullable;
if (valueType.equals(UUID.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID, UUID.class));
}
if (valueType.equals(Long.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG, Long.class));
}
if (valueType.equals(Integer.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER, Integer.class));
}
if (valueType.equals(Float.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "real", DocPropertyType.DOUBLE, ArrayElementConverter.FLOAT));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "real", DocPropertyType.DOUBLE, ArrayElementConverter.FLOAT, Float.class));
}
if (valueType.equals(Double.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE, Double.class));
}
if (valueType.equals(BigDecimal.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.BIG_DECIMAL));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.BIG_DECIMAL, BigDecimal.class));
}
if (valueType.equals(String.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING, String.class));
}
if (valueType.equals(Instant.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "timestamp", DocPropertyType.TEXT, ArrayElementConverter.INSTANT));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "timestamp", DocPropertyType.TEXT, ArrayElementConverter.INSTANT, Instant.class));
}
if (valueType.equals(LocalDate.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "date", DocPropertyType.TEXT, ArrayElementConverter.LOCAL_DATE));
return cache.computeIfAbsent(key, s -> new ScalarTypeArrayListH2(nullable, "date", DocPropertyType.TEXT, ArrayElementConverter.LOCAL_DATE, LocalDate.class));
}
throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping");
} finally {
@@ -74,12 +74,12 @@ final class ScalarTypeArrayListH2 extends ScalarTypeArrayList {
@Override
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
return new ScalarTypeArrayListH2(nullable, "varchar", DocPropertyType.TEXT, new ArrayElementConverter.EnumConverter(scalarType));
return new ScalarTypeArrayListH2(nullable, "varchar", DocPropertyType.TEXT, new ArrayElementConverter.EnumConverter(scalarType), scalarType.type());
}
}
private ScalarTypeArrayListH2(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
super(nullable, arrayType, docPropertyType, converter);
private ScalarTypeArrayListH2(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter, Class<?> elementType) {
super(nullable, arrayType, docPropertyType, converter, elementType);
}
@Override
@@ -42,19 +42,19 @@ class ScalarTypeArraySet extends ScalarTypeArrayBase<Set> implements ScalarTypeA
try {
String key = valueType + ":" + nullable;
if (valueType.equals(UUID.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID, UUID.class));
}
if (valueType.equals(Long.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG, Long.class));
}
if (valueType.equals(Integer.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER, Integer.class));
}
if (valueType.equals(Double.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE, Double.class));
}
if (valueType.equals(String.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySet(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING, String.class));
}
throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping");
} finally {
@@ -64,18 +64,24 @@ class ScalarTypeArraySet extends ScalarTypeArrayBase<Set> implements ScalarTypeA
@Override
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
return new ScalarTypeArraySet(nullable, arrayTypeFor(scalarType), scalarType.docType(), new ArrayElementConverter.EnumConverter(scalarType));
return new ScalarTypeArraySet(nullable, arrayTypeFor(scalarType), scalarType.docType(), new ArrayElementConverter.EnumConverter(scalarType), scalarType.type());
}
}
private final String arrayType;
private final ArrayElementConverter converter;
private final Class<?> elementType;
public ScalarTypeArraySet(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
public ScalarTypeArraySet(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter, Class<?> elementType) {
super(Set.class, Types.ARRAY, docPropertyType, nullable);
this.arrayType = arrayType;
this.converter = converter;
this.elementType = elementType;
}
@Override
public Class<?> elementType() {
return elementType;
}
@Override
@@ -37,19 +37,19 @@ final class ScalarTypeArraySetH2 extends ScalarTypeArraySet {
try {
String key = valueType + ":" + nullable;
if (valueType.equals(UUID.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "uuid", DocPropertyType.UUID, ArrayElementConverter.UUID, UUID.class));
}
if (valueType.equals(Long.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "bigint", DocPropertyType.LONG, ArrayElementConverter.LONG, Long.class));
}
if (valueType.equals(Integer.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "integer", DocPropertyType.INTEGER, ArrayElementConverter.INTEGER, Integer.class));
}
if (valueType.equals(Double.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "float", DocPropertyType.DOUBLE, ArrayElementConverter.DOUBLE, Double.class));
}
if (valueType.equals(String.class)) {
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING));
return cache.computeIfAbsent(key, s -> new ScalarTypeArraySetH2(nullable, "varchar", DocPropertyType.TEXT, ArrayElementConverter.STRING, String.class));
}
throw new IllegalArgumentException("Type [" + valueType + "] not supported for @DbArray mapping");
} finally {
@@ -59,13 +59,13 @@ final class ScalarTypeArraySetH2 extends ScalarTypeArraySet {
@Override
public ScalarType<?> typeForEnum(ScalarType<?> scalarType, boolean nullable) {
return new ScalarTypeArraySetH2(nullable, "varchar", DocPropertyType.TEXT, new ArrayElementConverter.EnumConverter(scalarType));
return new ScalarTypeArraySetH2(nullable, "varchar", DocPropertyType.TEXT, new ArrayElementConverter.EnumConverter(scalarType), scalarType.type());
}
}
@SuppressWarnings("rawtypes")
private ScalarTypeArraySetH2(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter) {
super(nullable, arrayType, docPropertyType, converter);
private ScalarTypeArraySetH2(boolean nullable, String arrayType, DocPropertyType docPropertyType, ArrayElementConverter converter, Class<?> elementType) {
super(nullable, arrayType, docPropertyType, converter, elementType);
}
@Override
@@ -1,7 +1,10 @@
package io.ebeaninternal.server.type;
import io.ebean.annotation.MutationDetection;
import io.ebean.core.type.DocPropertyType;
import java.util.UUID;
/**
* Base for the JSON collection value types (List, Set).
* <p>
@@ -10,9 +13,9 @@ import io.ebean.core.type.DocPropertyType;
*/
abstract class ScalarTypeJsonCollectionValue<T> extends ScalarTypeJsonValue<T> implements ScalarTypeArray {
ScalarTypeJsonCollectionValue(Class<T> type, int jdbcType, JsonStorage storage, boolean keepSource,
ScalarTypeJsonCollectionValue(Class<T> type, int jdbcType, JsonStorage storage, MutationDetection mutationDetection,
boolean nullable, DocPropertyType docType) {
super(type, jdbcType, storage, keepSource, nullable, "[]", docType);
super(type, jdbcType, storage, mutationDetection, nullable, "[]", docType);
}
@Override
@@ -29,4 +32,27 @@ abstract class ScalarTypeJsonCollectionValue<T> extends ScalarTypeJsonValue<T> i
return "varchar[]";
}
}
/**
* Derive the element type from the docType - used only to determine the ScalarType to use
* when binding an element (e.g. for an empty collection where the element type can't be
* determined from the collection content).
*/
@Override
public Class<?> elementType() {
switch (docType()) {
case UUID:
return UUID.class;
case SHORT:
case INTEGER:
return Integer.class;
case LONG:
return Long.class;
case FLOAT:
case DOUBLE:
return Double.class;
default:
return String.class;
}
}
}
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.type;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.ebean.annotation.MutationDetection;
import io.ebean.config.dbplatform.DbPlatformType;
import io.ebean.core.type.DocPropertyType;
import io.ebean.core.type.PostgresHelper;
@@ -25,20 +26,20 @@ class ScalarTypeJsonList extends ScalarTypeJsonCollectionValue<List> {
/**
* Return the appropriate ScalarType for the requested dbType and platform.
*/
static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) {
static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) {
if (postgres) {
switch (dbType) {
case DbPlatformType.JSONB:
return new ScalarTypeJsonList(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, keepSource);
return new ScalarTypeJsonList(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, mutationDetection);
case DbPlatformType.JSON:
return new ScalarTypeJsonList(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, keepSource);
return new ScalarTypeJsonList(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, mutationDetection);
}
}
return new ScalarTypeJsonList(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource);
return new ScalarTypeJsonList(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, mutationDetection);
}
ScalarTypeJsonList(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, boolean keepSource) {
super(List.class, jdbcType, storage, keepSource, nullable, docType);
ScalarTypeJsonList(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) {
super(List.class, jdbcType, storage, mutationDetection, nullable, docType);
}
@Override
@@ -87,8 +88,8 @@ class ScalarTypeJsonList extends ScalarTypeJsonCollectionValue<List> {
private final ArrayElementConverter converter;
VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) {
super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource);
VarcharWithConverter(DocPropertyType docType, boolean nullable, ArrayElementConverter converter) {
super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, MutationDetection.DEFAULT);
this.converter = converter;
}
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.type;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.ebean.annotation.MutationDetection;
import io.ebean.config.dbplatform.DbPlatformType;
import io.ebean.core.type.DocPropertyType;
import io.ebean.core.type.PostgresHelper;
@@ -22,8 +23,8 @@ class ScalarTypeJsonMap extends ScalarTypeJsonValue<Map> {
/**
* Return the ScalarType for the requested dbType and platform.
*/
static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, boolean keepSource) {
return new ScalarTypeJsonMap(storageFor(postgres, dbType), keepSource);
static ScalarTypeJsonMap typeFor(boolean postgres, int dbType, MutationDetection mutationDetection) {
return new ScalarTypeJsonMap(storageFor(postgres, dbType), mutationDetection);
}
/**
@@ -47,8 +48,8 @@ class ScalarTypeJsonMap extends ScalarTypeJsonValue<Map> {
}
}
ScalarTypeJsonMap(JsonStorage storage, boolean keepSource) {
super(Map.class, storage.jdbcType(), storage, keepSource, true, null, DocPropertyType.OBJECT);
ScalarTypeJsonMap(JsonStorage storage, MutationDetection mutationDetection) {
super(Map.class, storage.jdbcType(), storage, mutationDetection, true, null, DocPropertyType.OBJECT);
}
@Override
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.type;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.ebean.annotation.MutationDetection;
import io.ebean.core.type.ScalarType;
import io.ebean.text.TextException;
import io.ebean.text.json.EJson;
@@ -23,13 +24,13 @@ final class ScalarTypeJsonMapEnum<T extends Enum<T>> extends ScalarTypeJsonMap {
private final ScalarType<T> enumType;
static ScalarType<?> typeFor(boolean postgres, int dbType, ScalarType<? extends Enum<?>> enumType, boolean keepSource) {
return new ScalarTypeJsonMapEnum<>(storageFor(postgres, dbType), enumType, keepSource);
static ScalarType<?> typeFor(boolean postgres, int dbType, ScalarType<? extends Enum<?>> enumType, MutationDetection mutationDetection) {
return new ScalarTypeJsonMapEnum<>(storageFor(postgres, dbType), enumType, mutationDetection);
}
@SuppressWarnings("unchecked")
private ScalarTypeJsonMapEnum(JsonStorage storage, ScalarType<? extends Enum> enumType, boolean keepSource) {
super(storage, keepSource);
private ScalarTypeJsonMapEnum(JsonStorage storage, ScalarType<? extends Enum> enumType, MutationDetection mutationDetection) {
super(storage, mutationDetection);
this.enumType = (ScalarType<T>) enumType;
}
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.type;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.ebean.annotation.MutationDetection;
import io.ebean.config.dbplatform.DbPlatformType;
import io.ebean.core.type.DocPropertyType;
import io.ebean.core.type.PostgresHelper;
@@ -25,20 +26,20 @@ class ScalarTypeJsonSet extends ScalarTypeJsonCollectionValue<Set> {
/**
* Return the appropriate ScalarType for the requested dbType and platform.
*/
static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, boolean keepSource) {
static ScalarType<?> typeFor(boolean postgres, int dbType, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) {
if (postgres) {
switch (dbType) {
case DbPlatformType.JSONB:
return new ScalarTypeJsonSet(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, keepSource);
return new ScalarTypeJsonSet(DbPlatformType.JSONB, JsonStorage.postgres(PostgresHelper.JSONB_TYPE), docType, nullable, mutationDetection);
case DbPlatformType.JSON:
return new ScalarTypeJsonSet(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, keepSource);
return new ScalarTypeJsonSet(DbPlatformType.JSON, JsonStorage.postgres(PostgresHelper.JSON_TYPE), docType, nullable, mutationDetection);
}
}
return new ScalarTypeJsonSet(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource);
return new ScalarTypeJsonSet(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, mutationDetection);
}
ScalarTypeJsonSet(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, boolean keepSource) {
super(Set.class, jdbcType, storage, keepSource, nullable, docType);
ScalarTypeJsonSet(int jdbcType, JsonStorage storage, DocPropertyType docType, boolean nullable, MutationDetection mutationDetection) {
super(Set.class, jdbcType, storage, mutationDetection, nullable, docType);
}
@Override
@@ -89,8 +90,8 @@ class ScalarTypeJsonSet extends ScalarTypeJsonCollectionValue<Set> {
private final ArrayElementConverter converter;
VarcharWithConverter(DocPropertyType docType, boolean nullable, boolean keepSource, ArrayElementConverter converter) {
super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, keepSource);
VarcharWithConverter(DocPropertyType docType, boolean nullable, ArrayElementConverter converter) {
super(Types.VARCHAR, JsonStorage.VARCHAR, docType, nullable, MutationDetection.DEFAULT);
this.converter = converter;
}
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.type;
import io.ebean.annotation.MutationDetection;
import io.ebean.core.type.DataBinder;
import io.ebean.core.type.DataReader;
import io.ebean.core.type.DocPropertyType;
@@ -20,11 +21,18 @@ import java.sql.SQLException;
* backed {@code EJson} facade)</li>
* </ul>
* This removes the previous explosion of per-storage and per-platform subclasses.
* <p>
* Mutation detection: {@code DEFAULT} uses the legacy ModifyAware wrapper based dirty
* checking. {@code NONE} disables dirty checking entirely (mutable() is false and the
* property is only included in an update when explicitly set). {@code HASH} and
* {@code SOURCE} are handled via {@code BeanPropertyJsonMapper} (json content is kept
* for the read/bind round trip so that a checksum or the source content can be compared).
*/
abstract class ScalarTypeJsonValue<T> extends ScalarTypeBase<T> {
protected final JsonStorage storage;
protected final boolean keepSource;
private final boolean mutable;
private final boolean nullable;
private final String emptyJson;
private final DocPropertyType docType;
@@ -33,11 +41,12 @@ abstract class ScalarTypeJsonValue<T> extends ScalarTypeBase<T> {
* @param emptyJson JSON bound when the value is null and the property is not nullable
* (e.g. {@code "[]"} for collections), or null to always bind SQL null.
*/
ScalarTypeJsonValue(Class<T> type, int jdbcType, JsonStorage storage, boolean keepSource,
ScalarTypeJsonValue(Class<T> type, int jdbcType, JsonStorage storage, MutationDetection mutationDetection,
boolean nullable, String emptyJson, DocPropertyType docType) {
super(type, false, jdbcType);
this.storage = storage;
this.keepSource = keepSource;
this.keepSource = mutationDetection == MutationDetection.HASH || mutationDetection == MutationDetection.SOURCE;
this.mutable = mutationDetection != MutationDetection.NONE;
this.nullable = nullable;
this.emptyJson = emptyJson;
this.docType = docType;
@@ -51,7 +60,7 @@ abstract class ScalarTypeJsonValue<T> extends ScalarTypeBase<T> {
@Override
public final boolean mutable() {
return true;
return mutable;
}
@Override
@@ -117,6 +117,31 @@ class CQueryBuilderTest {
assertThat(countSql).isEqualTo("select count(*) from ( select t0.id from ad t0) as c");
}
@Test
void wrapSelectExists_default_usesScalarExists() {
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", false, "");
assertThat(sql).isEqualTo("select exists(select 1 from o_order t0 where t0.id > ?)");
}
/**
* SQL Server does not support exists(...) as a directly selectable scalar
* boolean expression - see https://github.com/ebean-orm/ebean/issues/3848
*/
@Test
void wrapSelectExists_existsWithCaseWhen_wrapsAsCaseWhen() {
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", true, "");
assertThat(sql).isEqualTo("select case when exists(select 1 from o_order t0 where t0.id > ?) then 1 else 0 end");
}
/**
* Oracle also requires a FROM clause on every select (from dual) - see https://github.com/ebean-orm/ebean/issues/3848
*/
@Test
void wrapSelectExists_existsWithCaseWhenAndFromClause_appendsFromClause() {
String sql = CQueryBuilder.wrapSelectExists("select 1 from o_order t0 where t0.id > ?", true, " from dual");
assertThat(sql).isEqualTo("select case when exists(select 1 from o_order t0 where t0.id > ?) then 1 else 0 end from dual");
}
@Test
void inlineSqlCommentLabel_rootExplicitLabel_prefixesBeanType() {
String label = CQueryBuilder.inlineSqlCommentLabel("fetchMachineFleets", null, false, "COrganisationMachine");
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.type;
import io.ebean.annotation.MutationDetection;
import io.ebean.config.dbplatform.ExtraDbTypes;
import io.ebean.core.type.DocPropertyType;
import org.junit.jupiter.api.Test;
@@ -11,19 +12,19 @@ public class ScalarTypeJsonListTest extends BasePlatformArrayTypeFactoryTest {
@Test
public void typeFor_expect_nullToEmpty_when_postgresNonNull() throws SQLException {
assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, false, false));
assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, false, false));
assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT));
assertBindNullTo_PGObjectEmpty(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT));
assertBindNullTo_EmptyString(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, false, false));
assertBindNullTo_EmptyString(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, false, MutationDetection.DEFAULT));
}
@Test
public void typeFor_expect_nullToNull_when_nullable() throws SQLException {
assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, true, false));
assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, true, false));
assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONB, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT));
assertBindNullTo_PGObjectNull(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSON, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT));
assertBindNullTo_Null(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, true, false));
assertBindNullTo_Null(ScalarTypeJsonList.typeFor(true, ExtraDbTypes.JSONVarchar, DocPropertyType.OBJECT, true, MutationDetection.DEFAULT));
}
}
@@ -686,7 +686,8 @@ public class BaseTableDdl implements TableDdl {
if (hasValue(alterColumn.getUniqueOneToOne())) {
alterColumnAddUniqueOneToOneConstraint(writer, alterColumn);
}
if (hasValue(alterColumn.getComment())) {
if (hasValue(alterColumn.getComment()) && !platformDdl.isInlineComments()) {
// platform supports comments as a separate statement (e.g. postgres)
alterColumnComment(writer, alterColumn);
}
if (hasValue(alterColumn.getDropCheckConstraint())) {
@@ -700,7 +701,10 @@ public class BaseTableDdl implements TableDdl {
}
if (typeChange(alterColumn)
|| hasValue(alterColumn.getDefaultValue())
|| alterColumn.isNotnull() != null) {
|| alterColumn.isNotnull() != null
|| (hasValue(alterColumn.getComment()) && platformDdl.isInlineComments())) {
// platforms with inline comments (e.g. mysql) must restate the whole column
// definition (including comment) even when only the comment is changing
alterColumn(writer, alterColumn);
}
if (alterCheckConstraint) {
@@ -824,7 +828,9 @@ public class BaseTableDdl implements TableDdl {
platformDdl.alterTableAddColumn(writer, tableName, column, onHistoryTable, help.getDefaultValue());
final String comment = column.getComment();
if (comment != null && !comment.isEmpty()) {
if (comment != null && !comment.isEmpty() && !platformDdl.isInlineComments()) {
// platforms with inline comments (e.g. mysql) embed the comment directly into the
// "add column" statement itself rather than as a separate statement
platformDdl.addColumnComment(writer.applyPostAlter(), tableName, column.getName(), comment);
}
@@ -94,9 +94,10 @@ public class MySqlDdl extends PlatformDdl {
public void alterColumn(DdlWrite writer, AlterColumn alter) {
String tableName = alter.getTableName();
String columnName = alter.getColumnName();
boolean commentChange = hasValue(alter.getComment());
if (alter.getType() == null && alter.isNotnull() == null) {
// No type change or notNull change -> handle default value change
if (alter.getType() == null && alter.isNotnull() == null && !commentChange) {
// No type change, notNull change or comment change -> handle default value change
if (hasValue(alter.getDefaultValue())) {
alterColumnDefault(writer, alter);
}
@@ -115,13 +116,50 @@ public class MySqlDdl extends PlatformDdl {
if (hasValue(defaultValue) && !DdlHelp.isDropDefault(defaultValue)) {
buffer.append(" default ").append(convertDefaultValue(defaultValue));
}
// restate the comment (new, existing, or none) as mysql requires the whole column
// definition to be repeated - otherwise a comment could be silently dropped
String comment = alter.getComment() != null ? alter.getComment() : alter.getCurrentComment();
if (DdlHelp.isDropComment(comment)) {
comment = null;
}
appendColumnComment(buffer, comment);
}
}
@Override
public void alterTableAddColumn(DdlWrite writer, String tableName, Column column, boolean onHistoryTable, String defaultValue) {
String convertedType = convert(column.getType());
DdlBuffer buffer = alterTable(writer, tableName).append(addColumn, column.getName());
buffer.append(convertedType);
// Add default value also to history table if it is not excluded
if (defaultValue != null) {
if (!onHistoryTable || !isTrue(column.isHistoryExclude())) {
buffer.append(" default ");
buffer.append(defaultValue);
}
}
if (!onHistoryTable) {
if (isTrue(column.isNotnull())) {
buffer.appendWithSpace(columnNotNull);
}
// check constraints cannot be added in one statement for h2
if (!StringHelper.isNull(column.getCheckConstraint())) {
String ddl = alterTableAddCheckConstraint(tableName, column.getCheckConstraintName(), column.getCheckConstraint());
writer.applyPostAlter().appendStatement(ddl);
}
// comment must be inline as part of the column definition for mysql
appendColumnComment(buffer, column.getComment());
}
}
@Override
protected void writeColumnDefinition(DdlBuffer buffer, Column column, DdlIdentity identity) {
super.writeColumnDefinition(buffer, column, identity);
String comment = column.getComment();
appendColumnComment(buffer, column.getComment());
}
private void appendColumnComment(DdlBuffer buffer, String comment) {
if (!StringHelper.isNull(comment)) {
// in mysql 5.5 column comment save in information_schema.COLUMNS.COLUMN_COMMENT(VARCHAR 1024)
if (comment.length() > 500) {
@@ -28,6 +28,7 @@ import java.util.List;
* &lt;attribute name="notnull" type="{http://www.w3.org/2001/XMLSchema}boolean" /&gt;
* &lt;attribute name="currentNotnull" type="{http://www.w3.org/2001/XMLSchema}boolean" /&gt;
* &lt;attribute name="comment" type="{http://www.w3.org/2001/XMLSchema}string" /&gt;
* &lt;attribute name="currentComment" type="{http://www.w3.org/2001/XMLSchema}string" /&gt;
* &lt;attribute name="historyExclude" type="{http://www.w3.org/2001/XMLSchema}boolean" /&gt;
* &lt;attribute name="checkConstraint" type="{http://www.w3.org/2001/XMLSchema}string" /&gt;
* &lt;attribute name="checkConstraintName" type="{http://www.w3.org/2001/XMLSchema}string" /&gt;
@@ -77,6 +78,8 @@ public class AlterColumn {
protected Boolean currentNotnull;
@XmlAttribute(name = "comment")
protected String comment;
@XmlAttribute(name = "currentComment")
protected String currentComment;
@XmlAttribute(name = "historyExclude")
protected Boolean historyExclude;
@XmlAttribute(name = "checkConstraint")
@@ -360,6 +363,30 @@ public class AlterColumn {
this.comment = value;
}
/**
* Gets the value of the currentComment property.
* <p>
* This is the pre-existing comment value on the column, only populated when a platform
* needs to fully restate the column definition (e.g. mysql) so that the existing comment
* is not lost when another attribute (type, notnull, or the comment itself) changes.
*
* @return possible object is
* {@link String }
*/
public String getCurrentComment() {
return currentComment;
}
/**
* Sets the value of the currentComment property.
*
* @param value allowed object is
* {@link String }
*/
public void setCurrentComment(String value) {
this.currentComment = value;
}
/**
* Gets the value of the historyExclude property.
*
@@ -374,6 +374,7 @@ public class MColumn {
this.alterColumn = null;
boolean changeBaseAttribute = false;
boolean changeComment = false;
if (historyExclude != newColumn.historyExclude) {
getAlterColumn(tableName, tableWithHistory).setHistoryExclude(newColumn.historyExclude);
@@ -396,6 +397,7 @@ public class MColumn {
}
}
if (different(comment, newColumn.comment)) {
changeComment = true;
AlterColumn alter = getAlterColumn(tableName, tableWithHistory);
if (newColumn.comment == null) {
alter.setComment(DdlHelp.DROP_COMMENT);
@@ -459,10 +461,14 @@ public class MColumn {
if (alterColumn != null) {
modelDiff.addAlterColumn(alterColumn);
if (changeBaseAttribute) {
// support reverting these changes
if (changeBaseAttribute || changeComment) {
// Support reverting these changes and let platforms that must restate the whole
// column definition on any change (e.g. mysql) preserve unchanged attributes -
// don't lose the existing comment when altering type/notnull, and have the
// current type/notnull available to restate when only the comment is changing.
alterColumn.setCurrentType(type);
alterColumn.setCurrentNotnull(notnull);
alterColumn.setCurrentComment(comment);
}
}
}
@@ -2,12 +2,15 @@ package io.ebeaninternal.dbmigration.model.build;
import io.ebeaninternal.dbmigration.model.MColumn;
import io.ebeaninternal.dbmigration.model.MTable;
import io.ebean.config.dbplatform.DbPlatformType;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import io.ebeaninternal.server.deploy.TableJoin;
import io.ebeaninternal.server.deploy.TableJoinColumn;
import java.sql.Types;
/**
* Add the intersection table to the model.
*/
@@ -79,10 +82,23 @@ class ModelBuildIntersectionTable {
for (TableJoinColumn otherColumn : otherColumns) {
addColumn(table, targetDesc, otherColumn.getLocalDbColumn(), otherColumn.getForeignDbColumn());
}
if (manyProp.hasIntersectionOrderColumn()) {
addOrderColumn(table);
}
return table;
}
/**
* Add the extra (non PK) order column used to persist {@code @OrderColumn} position
* for a ManyToMany relationship.
*/
private void addOrderColumn(MTable table) {
DbPlatformType dbType = ctx.getDbTypeMap().get(Types.INTEGER);
MColumn col = new MColumn(manyProp.intersectionOrderColumn(), dbType.renderType(0, 0));
col.setNotnull(!manyProp.isIntersectionOrderColumnNullable());
table.addColumn(col);
}
private void addColumn(MTable table, BeanDescriptor<?> desc, String column, String findPropColumn) {
BeanProperty p = desc.idBinder().findBeanProperty(findPropColumn);
@@ -288,6 +288,7 @@
<xsd:attribute name="notnull" type="xsd:boolean"/>
<xsd:attribute name="currentNotnull" type="xsd:boolean"/>
<xsd:attribute name="comment" type="xsd:string"/>
<xsd:attribute name="currentComment" type="xsd:string"/>
<xsd:attribute name="historyExclude" type="xsd:boolean"/>
<xsd:attribute name="checkConstraint" type="xsd:string"/>
<xsd:attribute name="checkConstraintName" type="xsd:string"/>
@@ -180,6 +180,60 @@ public class PlatformDdl_AlterColumnTest {
}
@Test
public void mysql_alterColumn_commentOnly_rebuildsWithType() {
AlterColumn alter = new AlterColumn();
alter.setTableName("mytab");
alter.setColumnName("acol");
alter.setCurrentType("varchar(50)");
alter.setCurrentNotnull(Boolean.TRUE);
alter.setComment("new comment");
String sql = alterColumn(mysqlDdl, alter);
softly.assertThat(sql).isEqualTo("-- apply alter tables\n"
+ "alter table mytab modify acol varchar(50) not null comment 'new comment';\n");
}
@Test
public void mysql_alterColumn_typeChange_preservesExistingComment() {
AlterColumn alter = new AlterColumn();
alter.setTableName("mytab");
alter.setColumnName("acol");
alter.setCurrentType("varchar(20)");
alter.setType("varchar(50)");
alter.setCurrentComment("existing comment");
String sql = alterColumn(mysqlDdl, alter);
softly.assertThat(sql).isEqualTo("-- apply alter tables\n"
+ "alter table mytab modify acol varchar(50) comment 'existing comment';\n");
}
@Test
public void mysql_alterColumn_dropComment() {
AlterColumn alter = new AlterColumn();
alter.setTableName("mytab");
alter.setColumnName("acol");
alter.setCurrentType("varchar(50)");
alter.setComment("DROP COMMENT");
alter.setCurrentComment("existing comment");
String sql = alterColumn(mysqlDdl, alter);
softly.assertThat(sql).isEqualTo("-- apply alter tables\n"
+ "alter table mytab modify acol varchar(50);\n");
}
@Test
public void mysql_alterTableAddColumn_withComment() {
Column column = simpleColumn();
column.setComment("a comment");
DdlWrite writer = new DdlWrite();
mysqlDdl.alterTableAddColumn(writer, "my_table", column, false, "1");
softly.assertThat(writer.toString())
.isEqualTo("-- apply alter tables\n"
+ "alter table my_table add column my_column int default 1 not null comment 'a comment';\n");
}
@Test
public void testAlterColumnType() {
@@ -174,6 +174,53 @@ class MColumnTest {
assertThat(getAlterColumn(diff).getDefaultValue()).isEqualTo("abc");
}
@Test
void diffComment_add() {
ModelDiff diff = diff();
MColumn newCol = basic();
newCol.setComment("a comment");
basic().compare(diff, table, newCol);
assertChanges(diff);
AlterColumn alterColumn = getAlterColumn(diff);
assertThat(alterColumn.getComment()).isEqualTo("a comment");
// no pre-existing base attribute changed, current type/notnull/comment carried
// through anyway so platforms that must restate the whole column (e.g. mysql)
// have what they need to rebuild the statement
assertThat(alterColumn.getCurrentType()).isEqualTo("integer");
assertThat(alterColumn.getCurrentComment()).isNull();
}
@Test
void diffComment_remove() {
ModelDiff diff = diff();
MColumn newCol = basic();
MColumn oldCol = basic();
oldCol.setComment("a comment");
oldCol.compare(diff, table, newCol);
assertChanges(diff);
assertThat(getAlterColumn(diff).getComment()).isEqualTo("DROP COMMENT");
}
@Test
void diffType_preservesCurrentComment() {
ModelDiff diff = diff();
MColumn oldCol = basic();
oldCol.setComment("existing comment");
MColumn newCol = new MColumn("col", "integer(8)");
newCol.setComment("existing comment");
oldCol.compare(diff, table, newCol);
assertChanges(diff);
AlterColumn alterColumn = getAlterColumn(diff);
assertThat(alterColumn.getType()).isEqualTo("integer(8)");
assertThat(alterColumn.getComment()).isNull();
// current comment recorded so mysql can restate it when rebuilding the full
// column definition for the type change
assertThat(alterColumn.getCurrentComment()).isEqualTo("existing comment");
}
@Test
void diffReferencesAdd() {
ModelDiff diff = diff();
+1 -1
View File
@@ -47,7 +47,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.2</version>
<version>42.7.11</version>
<scope>provided</scope>
</dependency>
@@ -5,17 +5,23 @@ import io.ebean.Database;
import io.ebean.redis.DuelCache;
import org.domain.Person;
import org.domain.query.QPerson;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.assertThat;
class ClusterTest {
private Database createOther(DataSource dataSource) {
return Database.builder()
.dataSource(dataSource)
private static Database db;
private static Database other;
@BeforeAll
static void setup() {
// ensure the default server exists first
db = DB.getDefault();
other = Database.builder()
.dataSource(db.pluginApi().dataSource())
.loadFromProperties()
.defaultDatabase(false)
.name("other")
@@ -24,12 +30,13 @@ class ClusterTest {
.build();
}
@AfterAll
static void tearDown() {
other.shutdown(false, false);
}
@Test
void testBothNear() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.pluginApi().dataSource());
new QPerson()
.name.eq("Someone")
.delete();
@@ -59,10 +66,6 @@ class ClusterTest {
@Test
void test() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.pluginApi().dataSource());
for (int i = 0; i < 10; i++) {
Person foo = new Person("name " + i);
foo.save();
@@ -8,3 +8,10 @@ ebean:
platform: h2 # h2, postgres, mysql, oracle, sqlserver, sqlite
ddlMode: dropCreate # none | dropCreate | create | migration | createOnly | migrationDropCreate
dbName: myapp
# Keep the shared "ut_redis" test container running rather than removing it on JVM exit.
# ebean-redis and ebean-redisson both reuse this fixed-name/fixed-port container - with a
# parallel reactor build (mvn -T 1C) the module that finishes first would otherwise remove
# the container while the other module's tests are still using it.
redis:
shutdownMode: none
@@ -5,17 +5,23 @@ import io.ebean.Database;
import io.ebean.redisson.DuelCache;
import org.domain.Person;
import org.domain.query.QPerson;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.assertThat;
class ClusterTest {
private Database createOther(DataSource dataSource) {
return Database.builder()
.dataSource(dataSource)
private static Database db;
private static Database other;
@BeforeAll
static void setup() {
// ensure the default server exists first
db = DB.getDefault();
other = Database.builder()
.dataSource(db.pluginApi().dataSource())
.loadFromProperties()
.defaultDatabase(false)
.name("other")
@@ -24,12 +30,13 @@ class ClusterTest {
.build();
}
@AfterAll
static void tearDown() {
other.shutdown(false, false);
}
@Test
void testBothNear() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.pluginApi().dataSource());
new QPerson()
.name.eq("Someone")
.delete();
@@ -59,10 +66,6 @@ class ClusterTest {
@Test
void test() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.pluginApi().dataSource());
for (int i = 0; i < 10; i++) {
Person foo = new Person("name " + i);
foo.save();
+4 -4
View File
@@ -213,7 +213,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.2</version>
<version>42.7.11</version>
<exclusions>
<exclusion>
<groupId>org.checkerframework</groupId>
@@ -231,9 +231,9 @@
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.7.0</version>
<scope>test</scope>
</dependency>
@@ -3,6 +3,7 @@ package io.ebean.xtest.base;
import io.ebean.xtest.BaseTestCase;
import io.ebean.DB;
import io.ebean.DtoQuery;
import io.ebean.PagedList;
import io.ebean.ProfileLocation;
import io.ebean.xtest.ForPlatform;
import io.ebean.annotation.Platform;
@@ -10,6 +11,7 @@ import io.ebean.meta.MetaQueryMetric;
import io.ebean.meta.MetaTimedMetric;
import io.ebean.meta.ServerMetrics;
import io.ebean.test.LoggedSql;
import jakarta.persistence.PersistenceException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -20,6 +22,7 @@ import java.time.OffsetDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class DtoQueryFromOrmTest extends BaseTestCase {
@@ -411,6 +414,63 @@ public class DtoQueryFromOrmTest extends BaseTestCase {
assertThat(contactDtos).isNotEmpty();
}
@Test
public void findPagedList_fromOrmQuery() {
ResetBasicData.reset();
PagedList<ContactDto> pagedList = DB.find(Contact.class)
.select("id, email, " + concat("lastName", ", ", "firstName") + " as fullName")
.where().isNotNull("email")
.orderBy().asc("id")
.setFirstRow(0)
.setMaxRows(2)
.asDto(ContactDto.class)
.findPagedList();
List<ContactDto> dtos = pagedList.getList();
assertThat(dtos).hasSizeLessThanOrEqualTo(2);
int totalRowCount = pagedList.getTotalCount();
List<ContactDto> allDtos = DB.find(Contact.class)
.select("id, email, " + concat("lastName", ", ", "firstName") + " as fullName")
.where().isNotNull("email")
.orderBy().asc("id")
.asDto(ContactDto.class)
.findList();
assertThat(totalRowCount).isEqualTo(allDtos.size());
assertThat(pagedList.getPageSize()).isEqualTo(2);
assertThat(pagedList.getPageIndex()).isEqualTo(0);
}
@Test
public void findPagedList_noMaxRows_throws() {
ResetBasicData.reset();
DtoQuery<ContactDto> query = DB.find(Contact.class)
.select("id, email")
.where().isNotNull("email")
.asDto(ContactDto.class);
assertThatThrownBy(query::findPagedList)
.isInstanceOf(PersistenceException.class)
.hasMessageContaining("maxRows must be specified");
}
@Test
public void findPagedList_rawSql_throws() {
DtoQuery<ContactDto> query = DB.findDto(ContactDto.class,
"select id, email from contact where email is not null")
.setMaxRows(10);
assertThatThrownBy(query::findPagedList)
.isInstanceOf(PersistenceException.class)
.hasMessageContaining("only supported for a DtoQuery derived");
}
public static class ContactTotals {
String lastName;
@@ -15,6 +15,7 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class EbeanServerFactory_ServerConfigStart_Test {
@@ -83,7 +84,7 @@ public class EbeanServerFactory_ServerConfigStart_Test {
}
@Test
public void create_registeredDatabase_twice_returnsExistingInstance() {
public void create_registeredDatabase_twice_throwsException() {
DatabaseBuilder config = new DatabaseConfig();
config.setName("h2");
@@ -100,12 +101,15 @@ public class EbeanServerFactory_ServerConfigStart_Test {
config.addServerConfigStartup(serverConfig -> startupCount.incrementAndGet());
Database db = DatabaseFactory.create(config);
Database existing = DatabaseFactory.create(config);
try {
assertThatThrownBy(() -> DatabaseFactory.create(config))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("already registered");
assertThat(existing).isSameAs(db);
assertThat(startupCount.get()).isEqualTo(1);
db.shutdown(true, false);
assertThat(startupCount.get()).isEqualTo(1);
} finally {
db.shutdown(true, false);
}
}
@Test
@@ -962,12 +962,7 @@ public class TDSpiEbeanServer extends TDSpiServer implements SpiEbeanServer {
}
@Override
public Set<Property> checkUniqueness(Object bean) {
return Collections.emptySet();
}
@Override
public Set<Property> checkUniqueness(Object bean, Transaction transaction) {
public Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) {
return Collections.emptySet();
}
@@ -389,12 +389,7 @@ public class TDSpiServer implements SpiServer {
}
@Override
public Set<Property> checkUniqueness(Object bean) {
return null;
}
@Override
public Set<Property> checkUniqueness(Object bean, Transaction transaction) {
public Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) {
return null;
}
@@ -57,6 +57,24 @@ public class TestBeanCache extends BaseTestCase {
assertThat(sql).isEmpty();
}
@Test
public void findList_withInIdAsString_when_idTypeConverted_expect_noDuplicates() {
OCachedBean bean = new OCachedBean();
bean.setName("findList_withInIdAsString");
DB.save(bean);
// warm the bean cache
DB.find(OCachedBean.class, bean.getId());
// id passed as String rather than the bean's actual Long id type
List<OCachedBean> list = DB.find(OCachedBean.class)
.where().in("id", String.valueOf(bean.getId()))
.findList();
assertThat(list).hasSize(1);
assertThat(list.get(0).getId()).isEqualTo(bean.getId());
}
@Test
public void idsInFindMap() {
@@ -0,0 +1,37 @@
package org.tests.cache;
import io.ebean.DB;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.ECachedEnumId;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for #3110 - Finder.byId() on a @Cache entity with an Enum @Id (mapped via
* @EnumValue) threw a NumberFormatException on the second call (cache hit) because
* the bean cache key (the enum's name()) was being converted back to the bean id
* using the db-value based toBeanType() rather than parse() (the correct inverse
* of the cache key's format()).
*/
class TestCacheEnumId extends BaseTestCase {
@Test
void findById_enumId_whenCacheHit() {
ECachedEnumId bean = new ECachedEnumId();
bean.setStatus(ECachedEnumId.Status.APPROVED);
bean.setName("approved");
DB.save(bean);
// first find - misses the bean cache, loads from DB and populates the cache
ECachedEnumId bean0 = DB.find(ECachedEnumId.class, ECachedEnumId.Status.APPROVED);
assertThat(bean0).isNotNull();
assertThat(bean0.getStatus()).isEqualTo(ECachedEnumId.Status.APPROVED);
// second find - hits the bean cache, used to throw NumberFormatException
ECachedEnumId bean1 = DB.find(ECachedEnumId.class, ECachedEnumId.Status.APPROVED);
assertThat(bean1).isNotNull();
assertThat(bean1.getStatus()).isEqualTo(ECachedEnumId.Status.APPROVED);
assertThat(bean1.getName()).isEqualTo("approved");
}
}
@@ -0,0 +1,118 @@
package org.tests.inheritance;
import io.ebean.DB;
import io.ebean.plugin.ExpressionPath;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.*;
import java.sql.Timestamp;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Roland Praml, FOCONIS AG
*/
public class TestPropertyAccess {
private ExpressionPath custCretime = DB.getDefault().pluginApi().beanType(Customer.class).expressionPath("cretime");
private ExpressionPath custName = DB.getDefault().pluginApi().beanType(Customer.class).expressionPath("name");
// Note: Animal.name is only present in "Cat" not in "Dog"
private ExpressionPath animalName = DB.getDefault().pluginApi().beanType(Animal.class).expressionPath("name");
private ExpressionPath productName = DB.getDefault().pluginApi().beanType(Product.class).expressionPath("name");
private ExpressionPath animalSpecies = DB.getDefault().pluginApi().beanType(Animal.class).expressionPath("species");
@Test
void testOnInheritance() {
Cat cat = new Cat();
cat.setName("Tom");
DB.save(cat);
Dog dog = new Dog();
dog.setRegistrationNumber("FOO");
DB.save(dog);
Animal animal = DB.find(Animal.class, cat.getId());
assertThat(animalName.pathGet(animal)).isEqualTo("Tom");
animal = DB.find(Animal.class, dog.getId());
assertThat(animalName.pathGet(animal)).isNull();
animalName.pathSet(cat, "Jerry");
assertThat(cat.getName()).isEqualTo("Jerry");
assertThatThrownBy(() -> animalName.pathSet(dog, "Jerry"))
.isInstanceOf(IllegalArgumentException.class);
animalSpecies.pathSet(cat, "Angora");
animalSpecies.pathSet(dog, "Bulldog");
assertThat(cat.getSpecies()).isEqualTo("Angora");
assertThat(dog.getSpecies()).isEqualTo("Bulldog");
}
@Test
void testOnMappedSuperClass() {
Customer cust = new Customer();
Timestamp ts = new Timestamp(123);
custCretime.pathSet(cust, ts);
assertThat(custCretime.pathGet(cust)).isEqualTo(ts);
custName.pathSet(cust, "Roland");
assertThat(custName.pathGet(cust)).isEqualTo("Roland");
}
@Test
void testOnPlainBean() {
Product product = new Product();
productName.pathSet(product, "Roland");
assertThat(productName.pathGet(product)).isEqualTo("Roland");
}
@Test
void testOnContactGroup() {
ContactGroup cg = new ContactGroup();
// CHECKEM: Ist it OK to us the "custCretime" on "contactGroup"
assertThatThrownBy(() -> custCretime.pathGet(cg)).isInstanceOf(IllegalArgumentException.class);
}
@Test
void testOnCrossUsage() {
Product product = new Product();
Customer cust = new Customer();
Cat cat = new Cat();
Dog dog = new Dog();
assertThatThrownBy(() -> custCretime.pathGet(product)).isInstanceOf(IllegalArgumentException.class);
custCretime.pathGet(cust);
assertThatThrownBy(() -> custCretime.pathGet(cat)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> custCretime.pathGet(dog)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> custName.pathGet(product)).isInstanceOf(IllegalArgumentException.class);
custName.pathGet(cust);
assertThatThrownBy(() -> custName.pathGet(cat)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> custName.pathGet(dog)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> animalName.pathGet(product)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> animalName.pathGet(cust)).isInstanceOf(IllegalArgumentException.class);
animalName.pathGet(cat);
animalName.pathGet(dog);
productName.pathGet(product);
assertThatThrownBy(() -> productName.pathGet(cust)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> productName.pathGet(cat)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> productName.pathGet(dog)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> animalSpecies.pathGet(product)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> animalSpecies.pathGet(cust)).isInstanceOf(IllegalArgumentException.class);
animalSpecies.pathGet(cat);
animalSpecies.pathGet(dog);
}
}
@@ -1,13 +1,16 @@
package org.tests.insert;
import io.ebean.xtest.BaseTestCase;
import io.ebean.DB;
import io.ebean.Transaction;
import io.ebean.plugin.Property;
import io.ebean.test.LoggedSql;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.EBasicWithUniqueCon;
import org.tests.model.draftable.Document;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@@ -102,9 +105,131 @@ public class TestInsertCheckUnique extends BaseTestCase {
System.out.println("uniqueProperties > " + uniqueProperties);
System.out.println(" custom msg > " + msg);
}
LoggedSql.start();
assertThat(DB.checkUniqueness(doc2).toString()).contains("title");
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
if (isH2() || isPostgresCompatible()) {
assertThat(sql.get(0)).contains("select exists(select 1 from document t0 where t0.title = ?)");
}
}
}
/**
* When invoking checkUniqueness multiple times, we can benefit from the "exists" query cache if bean has query cache enabled
*/
@Test
public void testUseQueryCache() {
DB.find(EBasicWithUniqueCon.class).delete(); // clean up DB (otherwise test may be affected by other test)
EBasicWithUniqueCon basic = new EBasicWithUniqueCon();
basic.setName("foo");
basic.setOther("bar");
basic.setOtherOne("baz");
// create a new bean
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
if (isH2() || isPostgresCompatible()) {
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.other = ? and t0.other_one = ?)");
}
DB.save(basic);
try {
// reload from database
basic = DB.find(EBasicWithUniqueCon.class, basic.getId());
// and check again
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
if (isH2() || isPostgresCompatible()) {
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.id <> ? and t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.id <> ? and t0.other = ? and t0.other_one = ?)");
}
// and check again - expect to hit query cache
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).as("Expected to hit query cache").hasSize(0);
// and check again, where only one value is changed
basic.setOther("fooo");
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, null, true, false)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("fooo,baz)");
} finally {
DB.delete(EBasicWithUniqueCon.class, basic.getId());
}
}
/**
* When invoking checkUniqueness multiple times, we can benefit from the "exists" query cache if bean has query cache enabled
*/
@Test
public void testSkipClean() {
DB.find(EBasicWithUniqueCon.class).delete(); // clean up DB (otherwise test may be affected by other test)
EBasicWithUniqueCon basic = new EBasicWithUniqueCon();
basic.setName("foo");
basic.setOther("bar");
basic.setOtherOne("baz");
// create a new bean
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, null, false, true)).isEmpty();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
if (isH2() || isPostgresCompatible()) {
assertThat(sql.get(0)).contains("select exists(select 1 from e_basicverucon t0 where t0.name = ?)");
assertThat(sql.get(1)).contains("select exists(select 1 from e_basicverucon t0 where t0.other = ? and t0.other_one = ?)");
}
DB.save(basic);
try (Transaction txn = DB.beginTransaction()) {
// reload from database
basic = DB.find(EBasicWithUniqueCon.class, basic.getId());
// and check again. We do not check unmodified properties
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, txn, false, true)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).hasSize(0);
// and check again, where only one value is changed
basic.setOther("fooo");
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, txn, false, true)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("fooo,baz)");
// multiple checks will hit DB
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, txn, false, true)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
// enable also query cache
assertThat(DB.getDefault().checkUniqueness(basic, txn, true, true)).isEmpty();
LoggedSql.start();
assertThat(DB.getDefault().checkUniqueness(basic, txn, true, true)).isEmpty();
sql = LoggedSql.stop();
assertThat(sql).isEmpty();
} finally {
DB.delete(EBasicWithUniqueCon.class, basic.getId());
}
}
}
@@ -0,0 +1,148 @@
package org.tests.json;
import io.ebean.BeanState;
import io.ebean.DB;
import io.ebean.test.LoggedSql;
import io.ebean.xtest.BaseTestCase;
import io.ebean.xtest.IgnorePlatform;
import io.ebean.annotation.Platform;
import org.junit.jupiter.api.Test;
import org.tests.model.json.EBasicJsonMapMutation;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Verifies that {@code Map<String,Object>} @DbJson properties honour mutationDetection
* rather than always using ModifyAware based dirty checking - see issue #2840.
*/
public class TestDbJsonMapMutationDetection extends BaseTestCase {
private static Map<String, Object> content(String value) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("field", List.of(value));
return map;
}
@IgnorePlatform(Platform.MYSQL)
@Test
public void noneMode_inPlaceMutation_notDetected() {
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
bean.setNoneMap(content("a"));
DB.save(bean);
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
// mutate the map content directly (without calling the setter)
found.getNoneMap().put("field", List.of("mutated"));
LoggedSql.start();
DB.save(found);
// NONE means no attempt to detect mutation - so no update at all
assertThat(LoggedSql.stop()).isEmpty();
}
@IgnorePlatform(Platform.MYSQL)
@Test
public void noneMode_setEqualValue_noUpdate() {
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
bean.setNoneMap(content("a"));
DB.save(bean);
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
// set a brand new (but equal content) map instance via the setter
found.setNoneMap(content("a"));
BeanState state = DB.beanState(found);
assertThat(state.changedProps()).isEmpty();
LoggedSql.start();
DB.save(found);
assertThat(LoggedSql.stop()).isEmpty();
}
@IgnorePlatform(Platform.MYSQL)
@Test
public void noneMode_setDifferentValue_updates() {
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
bean.setNoneMap(content("a"));
DB.save(bean);
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
found.setNoneMap(content("b"));
LoggedSql.start();
DB.save(found);
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("none_map");
}
@IgnorePlatform(Platform.MYSQL)
@Test
public void hashMode_inPlaceMutation_detected() {
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
bean.setHashMap(content("a"));
DB.save(bean);
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
found.getHashMap().put("field", List.of("mutated"));
LoggedSql.start();
DB.save(found);
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("hash_map");
}
@IgnorePlatform(Platform.MYSQL)
@Test
public void hashMode_setEqualValue_noUpdate() {
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
bean.setHashMap(content("a"));
DB.save(bean);
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
// brand new map instance with equal content - hash should match so no update
found.setHashMap(content("a"));
LoggedSql.start();
DB.save(found);
assertThat(LoggedSql.stop()).isEmpty();
}
@IgnorePlatform(Platform.MYSQL)
@Test
public void sourceMode_setEqualValue_noUpdate() {
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
bean.setSourceMap(content("a"));
DB.save(bean);
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
found.setSourceMap(content("a"));
LoggedSql.start();
DB.save(found);
assertThat(LoggedSql.stop()).isEmpty();
}
@IgnorePlatform(Platform.MYSQL)
@Test
public void defaultMode_inPlaceMutation_stillDetectedViaModifyAware() {
// unchanged legacy behaviour - DEFAULT mode keeps using ModifyAware wrapper dirty checking
EBasicJsonMapMutation bean = new EBasicJsonMapMutation();
bean.setDefaultMap(content("a"));
DB.save(bean);
EBasicJsonMapMutation found = DB.find(EBasicJsonMapMutation.class, bean.getId());
found.getDefaultMap().put("field", List.of("mutated"));
LoggedSql.start();
DB.save(found);
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("default_map");
}
}
@@ -1,10 +1,12 @@
package org.tests.model.basic;
import io.ebean.annotation.Cache;
import jakarta.persistence.*;
import javax.validation.constraints.Size;
import java.sql.Timestamp;
@Entity
@Cache(enableQueryCache = true)
@Table(name = "e_basicverucon")
@UniqueConstraint(columnNames = {"other", "other_one"})
public class EBasicWithUniqueCon {
@@ -0,0 +1,49 @@
package org.tests.model.basic;
import io.ebean.annotation.Cache;
import io.ebean.annotation.EnumValue;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
/**
* Cached bean with an Enum @Id (mapped via @EnumValue) for testing #3110.
*/
@Cache
@Entity
@Table(name = "e_cached_enum_id")
public class ECachedEnumId {
public enum Status {
@EnumValue("1")
NEW,
@EnumValue("2")
APPROVED,
@EnumValue("3")
DELETED,
}
@Id
Status status;
String name;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -219,4 +219,36 @@ class TestIdClassScalar extends BaseTestCase {
DB.deleteAll(Arrays.asList(user, site));
}
/**
* Test for #3275 - update/delete on a fresh bean instance that has not been read
* from the database or previously inserted via this same bean instance (e.g. built
* directly from external data) used to throw a NullPointerException as the IdClass
* id had not yet been derived/cached on the bean.
*/
@Test
void update_freshBeanInstance_notPreviouslyLoaded() {
UUID siteId = UUID.randomUUID();
UUID userId = UUID.randomUUID();
BSiteUserD access = new BSiteUserD(BAccessLevel.ONE, siteId, userId);
DB.save(access);
// a fresh bean instance - never read from the db or saved via this instance
BSiteUserD freshUpdate = new BSiteUserD(BAccessLevel.TWO, siteId, userId);
freshUpdate.setVersion(access.getVersion());
DB.update(freshUpdate);
BEmbId id = new BEmbId(siteId, userId);
BSiteUserD found = DB.find(BSiteUserD.class, id);
assertThat(found).isNotNull();
assertThat(found.getAccessLevel()).isEqualTo(BAccessLevel.TWO);
// a fresh bean instance used for delete
BSiteUserD freshDelete = new BSiteUserD(BAccessLevel.TWO, siteId, userId);
freshDelete.setVersion(found.getVersion());
DB.delete(freshDelete);
assertThat(DB.find(BSiteUserD.class, id)).isNull();
}
}
@@ -0,0 +1,72 @@
package org.tests.model.elementcollection;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OrderColumn;
import jakarta.persistence.Version;
import java.util.ArrayList;
import java.util.List;
/**
* Element collection of embeddable values with an explicit {@code @OrderColumn}.
*/
@Entity
public class EcolPerson {
@Id
long id;
String name;
@ElementCollection
@CollectionTable(joinColumns = @JoinColumn(name = "person_id"))
@OrderColumn(name = "ordinal")
List<EcPhone> phoneNumbers = new ArrayList<>();
@Version
long version;
public EcolPerson(String name) {
this.name = name;
}
@Override
public String toString() {
return "person id:" + id + " name:" + name + " phs:" + phoneNumbers;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<EcPhone> getPhoneNumbers() {
return phoneNumbers;
}
public void setPhoneNumbers(List<EcPhone> phoneNumbers) {
this.phoneNumbers = phoneNumbers;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
}
@@ -0,0 +1,72 @@
package org.tests.model.elementcollection;
import io.ebean.DB;
import io.ebean.test.LoggedSql;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for issue #2393 - {@code @OrderColumn} on an {@code @ElementCollection}.
*/
public class TestElementCollectionOrderColumn extends BaseTestCase {
@Test
public void insert_populatesOrderColumn() {
LoggedSql.start();
EcolPerson person = new EcolPerson("OrderCol1");
person.getPhoneNumbers().add(new EcPhone("64", "021", "1111"));
person.getPhoneNumbers().add(new EcPhone("64", "021", "2222"));
person.getPhoneNumbers().add(new EcPhone("64", "021", "3333"));
DB.save(person);
List<String> sql = LoggedSql.stop();
// find the insert statements for the collection table and check ordinal column/values
List<String> inserts = sql.stream()
.filter(s -> s.contains("insert into ecol_person_phone_numbers"))
.collect(Collectors.toList());
assertThat(inserts).isNotEmpty();
assertThat(inserts.get(0)).contains("ordinal");
// reload and confirm ordering is maintained
EcolPerson found = DB.find(EcolPerson.class, person.getId());
List<EcPhone> phones = found.getPhoneNumbers();
assertThat(phones).hasSize(3);
assertThat(phones.get(0).getNumber()).isEqualTo("1111");
assertThat(phones.get(1).getNumber()).isEqualTo("2222");
assertThat(phones.get(2).getNumber()).isEqualTo("3333");
DB.delete(person);
}
@Test
public void fetch_hasOrderByOnOrdinal() {
EcolPerson person = new EcolPerson("OrderCol2");
person.getPhoneNumbers().add(new EcPhone("64", "021", "1111"));
person.getPhoneNumbers().add(new EcPhone("64", "021", "2222"));
DB.save(person);
LoggedSql.start();
EcolPerson found = DB.find(EcolPerson.class)
.fetch("phoneNumbers")
.where().idEq(person.getId())
.findOne();
List<String> sql = LoggedSql.stop();
assertThat(sql).isNotEmpty();
String trimmed = trimSql(sql.get(0));
assertThat(trimmed).contains("order by").contains("ordinal");
assertThat(found.getPhoneNumbers()).hasSize(2);
DB.delete(person);
}
}
@@ -1,21 +1,36 @@
package org.tests.model.embedded;
import io.ebean.DB;
import org.junit.jupiter.api.Disabled;
import io.ebean.annotation.Platform;
import io.ebean.xtest.BaseTestCase;
import io.ebean.xtest.ForPlatform;
import org.junit.jupiter.api.Test;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
class TestEmbeddedDbArray {
class TestEmbeddedDbArray extends BaseTestCase {
/**
* Failing test case for #2477
* Test case for #2477 - inserting an empty collection into a @DbArray column
* nested inside an @ElementCollection @Embeddable used to throw a NoSuchElementException.
* <p>
* Restricted to Postgres as the underlying platform - other platforms (e.g. MariaDB, SQLServer)
* fall back to JSON storage for @DbArray and MultiValueBind doesn't support binding a single
* array value in that case, which is a separate, pre-existing limitation.
*/
@Disabled
@Test
void testArrayInsert() {
@ForPlatform(Platform.POSTGRES)
void testArrayInsert_empty() {
EmbArrayMaster t = new EmbArrayMaster(singletonList(new EmbArrayMaster.EmbArrayDetail(emptyList())));
DB.insert(t);
}
@Test
@ForPlatform(Platform.POSTGRES)
void testArrayInsert_nonEmpty() {
EmbArrayMaster t = new EmbArrayMaster(singletonList(new EmbArrayMaster.EmbArrayDetail(asList("a", "b"))));
DB.insert(t);
}
}
@@ -0,0 +1,87 @@
package org.tests.model.json;
import io.ebean.annotation.DbJson;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import java.util.Map;
import static io.ebean.annotation.MutationDetection.HASH;
import static io.ebean.annotation.MutationDetection.NONE;
import static io.ebean.annotation.MutationDetection.SOURCE;
/**
* Entity with {@code Map<String,Object>} @DbJson properties covering each
* {@code MutationDetection} mode - used to verify that the built-in Map JSON
* type honours mutationDetection rather than always using ModifyAware checking.
*/
@Entity
public class EBasicJsonMapMutation {
@Id
Long id;
@DbJson
Map<String, Object> defaultMap;
@DbJson(mutationDetection = NONE)
Map<String, Object> noneMap;
@DbJson(mutationDetection = HASH)
Map<String, Object> hashMap;
@DbJson(mutationDetection = SOURCE)
Map<String, Object> sourceMap;
@Version
Long version;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Map<String, Object> getDefaultMap() {
return defaultMap;
}
public void setDefaultMap(Map<String, Object> defaultMap) {
this.defaultMap = defaultMap;
}
public Map<String, Object> getNoneMap() {
return noneMap;
}
public void setNoneMap(Map<String, Object> noneMap) {
this.noneMap = noneMap;
}
public Map<String, Object> getHashMap() {
return hashMap;
}
public void setHashMap(Map<String, Object> hashMap) {
this.hashMap = hashMap;
}
public Map<String, Object> getSourceMap() {
return sourceMap;
}
public void setSourceMap(Map<String, Object> sourceMap) {
this.sourceMap = sourceMap;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
}
@@ -88,35 +88,6 @@ public class DeleteById_SoftDelete_Tests extends BaseTestCase {
cover.deletePermanent();
}
@Test
public void queryByIdDelete_when_softDelete() {
Cover cover = new Cover("q1");
cover.save();
LoggedSql.start();
DB.find(Cover.class).setId(cover.getId()).delete();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
if (isPlatformBooleanNative()) {
assertSql(sql.get(0)).contains("update cover set deleted=true where id = ?");
} else {
assertSql(sql.get(0)).contains("update cover set deleted=1 where id = ?");
}
assertNull(DB.find(Cover.class, cover.getId()));
Cover softDeleted = DB.find(Cover.class)
.setIncludeSoftDeletes()
.setId(cover.getId())
.findOne();
assertNotNull(softDeleted);
assertThat(softDeleted.isDeleted()).isTrue();
cover.deletePermanent();
}
@Test
public void deletePermanentById_when_softDelete() {
@@ -0,0 +1,37 @@
package org.tests.order;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class M2mOrderChild {
@Id
long id;
String name;
public M2mOrderChild() {
}
public M2mOrderChild(String name) {
this.name = name;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "M2mOrderChild[" + id + "," + name + "]";
}
}
@@ -0,0 +1,59 @@
package org.tests.order;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OrderColumn;
import java.util.ArrayList;
import java.util.List;
/**
* ManyToMany relationship with an {@code @OrderColumn} - the order value is stored on the
* intersection/join table (m2m_order_master_m2m_order_child) rather than on the target bean.
*/
@Entity
public class M2mOrderMaster {
@Id
long id;
String name;
@ManyToMany(cascade = CascadeType.ALL)
@OrderColumn(name = "sort_order")
List<M2mOrderChild> children = new ArrayList<>();
public M2mOrderMaster() {
}
public M2mOrderMaster(String name) {
this.name = name;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<M2mOrderChild> getChildren() {
return children;
}
public void setChildren(List<M2mOrderChild> children) {
this.children = children;
}
@Override
public String toString() {
return "M2mOrderMaster[" + id + "," + name + "]";
}
}
@@ -0,0 +1,98 @@
package org.tests.order;
import io.ebean.DB;
import io.ebean.xtest.base.TransactionalTestCase;
import io.ebean.test.LoggedSql;
import org.junit.jupiter.api.Test;
import java.util.Comparator;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class TestM2mOrderColumn extends TransactionalTestCase {
@Test
void insert_thenOrderIsPersistedAndReturnedInOrder() {
M2mOrderMaster master = new M2mOrderMaster("m1");
for (int i = 4; i >= 0; i--) {
master.getChildren().add(new M2mOrderChild("c" + i));
}
DB.save(master);
// plain findOne() + lazy load of children (secondary query)
M2mOrderMaster result = DB.find(M2mOrderMaster.class).findOneOrEmpty().orElseThrow();
assertThat(result.getChildren()).extracting(M2mOrderChild::getName)
.containsExactly("c4", "c3", "c2", "c1", "c0");
}
@Test
void fetch_thenOrderIsRespected() {
M2mOrderMaster master = new M2mOrderMaster("mf");
for (int i = 4; i >= 0; i--) {
master.getChildren().add(new M2mOrderChild("cf" + i));
}
DB.save(master);
// fetch("children") uses a SQL JOIN rather than a secondary query
M2mOrderMaster result = DB.find(M2mOrderMaster.class)
.fetch("children")
.where().idEq(master.getId())
.findOneOrEmpty()
.orElseThrow();
assertThat(result.getChildren()).extracting(M2mOrderChild::getName)
.containsExactly("cf4", "cf3", "cf2", "cf1", "cf0");
}
@Test
void fetchQuery_thenOrderIsRespected() {
M2mOrderMaster master = new M2mOrderMaster("m2");
for (int i = 4; i >= 0; i--) {
master.getChildren().add(new M2mOrderChild("cq" + i));
}
DB.save(master);
M2mOrderMaster result = DB.find(M2mOrderMaster.class)
.fetchQuery("children")
.where().idEq(master.getId())
.findOneOrEmpty()
.orElseThrow();
assertThat(result.getChildren()).extracting(M2mOrderChild::getName)
.containsExactly("cq4", "cq3", "cq2", "cq1", "cq0");
}
@Test
void reorder_thenNewOrderPersistedOnReload() {
M2mOrderMaster master = new M2mOrderMaster("m3");
for (int i = 0; i < 5; i++) {
master.getChildren().add(new M2mOrderChild("r" + i));
}
DB.save(master);
M2mOrderMaster result = DB.find(M2mOrderMaster.class).findOneOrEmpty().orElseThrow();
result.getChildren().sort(Comparator.comparing(M2mOrderChild::getName).reversed());
DB.save(result);
M2mOrderMaster reloaded = DB.find(M2mOrderMaster.class).findOneOrEmpty().orElseThrow();
assertThat(reloaded.getChildren()).extracting(M2mOrderChild::getName)
.containsExactly("r4", "r3", "r2", "r1", "r0");
}
@Test
void insert_sqlContainsOrderColumn() {
M2mOrderMaster master = new M2mOrderMaster("m4");
master.getChildren().add(new M2mOrderChild("s0"));
master.getChildren().add(new M2mOrderChild("s1"));
LoggedSql.start();
DB.save(master);
List<String> sql = LoggedSql.stop();
boolean foundIntersectionInsert = sql.stream().anyMatch(s -> s.contains("insert into") && s.contains("sort_order"));
assertThat(foundIntersectionInsert)
.as("sql: %s", sql)
.isTrue();
}
}
@@ -169,6 +169,7 @@ SET @@system_versioning_alter_history = 1;
-- NOTE: table has @History - special migration may be necessary
update migtest_e_history6 set test_number1 = 42 where test_number1 is null;
-- apply alter tables
alter table `table` modify `index` varchar(255) comment 'this is an other comment';
alter table `table` modify textfield varchar(255);
alter table `table` add column `select` varchar(255);
alter table `table` add column textfield2 varchar(255);
@@ -185,7 +186,7 @@ alter table migtest_e_basic add column new_boolean_field2 tinyint(1) default 1 n
alter table migtest_e_basic add column progress integer default 0 not null;
alter table migtest_e_basic add column new_integer integer default 42 not null;
alter table migtest_e_history add system versioning;
alter table migtest_e_history modify test_string bigint;
alter table migtest_e_history modify test_string bigint comment 'Column altered to long now';
alter table migtest_e_history2 modify test_string varchar(255) not null default 'unknown';
alter table migtest_e_history2 add column test_string2 varchar(255);
alter table migtest_e_history2 add column test_string3 varchar(255) default 'unknown' not null;
@@ -50,6 +50,7 @@ SET @@system_versioning_alter_history = 1;
-- NOTE: table has @History - special migration may be necessary
update migtest_e_history6 set test_number2 = 7 where test_number2 is null;
-- apply alter tables
alter table `table` modify `index` varchar(255) comment 'this is a comment';
alter table migtest_e_basic modify status varchar(1);
alter table migtest_e_basic modify status2 varchar(1) not null default 'N';
alter table migtest_e_basic modify a_lob varchar(255) not null default 'X';
@@ -58,6 +59,7 @@ alter table migtest_e_basic add column description_file longblob;
alter table migtest_e_basic add column old_boolean tinyint(1) default 0 not null;
alter table migtest_e_basic add column old_boolean2 tinyint(1);
alter table migtest_e_basic add column eref_id integer;
alter table migtest_e_history modify test_string bigint;
alter table migtest_e_history2 modify test_string varchar(255);
alter table migtest_e_history2 add column obsolete_string1 varchar(255);
alter table migtest_e_history2 add column obsolete_string2 varchar(255);
@@ -1,7 +1,7 @@
-2111548334, 1.0__initial.sql
1085383251, 1.1.sql
-858370485, 1.1.sql
919151678, 1.2__dropsFor_1.1.sql
-1145514481, 1.3.sql
1360066142, 1.3.sql
-924292968, 1.4__dropsFor_1.3.sql
561281075, R__order_views.sql
@@ -169,6 +169,7 @@ SET @@system_versioning_alter_history = 1;
-- NOTE: table has @History - special migration may be necessary
update migtest_e_history6 set test_number1 = 42 where test_number1 is null;
-- apply alter tables
alter table `table` modify `index` varchar(255) comment 'this is an other comment';
alter table `table` modify textfield varchar(255);
alter table `table` add column `select` varchar(255);
alter table `table` add column textfield2 varchar(255);
@@ -185,7 +186,7 @@ alter table migtest_e_basic add column new_boolean_field2 tinyint(1) default 1 n
alter table migtest_e_basic add column progress integer default 0 not null;
alter table migtest_e_basic add column new_integer integer default 42 not null;
alter table migtest_e_history add system versioning;
alter table migtest_e_history modify test_string bigint;
alter table migtest_e_history modify test_string bigint comment 'Column altered to long now';
alter table migtest_e_history2 modify test_string varchar(255) not null default 'unknown';
alter table migtest_e_history2 add column test_string2 varchar(255);
alter table migtest_e_history2 add column test_string3 varchar(255) default 'unknown' not null;
@@ -50,6 +50,7 @@ SET @@system_versioning_alter_history = 1;
-- NOTE: table has @History - special migration may be necessary
update migtest_e_history6 set test_number2 = 7 where test_number2 is null;
-- apply alter tables
alter table `table` modify `index` varchar(255) comment 'this is a comment';
alter table migtest_e_basic modify status varchar(1);
alter table migtest_e_basic modify status2 varchar(1) not null default 'N';
alter table migtest_e_basic modify a_lob varchar(255) not null default 'X';
@@ -58,6 +59,7 @@ alter table migtest_e_basic add column description_file longblob;
alter table migtest_e_basic add column old_boolean tinyint(1) default 0 not null;
alter table migtest_e_basic add column old_boolean2 tinyint(1);
alter table migtest_e_basic add column eref_id integer;
alter table migtest_e_history modify test_string bigint;
alter table migtest_e_history2 modify test_string varchar(255);
alter table migtest_e_history2 add column obsolete_string1 varchar(255);
alter table migtest_e_history2 add column obsolete_string2 varchar(255);
@@ -1,8 +1,8 @@
725100959, I__create_procs.sql
-2111548334, 1.0__initial.sql
1085383251, 1.1.sql
-858370485, 1.1.sql
1187950993, 1.2__dropsFor_1.1.sql
-1145514481, 1.3.sql
1360066142, 1.3.sql
-298825700, 1.4__dropsFor_1.3.sql
561281075, R__order_views.sql
@@ -93,7 +93,7 @@
<addColumn tableName="migtest_e_softdelete">
<column name="deleted" type="boolean" defaultValue="false" notnull="true"/>
</addColumn>
<alterColumn columnName="&quot;index&quot;" tableName="&quot;table&quot;" withHistory="true" comment="this is an other comment"/>
<alterColumn columnName="&quot;index&quot;" tableName="&quot;table&quot;" withHistory="true" currentType="varchar" currentNotnull="false" comment="this is an other comment" currentComment="this is a comment"/>
<alterColumn columnName="textfield" tableName="&quot;table&quot;" withHistory="true" currentType="varchar" notnull="false" currentNotnull="true"/>
<addColumn tableName="&quot;table&quot;" withHistory="true">
<column name="&quot;select&quot;" type="varchar"/>
@@ -25,7 +25,7 @@
<addUniqueConstraint constraintName="uq_migtest_e_basic_indextest6" tableName="migtest_e_basic" columnNames="indextest6" oneToOne="false" nullableColumns="indextest6"/>
<alterTable name="migtest_e_basic" tablespace="db2;TSTABLES;" indexTablespace="db2;INDEXTS;" lobTablespace="db2;TSTABLES;"/>
<alterColumn columnName="test_status" tableName="migtest_e_enum" checkConstraint="check ( test_status in ('N','A','I'))" checkConstraintName="ck_migtest_e_enum_test_status"/>
<alterColumn columnName="test_string" tableName="migtest_e_history" withHistory="true" comment="DROP COMMENT"/>
<alterColumn columnName="test_string" tableName="migtest_e_history" withHistory="true" currentType="bigint" currentNotnull="false" comment="DROP COMMENT" currentComment="Column altered to long now"/>
<addTableComment name="migtest_e_history" comment="DROP COMMENT"/>
<alterColumn columnName="test_string" tableName="migtest_e_history2" withHistory="true" currentType="varchar" defaultValue="DROP DEFAULT" notnull="false" currentNotnull="true"/>
<addColumn tableName="migtest_e_history2" withHistory="true">
@@ -47,7 +47,7 @@
<column name="name" type="varchar(127)" notnull="true"/>
<uniqueConstraint name="uq_migtest_e_ref_name" columnNames="name" oneToOne="false" nullableColumns=""/>
</createTable>
<alterColumn columnName="&quot;index&quot;" tableName="&quot;table&quot;" withHistory="true" comment="this is a comment"/>
<alterColumn columnName="&quot;index&quot;" tableName="&quot;table&quot;" withHistory="true" currentType="varchar" currentNotnull="false" comment="this is a comment" currentComment="this is an other comment"/>
<alterTable name="migtest_mtm_c" tablespace="$TABLESPACE_DEFAULT" indexTablespace="$TABLESPACE_DEFAULT" lobTablespace="$TABLESPACE_DEFAULT"/>
<alterTable name="migtest_mtm_m" tablespace="$TABLESPACE_DEFAULT" indexTablespace="$TABLESPACE_DEFAULT" lobTablespace="$TABLESPACE_DEFAULT"/>
<addUniqueConstraint constraintName="uq_m12_otoc72" tableName="migtest_oto_child" columnNames="name" oneToOne="false" nullableColumns="name" platforms="MYSQL"/>
@@ -186,6 +186,7 @@ drop trigger table_history_upd;
drop trigger table_history_del;
drop view table_with_history;
-- apply alter tables
alter table `table` modify `index` varchar(255) comment 'this is an other comment';
alter table `table` modify textfield varchar(255);
alter table `table` add column `select` varchar(255);
alter table `table` add column textfield2 varchar(255);
@@ -203,7 +204,7 @@ alter table migtest_e_basic add column progress integer default 0 not null;
alter table migtest_e_basic add column new_integer integer default 42 not null;
alter table migtest_e_history add column sys_period_start datetime(6) default now(6);
alter table migtest_e_history add column sys_period_end datetime(6);
alter table migtest_e_history modify test_string bigint;
alter table migtest_e_history modify test_string bigint comment 'Column altered to long now';
alter table migtest_e_history2 modify test_string varchar(255) not null default 'unknown';
alter table migtest_e_history2 add column test_string2 varchar(255);
alter table migtest_e_history2 add column test_string3 varchar(255) default 'unknown' not null;
@@ -61,6 +61,7 @@ drop view migtest_e_history6_with_history;
-- NOTE: table has @History - special migration may be necessary
update migtest_e_history6 set test_number2 = 7 where test_number2 is null;
-- apply alter tables
alter table `table` modify `index` varchar(255) comment 'this is a comment';
alter table migtest_e_basic modify status varchar(1);
alter table migtest_e_basic modify status2 varchar(1) not null default 'N';
alter table migtest_e_basic modify a_lob varchar(255) not null default 'X';
@@ -69,6 +70,7 @@ alter table migtest_e_basic add column description_file longblob;
alter table migtest_e_basic add column old_boolean tinyint(1) default 0 not null;
alter table migtest_e_basic add column old_boolean2 tinyint(1);
alter table migtest_e_basic add column eref_id integer;
alter table migtest_e_history modify test_string bigint;
alter table migtest_e_history2 modify test_string varchar(255);
alter table migtest_e_history2 add column obsolete_string1 varchar(255);
alter table migtest_e_history2 add column obsolete_string2 varchar(255);
@@ -1,8 +1,8 @@
725100959, I__create_procs.sql
1980830787, 1.0__initial.sql
-151422898, 1.1.sql
1568294555, 1.1.sql
-1097227916, 1.2__dropsFor_1.1.sql
888094006, 1.3.sql
2018541974, 1.3.sql
261977585, 1.4__dropsFor_1.3.sql
561281075, R__order_views.sql
@@ -186,6 +186,7 @@ drop trigger table_history_upd;
drop trigger table_history_del;
drop view table_with_history;
-- apply alter tables
alter table `table` modify `index` varchar(255) comment 'this is an other comment';
alter table `table` modify textfield varchar(255);
alter table `table` add column `select` varchar(255);
alter table `table` add column textfield2 varchar(255);
@@ -203,7 +204,7 @@ alter table migtest_e_basic add column progress integer default 0 not null;
alter table migtest_e_basic add column new_integer integer default 42 not null;
alter table migtest_e_history add column sys_period_start datetime(6) default now(6);
alter table migtest_e_history add column sys_period_end datetime(6);
alter table migtest_e_history modify test_string bigint;
alter table migtest_e_history modify test_string bigint comment 'Column altered to long now';
alter table migtest_e_history2 modify test_string varchar(255) not null default 'unknown';
alter table migtest_e_history2 add column test_string2 varchar(255);
alter table migtest_e_history2 add column test_string3 varchar(255) default 'unknown' not null;
@@ -61,6 +61,7 @@ drop view migtest_e_history6_with_history;
-- NOTE: table has @History - special migration may be necessary
update migtest_e_history6 set test_number2 = 7 where test_number2 is null;
-- apply alter tables
alter table `table` modify `index` varchar(255) comment 'this is a comment';
alter table migtest_e_basic modify status varchar(1);
alter table migtest_e_basic modify status2 varchar(1) not null default 'N';
alter table migtest_e_basic modify a_lob varchar(255) not null default 'X';
@@ -69,6 +70,7 @@ alter table migtest_e_basic add column description_file longblob;
alter table migtest_e_basic add column old_boolean tinyint(1) default 0 not null;
alter table migtest_e_basic add column old_boolean2 tinyint(1);
alter table migtest_e_basic add column eref_id integer;
alter table migtest_e_history modify test_string bigint;
alter table migtest_e_history2 modify test_string varchar(255);
alter table migtest_e_history2 add column obsolete_string1 varchar(255);
alter table migtest_e_history2 add column obsolete_string2 varchar(255);
@@ -1,8 +1,8 @@
725100959, I__create_procs.sql
369577572, 1.0__initial.sql
-151422898, 1.1.sql
1568294555, 1.1.sql
-1097227916, 1.2__dropsFor_1.1.sql
888094006, 1.3.sql
2018541974, 1.3.sql
261977585, 1.4__dropsFor_1.3.sql
561281075, R__order_views.sql
@@ -33,6 +33,8 @@ public class OraclePlatform extends DatabasePlatform {
this.dbDefaultValue.setTrue("1");
this.dbDefaultValue.setNow("current_timestamp");
this.likeClauseRaw = "like ?";
this.existsWithCaseWhen = true;
this.existsFromClause = " from dual";
this.exceptionTranslator =
new SqlErrorCodes()
@@ -45,4 +45,11 @@ class OraclePlatformTest {
DbPlatformType dbType = platform.dbTypeMap().get(DbPlatformType.UUID);
assertThat(dbType.renderType(0, 0)).isEqualTo("raw(16)");
}
@Test
void existsWithCaseWhen_trueForOracle() {
OraclePlatform platform = new OraclePlatform();
assertThat(platform.existsWithCaseWhen()).isTrue();
assertThat(platform.existsFromClause()).isEqualTo(" from dual");
}
}
+1 -1
View File
@@ -29,7 +29,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.2</version>
<version>42.7.11</version>
<scope>test</scope>
</dependency>
@@ -26,6 +26,7 @@ abstract class SqlServerBasePlatform extends DatabasePlatform {
this.idInExpandedForm = true;
this.selectCountWithAlias = true;
this.selectCountWithColumnAlias = true;
this.existsWithCaseWhen = true;
this.sqlLimiter = new SqlServerSqlLimiter();
this.basicSqlLimiter = new SqlServerBasicSqlLimiter();
this.historySupport = new SqlServerHistorySupport();
@@ -40,6 +40,12 @@ class SqlServerPlatformTest {
assertEquals(dbPlatform.unQuote("[firstName]"), "firstName");
}
@Test
public void existsWithCaseWhen_trueForSqlServer() {
SqlServer17Platform dbPlatform = new SqlServer17Platform();
assertEquals(dbPlatform.existsWithCaseWhen(), true);
}
@Test
public void defaultTypesForDecimalAndVarchar() {
DatabasePlatform dbPlatform = new DatabasePlatform();