diff --git a/cayenne-server/src/main/java/org/apache/cayenne/DataRow.java b/cayenne-server/src/main/java/org/apache/cayenne/DataRow.java index e6181828b6..d36bc9fd12 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/DataRow.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/DataRow.java @@ -19,8 +19,10 @@ package org.apache.cayenne; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import org.apache.cayenne.map.DbRelationship; @@ -149,4 +151,52 @@ public String getEntityName() { public void setEntityName(String entityName) { this.entityName = entityName; } + + public int hashCode() { + int h = 0; + for (Entry curr : entrySet()) { + if (curr.getValue() instanceof byte[]) { + h += Objects.hashCode(curr.getKey()) ^ Arrays.hashCode((byte[]) curr.getValue()); + } else { + h += curr.hashCode(); + } + } + return h; + } + + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof Map)) + return false; + Map m = (Map) o; + if (m.size() != size()) + return false; + + try { + for (Entry e : entrySet()) { + String key = e.getKey(); + Object value = e.getValue(); + if (value == null) { + if (!(m.get(key) == null && m.containsKey(key))) + return false; + } else { + if (e.getValue() instanceof byte[] && m.get(key) instanceof byte[]) { + if (!Arrays.equals((byte[]) value, (byte[]) m.get(key))) { + return false; + } + } else { + if (!value.equals(m.get(key))) + return false; + } + } + + } + } catch (ClassCastException | NullPointerException unused) { + return false; + } + + return true; + } } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java index e6e5dab499..17e0dd46dd 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/DistinctResultIterator.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; +import java.util.Arrays; /** * A ResultIterator that does in-memory filtering of rows to return only @@ -144,13 +145,20 @@ private void checkNextUniqueRow() { while (delegate.hasNextRow()) { T next = delegate.nextRow(); - if (fetchedIds.add(next)) { + if (isUnique(next)) { this.nextDataRow = next; break; } } } + private boolean isUnique(T next) { + if(next instanceof Object[]){ + return fetchedIds.add(new ObjectArrayWrapper((Object[]) next)); + } + return fetchedIds.add(next); + } + private void checkNextRowWithUniqueId() { nextDataRow = null; while (delegate.hasNextRow()) { @@ -165,11 +173,37 @@ private void checkNextRowWithUniqueId() { id.put(pk.getName(), ((DataRow)next).get(pk.getName())); } - if (fetchedIds.add(id)) { + if (isUnique((T) id)) { this.nextDataRow = next; break; } } } + class ObjectArrayWrapper{ + private Object[] objects; + + ObjectArrayWrapper(Object[] objects){ + this.objects = objects; + } + + Object[] getObjects(){ + return objects; + } + + public int hashCode(){ + return Arrays.deepHashCode(objects); + } + + public boolean equals(Object o){ + if (o == this) + return true; + + if (!(o instanceof DistinctResultIterator.ObjectArrayWrapper)) + return false; + + return Arrays.deepEquals(this.getObjects(),((ObjectArrayWrapper)o).getObjects()); + } + } + } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java index adcf71a5e9..0af3ae6701 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java @@ -455,7 +455,7 @@ List appendOverriddenColumns(List column int type = getJdbcTypeForProperty(property); ColumnDescriptor descriptor; if(property.getType() != null) { - descriptor = new ColumnDescriptor(builder.toString(), type, property.getType().getName()); + descriptor = new ColumnDescriptor(builder.toString(), type, property.getType().getCanonicalName()); } else { descriptor = new ColumnDescriptor(builder.toString(), type); } diff --git a/cayenne-server/src/test/java/org/apache/cayenne/DataRowTest.java b/cayenne-server/src/test/java/org/apache/cayenne/DataRowTest.java index e145cd24f8..4b092f26fd 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/DataRowTest.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/DataRowTest.java @@ -22,6 +22,8 @@ import org.junit.Test; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; public class DataRowTest { @@ -35,4 +37,54 @@ public void testVersion() throws Exception { assertFalse(s3.getVersion() == s1.getVersion()); } + @Test + public void testEquals(){ + DataRow d1 = new DataRow(1); + d1.put("FIELD", "test".getBytes()); + + assertTrue(d1.equals(d1)); + + DataRow d2 = new DataRow(1); + d2.put("FIELD", "test".getBytes()); + + assertTrue(d1.equals(d2)); + assertTrue(d2.equals(d1)); + + DataRow d3 = new DataRow(1); + d3.put("FIELD", "test".getBytes()); + + assertTrue(d2.equals(d3)); + assertTrue(d1.equals(d3)); + + assertFalse(d1.equals(null)); + + DataRow d4 = new DataRow(1); + d4.put("FIELD1", "test".getBytes()); + + for(int i = 0; i < 5; i++) { + assertFalse(d3.equals(d4)); + } + + DataRow d5 = new DataRow(1); + d5.put("FIELD", "test1".getBytes()); + + DataRow d6 = new DataRow(1); + d6.put("FIELD", "test".getBytes()); + + assertFalse(d5.equals(d6)); + } + + @Test + public void testHashCode(){ + DataRow d1 = new DataRow(1); + d1.put("FIELD", "test".getBytes()); + + assertEquals(d1.hashCode(), d1.hashCode()); + + DataRow d2 = new DataRow(1); + d2.put("FIELD", "test".getBytes()); + + assertTrue(d1.equals(d2)); + assertEquals(d1.hashCode(), d2.hashCode()); + } } diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SelectActionIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SelectActionIT.java index b58f2facac..5be9a379a9 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SelectActionIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/SelectActionIT.java @@ -19,14 +19,23 @@ package org.apache.cayenne.access.jdbc; import org.apache.cayenne.DataRow; +import org.apache.cayenne.ObjectContext; import org.apache.cayenne.access.DataContext; +import org.apache.cayenne.configuration.server.ServerRuntime; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.CapsStrategy; import org.apache.cayenne.query.ObjectSelect; +import org.apache.cayenne.query.SQLTemplate; import org.apache.cayenne.query.SelectQuery; +import org.apache.cayenne.query.EJBQLQuery; +import org.apache.cayenne.test.jdbc.DBHelper; +import org.apache.cayenne.test.jdbc.TableHelper; import org.apache.cayenne.testdo.lob.ClobTestEntity; import org.apache.cayenne.testdo.lob.ClobTestRelation; +import org.apache.cayenne.testdo.lob.DistEntity; +import org.apache.cayenne.testdo.lob.DistEntityRel; import org.apache.cayenne.unit.UnitDbAdapter; import org.apache.cayenne.unit.di.server.CayenneProjects; import org.apache.cayenne.unit.di.server.ServerCase; @@ -44,6 +53,12 @@ public class SelectActionIT extends ServerCase { @Inject private DataContext context; + @Inject + private ServerRuntime serverRuntime; + + @Inject + private DBHelper dbHelper; + @Inject private UnitDbAdapter accessStackAdapter; @@ -80,6 +95,67 @@ public void testColumnSelect_DistinctResultIterator() throws Exception { } } + @Test + public void testAddingDistinctToQuery() throws Exception{ + if (accessStackAdapter.supportsLobs()){ + + TableHelper tDistEntity = new TableHelper(dbHelper, "DIST_ENTITY"); + tDistEntity.setColumns("ID", "NAME", "FIELD"); + + for (int i = 1; i <= 3; i++) { + tDistEntity.insert(i, "dist_entity" + i, ("dist_entity" + i).getBytes()); + } + + TableHelper tDistEntityRel = new TableHelper(dbHelper, "DIST_ENTITY_REL"); + tDistEntityRel.setColumns("ID", "DIST_ID", "NUM"); + + for (int i = 1; i <= 10; i++) { + if(i < 5) { + tDistEntityRel.insert(i, 1, i); + } + else{ + tDistEntityRel.insert(i, 3, i); + } + } + + ObjectContext objectContext = serverRuntime.newContext(); + + SQLTemplate select = new SQLTemplate(DistEntity.class, "SELECT t0.FIELD, t0.NAME, t0.ID FROM DIST_ENTITY t0 JOIN DIST_ENTITY_REL t1 ON (t0.ID = t1.DIST_ID) WHERE (t1.NUM > 0) AND (t0.NAME LIKE 'dist_entity1')"); + select.setColumnNamesCapitalization(CapsStrategy.UPPER); + List list1 = objectContext.performQuery(select); + + assertEquals(4, list1.size()); + + List list2 = ObjectSelect.query(DistEntity.class) + .where(DistEntity.DIST_REL.dot(DistEntityRel.NUM).gt(0)) + .and(DistEntity.NAME.like("dist_entity1")) + .select(objectContext); + + assertEquals(1,list2.size()); + + EJBQLQuery query1 = new EJBQLQuery("select d FROM DistEntity d JOIN d.distRel r where d.name='dist_entity1' and r.num>0"); + List list3 = context.performQuery(query1); + + assertEquals(4,list3.size()); + + List list4 = ObjectSelect + .columnQuery(DistEntity.class, DistEntity.NAME) + .where(DistEntity.DIST_REL.dot(DistEntityRel.NUM).gt(0)) + .and(DistEntity.NAME.eq("dist_entity1")) + .select(context); + + assertEquals(1,list4.size()); + + List list5 = ObjectSelect + .columnQuery(DistEntity.class, DistEntity.NAME, DistEntity.FIELD) + .where(DistEntity.DIST_REL.dot(DistEntityRel.NUM).gt(0)) + .and(DistEntity.NAME.eq("dist_entity1")) + .select(context); + + assertEquals(1,list5.size()); + } + } + protected void insertClobDb() { ClobTestEntity obj; for (int i = 0; i < 80; i++) { diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/DistEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/DistEntity.java new file mode 100644 index 0000000000..428643b908 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/DistEntity.java @@ -0,0 +1,9 @@ +package org.apache.cayenne.testdo.lob; + +import org.apache.cayenne.testdo.lob.auto._DistEntity; + +public class DistEntity extends _DistEntity { + + private static final long serialVersionUID = 1L; + +} diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/DistEntityRel.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/DistEntityRel.java new file mode 100644 index 0000000000..de84a8b3a7 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/DistEntityRel.java @@ -0,0 +1,9 @@ +package org.apache.cayenne.testdo.lob; + +import org.apache.cayenne.testdo.lob.auto._DistEntityRel; + +public class DistEntityRel extends _DistEntityRel { + + private static final long serialVersionUID = 1L; + +} diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/auto/_DistEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/auto/_DistEntity.java new file mode 100644 index 0000000000..1d82e2d4a6 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/auto/_DistEntity.java @@ -0,0 +1,129 @@ +package org.apache.cayenne.testdo.lob.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.List; + +import org.apache.cayenne.CayenneDataObject; +import org.apache.cayenne.exp.Property; +import org.apache.cayenne.testdo.lob.DistEntityRel; + +/** + * Class _DistEntity was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _DistEntity extends CayenneDataObject { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "ID"; + + public static final Property FIELD = Property.create("field", byte[].class); + public static final Property NAME = Property.create("name", String.class); + public static final Property> DIST_REL = Property.create("distRel", List.class); + + protected byte[] field; + protected String name; + + protected Object distRel; + + public void setField(byte[] field) { + beforePropertyWrite("field", this.field, field); + this.field = field; + } + + public byte[] getField() { + beforePropertyRead("field"); + return this.field; + } + + public void setName(String name) { + beforePropertyWrite("name", this.name, name); + this.name = name; + } + + public String getName() { + beforePropertyRead("name"); + return this.name; + } + + public void addToDistRel(DistEntityRel obj) { + addToManyTarget("distRel", obj, true); + } + + public void removeFromDistRel(DistEntityRel obj) { + removeToManyTarget("distRel", obj, true); + } + + @SuppressWarnings("unchecked") + public List getDistRel() { + return (List)readProperty("distRel"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "field": + return this.field; + case "name": + return this.name; + case "distRel": + return this.distRel; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "field": + this.field = (byte[])val; + break; + case "name": + this.name = (String)val; + break; + case "distRel": + this.distRel = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.field); + out.writeObject(this.name); + out.writeObject(this.distRel); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.field = (byte[])in.readObject(); + this.name = (String)in.readObject(); + this.distRel = in.readObject(); + } + +} diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/auto/_DistEntityRel.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/auto/_DistEntityRel.java new file mode 100644 index 0000000000..d12e88f12e --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lob/auto/_DistEntityRel.java @@ -0,0 +1,107 @@ +package org.apache.cayenne.testdo.lob.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.CayenneDataObject; +import org.apache.cayenne.exp.Property; +import org.apache.cayenne.testdo.lob.DistEntity; + +/** + * Class _DistEntityRel was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _DistEntityRel extends CayenneDataObject { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "ID"; + + public static final Property NUM = Property.create("num", Integer.class); + public static final Property DIST = Property.create("dist", DistEntity.class); + + protected Integer num; + + protected Object dist; + + public void setNum(int num) { + beforePropertyWrite("num", this.num, num); + this.num = num; + } + + public int getNum() { + beforePropertyRead("num"); + if(this.num == null) { + return 0; + } + return this.num; + } + + public void setDist(DistEntity dist) { + setToOneTarget("dist", dist, true); + } + + public DistEntity getDist() { + return (DistEntity)readProperty("dist"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "num": + return this.num; + case "dist": + return this.dist; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "num": + this.num = (Integer)val; + break; + case "dist": + this.dist = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.num); + out.writeObject(this.dist); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.num = (Integer)in.readObject(); + this.dist = in.readObject(); + } + +} diff --git a/cayenne-server/src/test/resources/cayenne-lob.xml b/cayenne-server/src/test/resources/cayenne-lob.xml index 630bb15657..c78de0f0ae 100644 --- a/cayenne-server/src/test/resources/cayenne-lob.xml +++ b/cayenne-server/src/test/resources/cayenne-lob.xml @@ -1,5 +1,7 @@ diff --git a/cayenne-server/src/test/resources/lob.map.xml b/cayenne-server/src/test/resources/lob.map.xml index df846e1375..f001af142c 100644 --- a/cayenne-server/src/test/resources/lob.map.xml +++ b/cayenne-server/src/test/resources/lob.map.xml @@ -21,6 +21,16 @@ + + + + + + + + + + @@ -39,6 +49,13 @@ + + + + + + + @@ -48,6 +65,14 @@ + + + + + + + +