From 315640df6b75f0d8bd1cba3c731b8eab3ea9ddae Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 26 Jan 2022 08:09:45 +0100 Subject: [PATCH 1/9] ADD: Failing test case --- .../java/org/tests/delete/TestDeleteByQuery.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ebean-test/src/test/java/org/tests/delete/TestDeleteByQuery.java b/ebean-test/src/test/java/org/tests/delete/TestDeleteByQuery.java index 4e826b584..8ff2ad54b 100644 --- a/ebean-test/src/test/java/org/tests/delete/TestDeleteByQuery.java +++ b/ebean-test/src/test/java/org/tests/delete/TestDeleteByQuery.java @@ -7,6 +7,7 @@ import io.ebean.test.LoggedSql; import org.junit.jupiter.api.Test; import org.tests.model.basic.*; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -233,6 +234,17 @@ public class TestDeleteByQuery extends BaseTestCase { DB.save(contact); + // check if we can delete more than 2100 in a batch on SqlServer + List moreContacts = new ArrayList<>(); + for (int i = 0; i < 2200; i++) { + Contact c = new Contact(); + c.setFirstName("DelByQueryFirstName"); + c.setLastName("deleteMe"); + c.setCustomer(all.get(0)); + moreContacts.add(c); + } + DB.saveAll(moreContacts); + DB.find(Contact.class).where().eq("firstName", "DelByQueryFirstName").delete(); Contact contactFind = DB.find(Contact.class, contact.getId()); From 7ffea8e922dcc46c92279dc4dab639c33d15aa45 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 26 Jan 2022 08:25:24 +0100 Subject: [PATCH 2/9] FIX: DeleteBatch for SQLServer --- .../server/persist/DefaultPersister.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 701b76402..eaa1e1c95 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -1,6 +1,7 @@ package io.ebeaninternal.server.persist; import io.ebean.*; +import io.ebean.annotation.Platform; import io.ebean.bean.BeanCollection; import io.ebean.bean.BeanCollection.ModifyListenMode; import io.ebean.bean.EntityBean; @@ -644,11 +645,25 @@ public final class DefaultPersister implements Persister { DeleteMode deleteMode = (permanent || !descriptor.isSoftDelete()) ? DeleteMode.HARD : DeleteMode.SOFT; return delete(descriptor, null, idList, transaction, deleteMode); } - + /** * Delete by Id or a List of Id's. */ private int delete(BeanDescriptor descriptor, Object id, List idList, Transaction transaction, DeleteMode deleteMode) { + int rows = 0; + if (server.databasePlatform().isPlatform(Platform.SQLSERVER)) { + // SqlServer has a 2100 parameter limit, so delete max 2000 ids at once + // this gives space up to 100 more query parameters. + while (idList != null && idList.size() > 2000) { + rows += deleteBatch(descriptor, id, idList.subList(0, 2000), transaction, deleteMode); + idList = idList.subList(2000, idList.size()); + } + } + rows += deleteBatch(descriptor, id, idList, transaction, deleteMode); + return rows; + } + + private int deleteBatch(BeanDescriptor descriptor, Object id, List idList, Transaction transaction, DeleteMode deleteMode) { SpiTransaction t = (SpiTransaction) transaction; if (t.isPersistCascade()) { BeanPropertyAssocOne[] propImportDelete = descriptor.propertiesOneImportedDelete(); From e5f37e638c28b90b177c97ddf100d142ac8dd77c Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 26 Jan 2022 11:46:02 +0100 Subject: [PATCH 3/9] Refactor UUID generator. Add uuid-property to serverConfig (#57) Co-authored-by: Roland Praml --- .../java/io/ebean/config/DatabaseConfig.java | 42 +++++ .../deploy/meta/DeployBeanDescriptor.java | 2 +- .../server/idgen/UuidV1IdGenerator.java | 175 ++++++++++++------ .../server/idgen/TestUuidGenerator.java | 133 ++++++++++++- 4 files changed, 290 insertions(+), 62 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index 281d0ea79..8042e32b6 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -371,6 +371,33 @@ public class DatabaseConfig { */ private String uuidStateFile; + /** + * The node id (=mac address) for Version 1 UUIDs. There are several options: + *
    + *
  • null (default) The generator tries to get the hardwarwe MAC + * address. If this fails, it will fall back to 'generate' mode.
  • + *
  • "generate" Hardware detection is skipped. It generates a + * random identifier and tries to persist this to the state file. This nodeId + * will be reused on next start. If persisting to the state file will fail also, + * it will fall back to 'random' mode.
    + * This mode is good, if the MAC address is not reliable, e.g. if you run + * multiple ebean instances on the same machine.
  • + *
  • "random" In this mode, a random node id is generated on each + * start. No stateFile is used. it will generate a new nodeId on each + * application start.
    + * This mode is good, if you have no write access to save the state file.
  • + *
  • "xx-xx-xx-xx-xx-xx" When an explicit nodeId is specified, + * this one is used.
  • + *
+ * Note: It is possible that multiple servers are sharing the same state file as + * long as they are in the same JVM/ClassLoader scope. In this case it is + * recommended to use the same uuidNodeId configuration. + * + * If you have multiple servers in different JVMs, do not share the state + * files! + */ + private String uuidNodeId; + /** * The clock used for setting the timestamps (e.g. @UpdatedTimestamp) on objects. */ @@ -2019,6 +2046,20 @@ public class DatabaseConfig { public void setUuidStateFile(String uuidStateFile) { this.uuidStateFile = uuidStateFile; } + + /** + * Returns the V1-UUID-NodeId + */ + public String getUuidNodeId() { + return uuidNodeId; + } + + /** + * Sets the V1-UUID-NodeId. + */ + public void setUuidNodeId(String uuidNodeId) { + this.uuidNodeId = uuidNodeId; + } /** * Return true if LocalTime should be persisted with nanos precision. @@ -2888,6 +2929,7 @@ public class DatabaseConfig { uuidVersion = p.getEnum(UuidVersion.class, "uuidVersion", uuidVersion); uuidStateFile = p.get("uuidStateFile", uuidStateFile); + uuidNodeId = p.get("uuidNodeId", uuidNodeId); localTimeWithNanos = p.getBoolean("localTimeWithNanos", localTimeWithNanos); jodaLocalTimeMode = p.get("jodaLocalTimeMode", jodaLocalTimeMode); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index e670dd136..effef1034 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -775,7 +775,7 @@ public class DeployBeanDescriptor { this.identityMode.setIdType(IdType.EXTERNAL); switch (config.getUuidVersion()) { case VERSION1: - this.idGenerator = UuidV1IdGenerator.getInstance(config.getUuidStateFile()); + this.idGenerator = UuidV1IdGenerator.getInstance(config.getUuidStateFile(), config.getUuidNodeId()); break; case VERSION1RND: this.idGenerator = UuidV1RndIdGenerator.INSTANCE; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1IdGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1IdGenerator.java index cdd99ea44..083e08df2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1IdGenerator.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/idgen/UuidV1IdGenerator.java @@ -12,7 +12,8 @@ import java.util.concurrent.ConcurrentHashMap; * It extends the UuidV1RndIdGenerator so that it can generate rfc4122 compliant Type 1 UUIDs. * * This generator produces real Type 1 UUIDs (best for sqlserver) - You should use this generator only, - * if you can guarantee, that mac addess is uniqe. + * if you can guarantee, that mac addess is uniqe or specify 'ebean.uuidNodeId' in your config file. + * */ public class UuidV1IdGenerator extends UuidV1RndIdGenerator { @@ -25,38 +26,36 @@ public class UuidV1IdGenerator extends UuidV1RndIdGenerator { /** * Returns an instance for given file. */ - public static UuidV1IdGenerator getInstance(String file) { - return INSTANCES.computeIfAbsent(new File(file), UuidV1IdGenerator::new); + public static UuidV1IdGenerator getInstance(String file, String nodeId) { + return getInstance(new File(file), nodeId); } /** * Returns an instance for given file. */ - public static UuidV1IdGenerator getInstance(File file) { - return INSTANCES.computeIfAbsent(file, UuidV1IdGenerator::new); + public static UuidV1IdGenerator getInstance(File file, String nodeId) { + return INSTANCES.computeIfAbsent(file, f ->new UuidV1IdGenerator(f, nodeId == null ? null : nodeId.toLowerCase())); } /** * Returns an alternative node id - set with the 'ebean.uuid.nodeId' system property. */ - private static byte[] getAlternativeNodeId() { + private static byte[] parseAlternativeNodeId(String altNodeId) { + String[] components = altNodeId.split("-"); + if (components.length != 6) { + throw new IllegalArgumentException(altNodeId + " is invalid. Expected format: xx-xx-xx-xx-xx-xx"); + } try { - String altNodeId = System.getProperty("ebean.uuid.nodeId"); - if (altNodeId != null) { - String[] components = altNodeId.split("-"); - if (components.length != 5) { - throw new IllegalArgumentException("Invalid nodeId string: " + altNodeId); - } - byte[] nodeId = new byte[6]; - for (int i=0; i<5; i++) { - nodeId[i] = Byte.decode("0x"+components[i]).byteValue(); - } - return nodeId; + byte[] nodeId = new byte[6]; + for (int i = 0; i < 6; i++) { + // do not use Byte.parseByte + // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6259307 + nodeId[i] = (byte) Integer.parseInt(components[i], 16); } - } catch (SecurityException se) { - // ignore + return nodeId; + } catch (IllegalArgumentException iae) { + throw new IllegalArgumentException(altNodeId + " is invalid.", iae); } - return null; } /** @@ -108,43 +107,85 @@ public class UuidV1IdGenerator extends UuidV1RndIdGenerator { * Creates a new instance of UuidGenerator. Note that there should not be more * than one instance per stateFile. */ - private UuidV1IdGenerator(final File stateFile) { + private UuidV1IdGenerator(final File stateFile, String altNodeId) { super(); this.stateFile = stateFile; try { - // See, if there is an alternative MAC address set. - nodeId = getAlternativeNodeId(); - if (nodeId != null) { - log.info("Using alternative MAC {} to generate Type 1 UUIDs", getNodeIdentifier()); + if (altNodeId == null) { + // using hardware mode + tryHardwareId(); + } else if (altNodeId.equals("generate")) { + tryGenerateMode(); + } else if (altNodeId.equals("random")) { + useRandomMode(); } else { - nodeId = getHardwareId(); - log.info("Using MAC {} to generate Type 1 UUIDs", getNodeIdentifier()); + // See, if there is an alternative MAC address set. + nodeId = parseAlternativeNodeId(altNodeId); + restoreState(); + log.info("Explicitly using ID {} to generate Type 1 UUIDs", getNodeIdentifier()); } - if (nodeId == null) { - canSaveState = false; - // RFC 4.5 use random portion for node - nodeId = super.getNodeIdBytes(); - log.error("Have to fall back to random node identifier {} (Reason: No suitable network interface found)", getNodeIdentifier()); + UUID uuid = nextId(null); + long ts = timeStamp.get(); + ts -= UUID_EPOCH_OFFSET; + ts /= MILLIS_TO_UUID; - } else { - boolean flag = restoreState(); - UUID uuid = nextId(null); - long ts = timeStamp.get(); - ts -= UUID_EPOCH_OFFSET; - ts /= MILLIS_TO_UUID; - - saveState(); - log.debug("RestoreState: {}, ClockSeq {}, Timestamp {}, uuid {}, stateFile: {})", flag, clockSeq.get(), - new Date(ts), uuid, stateFile); - } + saveState(); + log.debug("Saved state: clockSeq {}, timestamp {}, uuid {}, stateFile: {})", clockSeq.get(), new Date(ts), uuid, stateFile); } catch (IOException e) { - canSaveState = false; + log.error("There was a problem while detecting the nodeId. Falling back to random mode. Try using to specify 'ebean.uuidNodeId' property", e); + useRandomMode(); + } + } + + /** + * Tries to initialize the generator by retrieving the MAC address from + * hardware. If there is no suitable network interface found, it will fall back + * to "generate" mode. + * + * @throws IOException if state file is not readable or tryGenerateMode also + * fails. + */ + private void tryHardwareId() throws IOException { + try { + nodeId = getHardwareId(); + } catch (IOException e) { + log.error("Error while reading MAC address. Fall back to 'generate' mode", e); + tryGenerateMode(); + } + + if (nodeId != null) { + restoreState(); + log.info("Using MAC {} to generate Type 1 UUIDs", getNodeIdentifier()); + return; + } + log.warn("No suitable network interface found. Fall back to 'generate' mode"); + tryGenerateMode(); + } + + /** + * Tries the "generate" mode. A nodeId is generated once and saved to the state + * file + */ + private void tryGenerateMode() throws IOException { + if (restoreState()) { + log.info("Using recently generated nodeId {} to generate Type 1 UUIDs", getNodeIdentifier()); + } else { // RFC 4.5 use random portion for node nodeId = super.getNodeIdBytes(); - log.error("Have to fall back to random node identifier {} (Reason: {} )", getNodeIdentifier(), e.getMessage()); + log.info("Using a newly generated nodeId {} to generate Type 1 UUIDs", getNodeIdentifier()); } } + /** + * Random mode. A nodeId is generated every start up. no state file is + * maintained. This should always work. + */ + private void useRandomMode() { + canSaveState = false; + nodeId = super.getNodeIdBytes(); + log.info("Explicitly using a new random ID {} to generate Type 1 UUIDs", getNodeIdentifier()); + } + /** * Returns the Node-identifier (=MAC address) as string */ @@ -164,22 +205,38 @@ public class UuidV1IdGenerator extends UuidV1RndIdGenerator { */ private boolean restoreState() throws IOException { Properties prop = new Properties(); - if (stateFile.exists()) { - try (InputStream is = new FileInputStream(stateFile)) { - prop.load(is); - } + if (!stateFile.exists()) { + log.debug("State file '{}' does not exist", stateFile); + return false; } - if (getNodeIdentifier().equals(prop.getProperty("nodeId"))) { - try { - Integer seq = Integer.valueOf(prop.getProperty("clockSeq")) & 0x3FFF; - Long ts = Long.valueOf(prop.getProperty("timeStamp")); - clockSeq.set(seq); - timeStamp.set(ts); - log.debug("Restored state from '{}'", stateFile); - return true; - } catch (NumberFormatException nfe) { - // nop + try (InputStream is = new FileInputStream(stateFile)) { + prop.load(is); + } + + String propNodeId = prop.getProperty("nodeId"); + if (propNodeId == null || propNodeId.isEmpty()) { + log.warn("State file '{}' is incomplete", stateFile); + return false; // we cannot restore + } + try { + if (nodeId == null) { + nodeId = parseAlternativeNodeId(propNodeId); + } else if (!getNodeIdentifier().equals(propNodeId)) { + log.warn( + "The nodeId in the state file '{}' has changed from {} to {}. " + + "This can happen when MAC address changes or when two containers share the same state file", + stateFile, propNodeId, getNodeIdentifier()); + return false; } + Integer seq = Integer.valueOf(prop.getProperty("clockSeq")) & 0x3FFF; + Long ts = Long.valueOf(prop.getProperty("timeStamp")); + clockSeq.set(seq); + timeStamp.set(ts); + log.debug("State successfully restored: {}", prop); + return true; + + } catch (IllegalArgumentException nfe) { + log.error("State file '{}' is corrupt", stateFile, nfe); } return false; } diff --git a/ebean-test/src/test/java/io/ebeaninternal/server/idgen/TestUuidGenerator.java b/ebean-test/src/test/java/io/ebeaninternal/server/idgen/TestUuidGenerator.java index 50f8e81b9..9bed5a1d0 100644 --- a/ebean-test/src/test/java/io/ebeaninternal/server/idgen/TestUuidGenerator.java +++ b/ebean-test/src/test/java/io/ebeaninternal/server/idgen/TestUuidGenerator.java @@ -5,14 +5,26 @@ package io.ebeaninternal.server.idgen; import io.ebean.config.dbplatform.PlatformIdGenerator; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.Map; +import java.util.Properties; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.within; /** * Run some simple tests for the UUID generator. @@ -52,12 +64,129 @@ public class TestUuidGenerator { } } + private File stateFile; + + @BeforeEach + void beforeEach() throws IOException { + stateFile = new File("target/" + UUID.randomUUID() + ".state"); + } + + @AfterEach + void afterEach() { + stateFile.delete(); + } + + private void writePropertyFile(String nodeId, String clockSeq, String timestamp) throws IOException { + Properties prop = new Properties(); + prop.setProperty("nodeId", nodeId); + prop.setProperty("clockSeq", clockSeq); + prop.setProperty("timeStamp", timestamp); + try (OutputStream os = new FileOutputStream(stateFile)) { + prop.store(os, "ebean uuid state file"); + } + } + + private Properties readPropertyFile() throws IOException { + Properties prop = new Properties(); + try (InputStream is = new FileInputStream(stateFile)) { + prop.load(is); + } + return prop; + } + /** + * Test takes ~0.3 sec + */ + @Test + public void testUuidFixedMac() throws Exception { + UuidV1IdGenerator gen = UuidV1IdGenerator.getInstance(stateFile, "01-02-03-04-05-06"); + UUID uuid = gen.nextId(null); + assertThat(uuid.node()).isEqualTo(0x010203040506L); + Properties props = readPropertyFile(); + assertThat(props) + .containsEntry("nodeId", "01-02-03-04-05-06") + .containsEntry("clockSeq", String.valueOf(uuid.clockSequence())); + assertThat(Long.parseLong(props.getProperty("timeStamp"))) + .isCloseTo(uuid.timestamp(), within(2_000_000L)); + } + + @Test + public void testUuidInvalidMac() throws Exception { + assertThatThrownBy(()-> UuidV1IdGenerator.getInstance(stateFile, "01-02-03-04-05")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("01-02-03-04-05 is invalid. Expected format: xx-xx-xx-xx-xx-xx"); + assertThatThrownBy(()-> UuidV1IdGenerator.getInstance(stateFile, "01-02-03-04-05-GG")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("01-02-03-04-05-gg is invalid.") + .hasRootCauseInstanceOf(NumberFormatException.class) + .hasRootCauseMessage("For input string: \"gg\""); + assertThat(stateFile).doesNotExist(); + } + + @Test + public void testUuidGenerate() throws Exception { + UuidV1IdGenerator gen = UuidV1IdGenerator.getInstance(stateFile, "generate"); + UUID uuid = gen.nextId(null); + assertThat(uuid).isNotNull(); + assertThat(stateFile).exists(); + } + + @Test + public void testUuidRandom() throws Exception { + UuidV1IdGenerator gen = UuidV1IdGenerator.getInstance(stateFile, "random"); + UUID uuid = gen.nextId(null); + assertThat(uuid).isNotNull(); + assertThat(stateFile).doesNotExist(); + } + + @Test + public void testUuidFile() throws Exception { + writePropertyFile("12-34-56-78-90-AB", "4252", "0"); + UuidV1IdGenerator gen = UuidV1IdGenerator.getInstance(stateFile, "generate"); + UUID uuid = gen.nextId(null); + assertThat(uuid.node()).isEqualTo(0x1234567890ABL); + assertThat(uuid.clockSequence()).isEqualTo(4252); + } + + @Test + public void testUuidFileInvalidNodeId() throws Exception { + writePropertyFile("AB-CD-EF-GH-IJ-KL", "4252", "0"); + UuidV1IdGenerator gen = UuidV1IdGenerator.getInstance(stateFile, "generate"); + // a error message will be printed + UUID uuid = gen.nextId(null); + assertThat(uuid).isNotNull(); + } + + @Test + public void testInvalidFileName() throws Exception { + UuidV1IdGenerator gen = UuidV1IdGenerator.getInstance("/", null); + UUID uuid = gen.nextId(null); + assertThat(uuid).isNotNull(); + } + + @Test + public void testInvalidStateFile() throws Exception { + writePropertyFile("", "", ""); + UuidV1IdGenerator gen = UuidV1IdGenerator.getInstance(stateFile, null); + UUID uuid = gen.nextId(null); + assertThat(uuid).isNotNull(); + } + + @Test + public void testMacChange() throws Exception { + writePropertyFile("01-02-03-04-05", "1234", "1234"); + UuidV1IdGenerator gen = UuidV1IdGenerator.getInstance(stateFile, null); + UUID uuid = gen.nextId(null); + assertThat(uuid).isNotNull(); + // MAC must be updated with HW/ID + assertThat(readPropertyFile()).doesNotContainEntry("nodeId", "01-02-03-04-05"); + } + /** * Test takes ~0.3 sec */ @Test public void testUuidV1SingleThread() throws Exception { - testGenerator(1, 500_000, UuidV1IdGenerator.getInstance("ebean-test-uuid.state")); + testGenerator(1, 500_000, UuidV1IdGenerator.getInstance(stateFile, null)); } /** @@ -65,7 +194,7 @@ public class TestUuidGenerator { */ @Test public void testUuidV1MultiThread() throws Exception { - testGenerator(10, 50_000, UuidV1IdGenerator.getInstance("ebean-test-uuid.state")); + testGenerator(10, 50_000, UuidV1IdGenerator.getInstance(stateFile, null)); } /** From 2e1a6eec047e324d40356c1b50d9542dfeeaaa46 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 26 Jan 2022 16:49:12 +0100 Subject: [PATCH 4/9] DB2: Fix: DataBind/Read of native LocalDates --- .../config/dbplatform/DatabasePlatform.java | 14 +++++++ .../config/dbplatform/db2/DB2Platform.java | 1 + .../server/type/DefaultTypeManager.java | 12 +++++- .../server/type/ScalarTypeJodaLocalDate.java | 17 +------- .../type/ScalarTypeJodaLocalDateNative.java | 37 ++++++++++++++++++ .../server/type/ScalarTypeLocalDate.java | 17 +------- .../type/ScalarTypeLocalDateNative.java | 39 +++++++++++++++++++ 7 files changed, 103 insertions(+), 34 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDateNative.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateNative.java diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java index 8a6d570f6..d46f6dd37 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java @@ -15,6 +15,7 @@ import javax.persistence.PersistenceException; import javax.sql.DataSource; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -54,6 +55,15 @@ public class DatabasePlatform { protected boolean supportsSavepointId = true; protected boolean useMigrationStoredProcedures = false; + + /** + * Can we use native java time API objects in + * {@link ResultSet#getObject(int, Class)} and + * {@link PreparedStatement#setObject(int, Object)}. + * + * Not all drivers (DB2 e.g.) will support this. + */ + protected boolean supportsNativeJavaTime = true; /** * The behaviour used when ending a read only transaction at read committed isolation level. @@ -827,4 +837,8 @@ public class DatabasePlatform { protected void escapeLikeCharacter(char ch, StringBuilder sb) { sb.append(likeEscapeChar).append(ch); } + + public boolean supportsNativeJavaTime() { + return supportsNativeJavaTime; + } } diff --git a/ebean-api/src/main/java/io/ebean/config/dbplatform/db2/DB2Platform.java b/ebean-api/src/main/java/io/ebean/config/dbplatform/db2/DB2Platform.java index 61d14f4a6..5f2e44df9 100644 --- a/ebean-api/src/main/java/io/ebean/config/dbplatform/db2/DB2Platform.java +++ b/ebean-api/src/main/java/io/ebean/config/dbplatform/db2/DB2Platform.java @@ -24,6 +24,7 @@ public class DB2Platform extends DatabasePlatform { // TOOD: Check if we need to introduce a new platform (DB2_LUW_11 ?) this.maxTableNameLength = 18; this.maxConstraintNameLength = 18; + this.supportsNativeJavaTime = false; this.truncateTable = "truncate table %s reuse storage ignore delete triggers immediate"; this.sqlLimiter = new Db2SqlLimiter(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index 81d6a1ea2..94ebb0572 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -755,7 +755,11 @@ public final class DefaultTypeManager implements TypeManager { typeMap.put(java.nio.file.Path.class, new ScalarTypePath()); addType(java.time.Period.class, new ScalarTypePeriod()); - addType(java.time.LocalDate.class, new ScalarTypeLocalDate(jsonDate)); + if (config.getDatabasePlatform().supportsNativeJavaTime()) { + addType(java.time.LocalDate.class, new ScalarTypeLocalDateNative(jsonDate)); + } else { + addType(java.time.LocalDate.class, new ScalarTypeLocalDate(jsonDate)); + } addType(java.time.LocalDateTime.class, new ScalarTypeLocalDateTime(jsonDateTime)); addType(OffsetDateTime.class, new ScalarTypeOffsetDateTime(jsonDateTime, zoneId)); addType(ZonedDateTime.class, new ScalarTypeZonedDateTime(jsonDateTime, zoneId)); @@ -795,7 +799,11 @@ public final class DefaultTypeManager implements TypeManager { log.debug("Registering Joda data types"); addType(LocalDateTime.class, new ScalarTypeJodaLocalDateTime(jsonDateTime)); addType(DateTime.class, new ScalarTypeJodaDateTime(jsonDateTime)); - addType(LocalDate.class, new ScalarTypeJodaLocalDate(jsonDate)); + if (config.getDatabasePlatform().supportsNativeJavaTime()) { + addType(LocalDate.class, new ScalarTypeJodaLocalDateNative(jsonDate)); + } else { + addType(LocalDate.class, new ScalarTypeJodaLocalDate(jsonDate)); + } addType(org.joda.time.DateMidnight.class, new ScalarTypeJodaDateMidnight(jsonDate)); addType(org.joda.time.Period.class, new ScalarTypeJodaPeriod()); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDate.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDate.java index eb10a80bb..dd84612a8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDate.java @@ -14,7 +14,7 @@ import java.sql.Types; /** * ScalarType for Joda LocalDate. This maps to a JDBC Date. */ -final class ScalarTypeJodaLocalDate extends ScalarTypeBaseDate { +class ScalarTypeJodaLocalDate extends ScalarTypeBaseDate { ScalarTypeJodaLocalDate(JsonConfig.Date mode) { super(mode, LocalDate.class, false, Types.DATE); @@ -61,19 +61,4 @@ final class ScalarTypeJodaLocalDate extends ScalarTypeBaseDate { } return (LocalDate) value; } - - @Override - public void bind(DataBinder binder, LocalDate value) throws SQLException { - if (value == null) { - binder.setNull(Types.DATE); - } else { - binder.setObject(java.time.LocalDate.of(value.getYear(), value.getMonthOfYear(), value.getDayOfMonth())); - } - } - - @Override - public LocalDate read(DataReader reader) throws SQLException { - java.time.LocalDate jtDate = reader.getObject(java.time.LocalDate.class); - return jtDate == null ? null : new org.joda.time.LocalDate(jtDate.getYear(), jtDate.getMonthValue(), jtDate.getDayOfMonth()); - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDateNative.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDateNative.java new file mode 100644 index 000000000..d591fd300 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDateNative.java @@ -0,0 +1,37 @@ +package io.ebeaninternal.server.type; + +import io.ebean.config.JsonConfig; +import io.ebean.core.type.DataBinder; +import io.ebean.core.type.DataReader; +import io.ebeaninternal.server.core.BasicTypeConverter; +import org.joda.time.DateTimeZone; +import org.joda.time.LocalDate; + +import java.sql.Date; +import java.sql.SQLException; +import java.sql.Types; + +/** + * ScalarType for Joda LocalDate. This maps to a LocalDate. Not all drivers/platforms may support this. + */ +final class ScalarTypeJodaLocalDateNative extends ScalarTypeJodaLocalDate{ + + ScalarTypeJodaLocalDateNative(JsonConfig.Date mode) { + super(mode); + } + + @Override + public void bind(DataBinder binder, LocalDate value) throws SQLException { + if (value == null) { + binder.setNull(Types.DATE); + } else { + binder.setObject(java.time.LocalDate.of(value.getYear(), value.getMonthOfYear(), value.getDayOfMonth())); + } + } + + @Override + public LocalDate read(DataReader reader) throws SQLException { + java.time.LocalDate jtDate = reader.getObject(java.time.LocalDate.class); + return jtDate == null ? null : new org.joda.time.LocalDate(jtDate.getYear(), jtDate.getMonthValue(), jtDate.getDayOfMonth()); + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDate.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDate.java index 1e298ecac..1cfcda150 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDate.java @@ -16,7 +16,7 @@ import java.time.ZonedDateTime; /** * ScalarType for java.time.LocalDate. This maps to a JDBC Date. */ -final class ScalarTypeLocalDate extends ScalarTypeBaseDate { +class ScalarTypeLocalDate extends ScalarTypeBaseDate { ScalarTypeLocalDate(JsonConfig.Date mode) { super(mode, LocalDate.class, false, Types.DATE); @@ -63,19 +63,4 @@ final class ScalarTypeLocalDate extends ScalarTypeBaseDate { } return (LocalDate) value; } - - @Override - public void bind(DataBinder binder, LocalDate value) throws SQLException { - if (value == null) { - binder.setNull(Types.DATE); - } else { - binder.setObject(value); - } - } - - @Override - public LocalDate read(DataReader reader) throws SQLException { - return reader.getObject(LocalDate.class); - } - } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateNative.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateNative.java new file mode 100644 index 000000000..67da39e1e --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateNative.java @@ -0,0 +1,39 @@ +package io.ebeaninternal.server.type; + +import io.ebean.config.JsonConfig; +import io.ebean.core.type.DataBinder; +import io.ebean.core.type.DataReader; +import io.ebeaninternal.server.core.BasicTypeConverter; + +import java.sql.Date; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +/** + * ScalarType for java.time.LocalDate. This maps to a LocalDate. Not all drivers/platforms may support this. + */ +final class ScalarTypeLocalDateNative extends ScalarTypeLocalDate { + + ScalarTypeLocalDateNative(JsonConfig.Date mode) { + super(mode); + } + + @Override + public void bind(DataBinder binder, LocalDate value) throws SQLException { + if (value == null) { + binder.setNull(Types.DATE); + } else { + binder.setObject(value); + } + } + + @Override + public LocalDate read(DataReader reader) throws SQLException { + return reader.getObject(LocalDate.class); + } + +} From 19820273004e24f749fb7f0e5d2c013be181451b Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 26 Jan 2022 16:55:22 +0100 Subject: [PATCH 5/9] Organized imports --- .../server/type/ScalarTypeJodaLocalDate.java | 12 +++++------- .../server/type/ScalarTypeJodaLocalDateNative.java | 12 +++++------- .../server/type/ScalarTypeLocalDate.java | 9 +++------ .../server/type/ScalarTypeLocalDateNative.java | 13 ++++--------- 4 files changed, 17 insertions(+), 29 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDate.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDate.java index dd84612a8..f5f85a4db 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDate.java @@ -1,15 +1,13 @@ package io.ebeaninternal.server.type; -import io.ebean.config.JsonConfig; -import io.ebean.core.type.DataBinder; -import io.ebean.core.type.DataReader; -import io.ebeaninternal.server.core.BasicTypeConverter; +import java.sql.Date; +import java.sql.Types; + import org.joda.time.DateTimeZone; import org.joda.time.LocalDate; -import java.sql.Date; -import java.sql.SQLException; -import java.sql.Types; +import io.ebean.config.JsonConfig; +import io.ebeaninternal.server.core.BasicTypeConverter; /** * ScalarType for Joda LocalDate. This maps to a JDBC Date. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDateNative.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDateNative.java index d591fd300..328cccabb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDateNative.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeJodaLocalDateNative.java @@ -1,15 +1,13 @@ package io.ebeaninternal.server.type; +import java.sql.SQLException; +import java.sql.Types; + +import org.joda.time.LocalDate; + import io.ebean.config.JsonConfig; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; -import io.ebeaninternal.server.core.BasicTypeConverter; -import org.joda.time.DateTimeZone; -import org.joda.time.LocalDate; - -import java.sql.Date; -import java.sql.SQLException; -import java.sql.Types; /** * ScalarType for Joda LocalDate. This maps to a LocalDate. Not all drivers/platforms may support this. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDate.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDate.java index 1cfcda150..c3e26ce56 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDate.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDate.java @@ -1,18 +1,15 @@ package io.ebeaninternal.server.type; -import io.ebean.config.JsonConfig; -import io.ebean.core.type.DataBinder; -import io.ebean.core.type.DataReader; -import io.ebeaninternal.server.core.BasicTypeConverter; - import java.sql.Date; -import java.sql.SQLException; import java.sql.Timestamp; import java.sql.Types; import java.time.LocalDate; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import io.ebean.config.JsonConfig; +import io.ebeaninternal.server.core.BasicTypeConverter; + /** * ScalarType for java.time.LocalDate. This maps to a JDBC Date. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateNative.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateNative.java index 67da39e1e..96d28a90d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateNative.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/ScalarTypeLocalDateNative.java @@ -1,17 +1,12 @@ package io.ebeaninternal.server.type; +import java.sql.SQLException; +import java.sql.Types; +import java.time.LocalDate; + import io.ebean.config.JsonConfig; import io.ebean.core.type.DataBinder; import io.ebean.core.type.DataReader; -import io.ebeaninternal.server.core.BasicTypeConverter; - -import java.sql.Date; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.sql.Types; -import java.time.LocalDate; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; /** * ScalarType for java.time.LocalDate. This maps to a LocalDate. Not all drivers/platforms may support this. From 7278db56ea8cda9dcf4756820b4cfc84ecbee8d7 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 27 Jan 2022 09:12:57 +1300 Subject: [PATCH 6/9] Modify test TestUuidGenerator to allow odd jvm variance in root error message --- .../io/ebeaninternal/server/idgen/TestUuidGenerator.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ebean-test/src/test/java/io/ebeaninternal/server/idgen/TestUuidGenerator.java b/ebean-test/src/test/java/io/ebeaninternal/server/idgen/TestUuidGenerator.java index 9bed5a1d0..f311dff3a 100644 --- a/ebean-test/src/test/java/io/ebeaninternal/server/idgen/TestUuidGenerator.java +++ b/ebean-test/src/test/java/io/ebeaninternal/server/idgen/TestUuidGenerator.java @@ -85,7 +85,7 @@ public class TestUuidGenerator { prop.store(os, "ebean uuid state file"); } } - + private Properties readPropertyFile() throws IOException { Properties prop = new Properties(); try (InputStream is = new FileInputStream(stateFile)) { @@ -108,7 +108,7 @@ public class TestUuidGenerator { assertThat(Long.parseLong(props.getProperty("timeStamp"))) .isCloseTo(uuid.timestamp(), within(2_000_000L)); } - + @Test public void testUuidInvalidMac() throws Exception { assertThatThrownBy(()-> UuidV1IdGenerator.getInstance(stateFile, "01-02-03-04-05")) @@ -117,8 +117,7 @@ public class TestUuidGenerator { assertThatThrownBy(()-> UuidV1IdGenerator.getInstance(stateFile, "01-02-03-04-05-GG")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("01-02-03-04-05-gg is invalid.") - .hasRootCauseInstanceOf(NumberFormatException.class) - .hasRootCauseMessage("For input string: \"gg\""); + .hasRootCauseInstanceOf(NumberFormatException.class); assertThat(stateFile).doesNotExist(); } @@ -170,7 +169,7 @@ public class TestUuidGenerator { UUID uuid = gen.nextId(null); assertThat(uuid).isNotNull(); } - + @Test public void testMacChange() throws Exception { writePropertyFile("01-02-03-04-05", "1234", "1234"); From 404b167bcc46f22e8f451c1b0b4c2b578d0b66f3 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 27 Jan 2022 09:13:20 +1300 Subject: [PATCH 7/9] #2508 - Bump ebean-datasource to 7.5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 66a499e77..20aa39271 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ 1.1 12.13.1 4.2 - 7.3 + 7.5 12.14.1 12.14.1 From b9ba6b2db5c57835172e99e33b2ab2cbc609a81d Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 26 Jan 2022 16:28:30 +0100 Subject: [PATCH 8/9] DB2: Adapted Noemi's patch to new ebean version --- .../server/query/SqlTreeBuilder.java | 6 ++++ .../server/query/SqlTreeNode.java | 2 ++ .../server/query/SqlTreeNodeBean.java | 33 +++++++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 0ee056430..ed687aff8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -53,6 +53,7 @@ public final class SqlTreeBuilder { private final SpiQuery.TemporalMode temporalMode; private SqlTreeNode rootNode; private boolean sqlDistinct; + private final boolean distinctNoLobs; /** * Construct for RawSql query. @@ -65,6 +66,7 @@ public final class SqlTreeBuilder { this.query = null; this.subQuery = false; this.distinctOnPlatform = false; + this.distinctNoLobs = false; this.queryDetail = queryDetail; this.predicates = predicates; this.temporalMode = SpiQuery.TemporalMode.CURRENT; @@ -97,6 +99,7 @@ public final class SqlTreeBuilder { this.predicates = predicates; this.alias = new SqlTreeAlias(request.baseTableAlias(), temporalMode); this.distinctOnPlatform = builder.isPlatformDistinctOn(); + this.distinctNoLobs = builder.isPlatformDistinctNoLobs(); String fromForUpdate = builder.fromForUpdate(query); CQueryHistorySupport historySupport = builder.getHistorySupport(query); CQueryDraftSupport draftSupport = builder.getDraftSupport(query); @@ -266,6 +269,9 @@ public final class SqlTreeBuilder { if (joinList != null) { joinList.add(selectNode); } + if (sqlDistinct && distinctNoLobs) { + selectNode.unselectLobs(); + } return selectNode; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java index 54632d9ca..d4d22908c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java @@ -78,4 +78,6 @@ interface SqlTreeNode { * Create the loader for this node. */ SqlTreeLoad createLoad(); + + default void unselectLobs() {}; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java index 4836fa3fd..6214137c1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -26,8 +26,8 @@ class SqlTreeNodeBean implements SqlTreeNode { /** * Set to true if this is a partial object fetch. */ - final boolean partialObject; - final STreeProperty[] properties; + boolean partialObject; + STreeProperty[] properties; /** * Extra where clause added by Where annotation on associated many. */ @@ -399,4 +399,33 @@ class SqlTreeNodeBean implements SqlTreeNode { } return false; } + + + @Override + public void unselectLobs() { + if (children != null) { + for (SqlTreeNode child : children) { + child.unselectLobs(); + } + } + if (hasLob()) { + List lst = new ArrayList<>(); + for (STreeProperty prop : properties) { + if (!prop.isDbLob()) { + lst.add(prop); + } + } + properties = lst.toArray(new STreeProperty[0]); + partialObject = true; + } + } + + private boolean hasLob() { + for (STreeProperty prop : properties) { + if (prop.isDbLob()) { + return true; + } + } + return false; + } } From 69a753fc4f78958f409e8514dcbe44eabbc866eb Mon Sep 17 00:00:00 2001 From: Noemi Szemenyei Date: Fri, 21 Jan 2022 14:30:06 +0100 Subject: [PATCH 9/9] DB2:Unselect LOBs when making a distinct select --- .../server/deploy/BeanProperty.java | 21 +++++++- .../DynamicPropertyAggregationFormula.java | 5 ++ .../server/query/CQueryBuilder.java | 4 ++ .../server/query/STreeProperty.java | 5 ++ .../server/query/SqlTreeNode.java | 8 ++- .../java/org/tests/json/TestJsonMapBasic.java | 54 +++++++++++++++---- 6 files changed, 85 insertions(+), 12 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index bbee58e60..72379b232 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -1160,7 +1160,26 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { public boolean isLob() { return lob; } - + + /** + * Returns true if this isLob() or the type will effectively map to a lob. + */ + public boolean isDbLob() { + if (lob) { + return true; + } + switch (dbType) { + case DbPlatformType.JSON: + case DbPlatformType.JSONB: + return dbLength == 0; // must be analog to DbPlatformTypeMapping.lookup + case DbPlatformType.JSONBlob: + case DbPlatformType.JSONClob: + return true; + default: + return false; + } + } + public static boolean isLobType(int type) { switch (type) { case Types.CLOB: diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyAggregationFormula.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyAggregationFormula.java index 970145632..ec1c7035b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyAggregationFormula.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/DynamicPropertyAggregationFormula.java @@ -62,4 +62,9 @@ class DynamicPropertyAggregationFormula extends DynamicPropertyBase { ctx.appendParseSelect(parsedFormula, alias); } + @Override + public boolean isDbLob() { + return false; + } + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java index 1ad0dae58..1d32ce994 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java @@ -760,6 +760,10 @@ final class CQueryBuilder { return dbPlatform.isPlatform(Platform.POSTGRES); } + boolean isPlatformDistinctNoLobs() { + return dbPlatform.isPlatform(Platform.DB2); // CHECKME: Also oracle? + } + /** * Return the 'for update' FROM hint (sql server). */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeProperty.java index 0981d5ac4..c13126858 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeProperty.java @@ -28,6 +28,11 @@ public interface STreeProperty extends ScalarDataReader { * Return true if the property is the Id. */ boolean isId(); + + /** + * Returns true, if this is a lob property from db-perspective. + */ + boolean isDbLob(); /** * Return true if the property is an embedded type. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java index d4d22908c..3d50a9377 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java @@ -78,6 +78,10 @@ interface SqlTreeNode { * Create the loader for this node. */ SqlTreeLoad createLoad(); - - default void unselectLobs() {}; + + /** + * Unselect lobs (for distinct queries on DB2 and Oracle). + */ + default void unselectLobs() { + }; } diff --git a/ebean-test/src/test/java/org/tests/json/TestJsonMapBasic.java b/ebean-test/src/test/java/org/tests/json/TestJsonMapBasic.java index e186de792..1c1cebb43 100644 --- a/ebean-test/src/test/java/org/tests/json/TestJsonMapBasic.java +++ b/ebean-test/src/test/java/org/tests/json/TestJsonMapBasic.java @@ -3,13 +3,18 @@ package org.tests.json; import io.ebean.BaseTestCase; import io.ebean.DB; import io.ebean.Query; +import io.ebean.annotation.ForPlatform; +import io.ebean.annotation.Platform; import io.ebean.test.LoggedSql; import io.ebean.text.json.EJson; + +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.tests.model.json.EBasicJsonMap; import org.tests.model.json.EBasicJsonMapDetail; import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -18,15 +23,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class TestJsonMapBasic extends BaseTestCase { - @Test - public void whereManyPredicate() { + private final EBasicJsonMap bean = new EBasicJsonMap();; - if (!isPostgres()) { - // testing postgres specific select distinct on clause - return; + @AfterEach + void cleanup() { + if (bean != null && bean.getId() != null) { + DB.delete(EBasicJsonMap.class, bean.getId()); } + } + + @Test + @ForPlatform(Platform.POSTGRES) + public void whereManyPredicatePg() { - EBasicJsonMap bean = new EBasicJsonMap(); bean.setName("own1"); bean.getDetails().add(new EBasicJsonMapDetail("detail1")); bean.getDetails().add(new EBasicJsonMapDetail("detail2")); @@ -50,6 +59,36 @@ public class TestJsonMapBasic extends BaseTestCase { assertThat(query2.getGeneratedSql()).contains("select distinct on (t0.id) "); } + @Test + @ForPlatform(Platform.DB2) + public void whereManyPredicateDb2() { + + Map m1 = new HashMap<>(); + m1.put("foo", "bar"); + bean.setContent(m1); + bean.setName("own1"); + bean.getDetails().add(new EBasicJsonMapDetail("db2-detail1")); + bean.getDetails().add(new EBasicJsonMapDetail("db2-detail2")); + + DB.save(bean); + Query query1 = DB.find(EBasicJsonMap.class).select("*").fetch("details").where() + .startsWith("details.name", "db2-detail").query(); + + List lst = query1.findList(); + + assertThat(query1.getGeneratedSql()).contains("select distinct t0.id, t0.name, t0.version,") + .doesNotContain("content"); + assertThat(lst).hasSize(1); + assertThat(lst.get(0).getContent()).containsEntry("foo", "bar"); + + Query query2 = DB.find(EBasicJsonMap.class).where().startsWith("details.name", "db2-detail") + .query(); + query2.findList(); + + assertThat(query2.getGeneratedSql()).contains("select distinct t0.id, t0.name, t0.version from"); + + } + @Test public void testInsertUpdateDelete() throws IOException { @@ -58,7 +97,6 @@ public class TestJsonMapBasic extends BaseTestCase { Map content = EJson.parseObject(s0); - EBasicJsonMap bean = new EBasicJsonMap(); bean.setName("one"); bean.setContent(content); @@ -106,7 +144,6 @@ public class TestJsonMapBasic extends BaseTestCase { String s0 = "{\"docId\":22,\"contentId\":\"initialDoc\"}"; Map content = EJson.parseObject(s0); - EBasicJsonMap bean = new EBasicJsonMap(); bean.setName("one"); bean.setContent(content); @@ -133,6 +170,5 @@ public class TestJsonMapBasic extends BaseTestCase { assertThat(content2.get("contentId")).isEqualTo("updatedDoc222"); assertThat(content2.get("docId")).isEqualTo(222L); - DB.delete(found); } }