diff --git a/pom.xml b/pom.xml index 5b056dc3f..c6f265724 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ io.ebean ebean - 11.22.6-SNAPSHOT + 11.22.7-SNAPSHOT jar ebean diff --git a/src/main/java/io/ebean/OrderBy.java b/src/main/java/io/ebean/OrderBy.java index e9f68f6f5..7ce78f340 100644 --- a/src/main/java/io/ebean/OrderBy.java +++ b/src/main/java/io/ebean/OrderBy.java @@ -1,9 +1,12 @@ package io.ebean; +import io.ebean.util.StringHelper; + import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; /** * Represents an Order By for a Query. @@ -74,6 +77,15 @@ public final class OrderBy implements Serializable { return query; } + /** + * Add a property with ascending order to this OrderBy. + */ + public Query asc(String propertyName, String collation) { + + list.add(new Property(propertyName, true, collation)); + return query; + } + /** * Add a property with descending order to this OrderBy. */ @@ -83,6 +95,16 @@ public final class OrderBy implements Serializable { return query; } + /** + * Add a property with descending order to this OrderBy. + */ + public Query desc(String propertyName, String collation) { + + list.add(new Property(propertyName, false, collation)); + return query; + } + + /** * Return true if the property is known to be contained in the order by clause. */ @@ -231,6 +253,8 @@ public final class OrderBy implements Serializable { private boolean ascending; + private String collation; + private String nulls; private String highLow; @@ -247,17 +271,32 @@ public final class OrderBy implements Serializable { this.highLow = highLow; } + public Property(String property, boolean ascending, String collation) { + this.property = property; + this.ascending = ascending; + this.collation = collation; + } + + public Property(String property, boolean ascending, String collation, String nulls, String highLow) { + this.property = property; + this.ascending = ascending; + this.collation = collation; + this.nulls = nulls; + this.highLow = highLow; + } + /** * Return a copy of this Property with the path trimmed. */ public Property copyWithTrim(String path) { - return new Property(property.substring(path.length() + 1), ascending, nulls, highLow); + return new Property(property.substring(path.length() + 1), ascending, collation, nulls, highLow); } @Override public int hashCode() { int hc = property.hashCode(); hc = hc * 92821 + (ascending ? 0 : 1); + hc = hc * 92821 + (collation == null ? 0 : collation.hashCode()); hc = hc * 92821 + (nulls == null ? 0 : nulls.hashCode()); hc = hc * 92821 + (highLow == null ? 0 : highLow.hashCode()); return hc; @@ -274,8 +313,9 @@ public final class OrderBy implements Serializable { Property e = (Property) obj; if (ascending != e.ascending) return false; if (!property.equals(e.property)) return false; - if (nulls != null ? !nulls.equals(e.nulls) : e.nulls != null) return false; - return highLow != null ? highLow.equals(e.highLow) : e.highLow == null; + if (!Objects.equals(collation, e.collation)) return false; + if (!Objects.equals(nulls, e.nulls)) return false; + return Objects.equals(highLow, e.highLow); } @Override @@ -284,7 +324,7 @@ public final class OrderBy implements Serializable { } public String toStringFormat() { - if (nulls == null) { + if (nulls == null && collation == null) { if (ascending) { return property; } else { @@ -292,11 +332,23 @@ public final class OrderBy implements Serializable { } } else { StringBuilder sb = new StringBuilder(); - sb.append(property); + if (collation != null) { + if (collation.contains("${}")) { + // this is a complex collation, e.g. DB2 - we must replace the property + sb.append(StringHelper.replaceString(collation, "${}", property)); + } else { + sb.append(property); + sb.append(" collate ").append(collation); + } + } else { + sb.append(property); + } if (!ascending) { sb.append(" ").append("desc"); } - sb.append(" ").append(nulls).append(" ").append(highLow); + if (nulls != null) { + sb.append(" ").append(nulls).append(" ").append(highLow); + } return sb.toString(); } } @@ -319,7 +371,7 @@ public final class OrderBy implements Serializable { * Return a copy of this property. */ public Property copy() { - return new Property(property, ascending, nulls, highLow); + return new Property(property, ascending, collation, nulls, highLow); } /** diff --git a/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/src/main/java/io/ebean/bean/EntityBeanIntercept.java index e35384ad4..fcd6bb7ad 100644 --- a/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -71,21 +71,30 @@ public final class EntityBeanIntercept implements Serializable { /** * Used when a bean is partially filled. */ - private final boolean[] loadedProps; - - private boolean fullyLoadedBean; + private static final byte FLAG_LOADED_PROP = 1; /** * Set of changed properties. */ - private boolean[] changedProps; + private static final byte FLAG_CHANGED_PROP = 2; /** * Flags indicating if a property is a dirty embedded bean. Used to distingush * between an embedded bean being completely overwritten and one of its * embedded properties being made dirty. */ - private boolean[] embeddedDirty; + private static final byte FLAG_EMBEDDED_DIRTY = 4; + + /** + * Flags indicating if a property is a dirty embedded bean. Used to distingush + * between an embedded bean being completely overwritten and one of its + * embedded properties being made dirty. + */ + private static final byte FLAG_ORIG_VALUE_SET = 8; + + private final byte[] flags; + + private boolean fullyLoadedBean; private Object[] origValues; @@ -102,7 +111,7 @@ public final class EntityBeanIntercept implements Serializable { */ public EntityBeanIntercept(Object ownerBean) { this.owner = (EntityBean) ownerBean; - this.loadedProps = new boolean[owner._ebean_getPropertyNames().length]; + this.flags = new byte[owner._ebean_getPropertyNames().length]; } /** @@ -213,8 +222,8 @@ public final class EntityBeanIntercept implements Serializable { * Check each property to see if the bean is partially loaded. */ public boolean isPartial() { - for (boolean loadedProp : loadedProps) { - if (!loadedProp) { + for (byte flag : flags) { + if ((flag & FLAG_LOADED_PROP) == 0) { return true; } } @@ -259,10 +268,10 @@ public final class EntityBeanIntercept implements Serializable { * Return true if only the Id property has been loaded. */ public boolean hasIdOnly(int idIndex) { - for (int i = 0; i < loadedProps.length; i++) { + for (int i = 0; i < flags.length; i++) { if (i == idIndex) { - if (!loadedProps[i]) return false; - } else if (loadedProps[i]) { + if ((flags[i] & FLAG_LOADED_PROP) == 0) return false; + } else if ((flags[i] & FLAG_LOADED_PROP) != 0) { return false; } } @@ -284,9 +293,9 @@ public final class EntityBeanIntercept implements Serializable { if (idPos > -1) { // For cases where properties are set on constructor // set every non Id property to unloaded (for lazy loading) - for (int i = 0; i < loadedProps.length; i++) { + for (int i = 0; i < flags.length; i++) { if (i != idPos) { - loadedProps[i] = false; + flags[i] &= ~FLAG_LOADED_PROP; } } } @@ -352,7 +361,9 @@ public final class EntityBeanIntercept implements Serializable { this.owner._ebean_setEmbeddedLoaded(); this.lazyLoadProperty = -1; this.origValues = null; - this.changedProps = null; + for (int i = 0; i < flags.length; i++) { + flags[i] &= ~(FLAG_CHANGED_PROP + FLAG_ORIG_VALUE_SET); + } this.dirty = false; } @@ -475,7 +486,11 @@ public final class EntityBeanIntercept implements Serializable { if (position == -1) { throw new IllegalArgumentException("Property " + propertyName + " not found"); } - loadedProps[position] = loaded; + if (loaded) { + flags[position] |= FLAG_LOADED_PROP; + } else { + flags[position] &= ~FLAG_LOADED_PROP; + } } /** @@ -483,22 +498,22 @@ public final class EntityBeanIntercept implements Serializable { * constructor. */ public void setPropertyUnloaded(int propertyIndex) { - loadedProps[propertyIndex] = false; + flags[propertyIndex] &= ~FLAG_LOADED_PROP; } /** * Set the property to be loaded. */ public void setLoadedProperty(int propertyIndex) { - loadedProps[propertyIndex] = true; + flags[propertyIndex] |= FLAG_LOADED_PROP; } /** * Set all properties to be loaded (post insert). */ public void setLoadedPropertyAll() { - for (int i = 0; i < loadedProps.length; i++) { - loadedProps[i] = true; + for (int i = 0; i < flags.length; i++) { + flags[i] |= FLAG_LOADED_PROP; } } @@ -506,14 +521,14 @@ public final class EntityBeanIntercept implements Serializable { * Return true if the property is loaded. */ public boolean isLoadedProperty(int propertyIndex) { - return loadedProps[propertyIndex]; + return (flags[propertyIndex] & FLAG_LOADED_PROP) != 0; } /** * Return true if the property is considered changed. */ public boolean isChangedProperty(int propertyIndex) { - return (changedProps != null && changedProps[propertyIndex]); + return (flags[propertyIndex] & FLAG_CHANGED_PROP) != 0; } /** @@ -521,8 +536,7 @@ public final class EntityBeanIntercept implements Serializable { * embedded properties is dirty. */ public boolean isDirtyProperty(int propertyIndex) { - return (changedProps != null && changedProps[propertyIndex] - || embeddedDirty != null && embeddedDirty[propertyIndex]); + return (flags[propertyIndex] & (FLAG_CHANGED_PROP + FLAG_EMBEDDED_DIRTY)) != 0; } /** @@ -534,27 +548,22 @@ public final class EntityBeanIntercept implements Serializable { } public void setChangedProperty(int propertyIndex) { - if (changedProps == null) { - changedProps = new boolean[owner._ebean_getPropertyNames().length]; - } - changedProps[propertyIndex] = true; + flags[propertyIndex] |= FLAG_CHANGED_PROP; } /** * Set that an embedded bean has had one of its properties changed. */ private void setEmbeddedPropertyDirty(int propertyIndex) { - if (embeddedDirty == null) { - embeddedDirty = new boolean[owner._ebean_getPropertyNames().length]; - } - embeddedDirty[propertyIndex] = true; + flags[propertyIndex] |= FLAG_EMBEDDED_DIRTY; } private void setOriginalValue(int propertyIndex, Object value) { if (origValues == null) { origValues = new Object[owner._ebean_getPropertyNames().length]; } - if (origValues[propertyIndex] == null) { + if ((flags[propertyIndex] & FLAG_ORIG_VALUE_SET) == 0) { + flags[propertyIndex] |= FLAG_ORIG_VALUE_SET; origValues[propertyIndex] = value; } } @@ -574,13 +583,9 @@ public final class EntityBeanIntercept implements Serializable { */ public void setNewBeanForUpdate() { - if (changedProps == null) { - changedProps = new boolean[owner._ebean_getPropertyNames().length]; - } - - for (int i = 0; i < loadedProps.length; i++) { - if (loadedProps[i]) { - changedProps[i] = true; + for (int i = 0; i < flags.length; i++) { + if ((flags[i] & FLAG_LOADED_PROP) != 0) { + flags[i] |= FLAG_CHANGED_PROP; } } setDirty(true); @@ -594,8 +599,8 @@ public final class EntityBeanIntercept implements Serializable { return null; } Set props = new LinkedHashSet<>(); - for (int i = 0; i < loadedProps.length; i++) { - if (loadedProps[i]) { + for (int i = 0; i < flags.length; i++) { + if ((flags[i] & FLAG_LOADED_PROP) != 0) { props.add(getProperty(i)); } } @@ -609,12 +614,8 @@ public final class EntityBeanIntercept implements Serializable { int len = getPropertyLength(); boolean[] dirties = new boolean[len]; for (int i = 0; i < len; i++) { - if (changedProps != null && changedProps[i]) { - dirties[i] = true; - } else if (embeddedDirty != null && embeddedDirty[i]) { - // an embedded property has been changed - recurse - dirties[i] = true; - } + // this, or an embedded property has been changed - recurse + dirties[i] = (flags[i] & (FLAG_CHANGED_PROP + FLAG_EMBEDDED_DIRTY)) != 0; } return dirties; } @@ -634,11 +635,11 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyNames(Set props, String prefix) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if (changedProps != null && changedProps[i]) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0) { // the property has been changed on this bean String propName = (prefix == null ? getProperty(i) : prefix + getProperty(i)); props.add(propName); - } else if (embeddedDirty != null && embeddedDirty[i]) { + } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { // an embedded property has been changed - recurse EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i); embeddedBean._ebean_getIntercept().addDirtyPropertyNames(props, getProperty(i) + "."); @@ -654,12 +655,12 @@ public final class EntityBeanIntercept implements Serializable { String[] names = owner._ebean_getPropertyNames(); int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if (changedProps != null && changedProps[i]) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0) { // the property has been changed on this bean if (propertyNames.contains(names[i])) { return true; } - } else if (embeddedDirty != null && embeddedDirty[i]) { + } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { if (propertyNames.contains(names[i])) { return true; } @@ -683,15 +684,16 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyValues(Map dirtyValues, String prefix) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if (changedProps != null && changedProps[i]) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0) { // the property has been changed on this bean String propName = (prefix == null ? getProperty(i) : prefix + getProperty(i)); Object newVal = owner._ebean_getField(i); Object oldVal = getOrigValue(i); + if (!areEqual(oldVal, newVal)) { + dirtyValues.put(propName, new ValuePair(newVal, oldVal)); + } - dirtyValues.put(propName, new ValuePair(newVal, oldVal)); - - } else if (embeddedDirty != null && embeddedDirty[i]) { + } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { // an embedded property has been changed - recurse EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i); embeddedBean._ebean_getIntercept().addDirtyPropertyValues(dirtyValues, getProperty(i) + "."); @@ -705,13 +707,15 @@ public final class EntityBeanIntercept implements Serializable { public void addDirtyPropertyValues(BeanDiffVisitor visitor) { int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if (changedProps != null && changedProps[i]) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0) { // the property has been changed on this bean Object newVal = owner._ebean_getField(i); Object oldVal = getOrigValue(i); - visitor.visit(i, newVal, oldVal); + if (!areEqual(oldVal, newVal)) { + visitor.visit(i, newVal, oldVal); + } - } else if (embeddedDirty != null && embeddedDirty[i]) { + } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { // an embedded property has been changed - recurse EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i); visitor.visitPush(i); @@ -739,9 +743,9 @@ public final class EntityBeanIntercept implements Serializable { } int len = getPropertyLength(); for (int i = 0; i < len; i++) { - if (changedProps != null && changedProps[i]) { + if ((flags[i] & FLAG_CHANGED_PROP) != 0) { sb.append(i).append(','); - } else if (embeddedDirty != null && embeddedDirty[i]) { + } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { // an embedded property has been changed - recurse EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i); sb.append(i).append('['); @@ -765,15 +769,12 @@ public final class EntityBeanIntercept implements Serializable { return sb; } - /** - * Return the set of property names for changed properties. - */ - public boolean[] getChanged() { - return changedProps; - } - public boolean[] getLoaded() { - return loadedProps; + boolean[] ret= new boolean[flags.length]; + for (int i = 0; i < ret.length; i++) { + ret[i] = (flags[i] & FLAG_LOADED_PROP) != 0; + } + return ret; } /** @@ -821,7 +822,7 @@ public final class EntityBeanIntercept implements Serializable { */ private void loadBeanInternal(int loadProperty, BeanLoader loader) { - if (loadedProps == null || loadedProps[loadProperty]) { + if ((flags[loadProperty] & FLAG_LOADED_PROP) != 0) { // race condition where multiple threads calling preGetter concurrently return; } @@ -887,7 +888,7 @@ public final class EntityBeanIntercept implements Serializable { * Called when a BeanCollection is initialised automatically. */ public void initialisedMany(int propertyIndex) { - loadedProps[propertyIndex] = true; + flags[propertyIndex] |= FLAG_LOADED_PROP; } private void preGetterCallback(int propertyIndex) { diff --git a/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java index 65bdfff4c..152cd3588 100644 --- a/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/io/ebean/config/dbplatform/DatabasePlatform.java @@ -64,8 +64,6 @@ public class DatabasePlatform { */ protected String closeQuote = "\""; - protected String concatOperator = "||"; - /** * When set to true all db column names and table names use quoted identifiers. */ @@ -455,13 +453,6 @@ public class DatabasePlatform { return openQuote; } - /** - * Return the DB concat operator. - */ - public String getConcatOperator() { - return concatOperator; - } - /** * Return the JDBC type used to store booleans. */ diff --git a/src/main/java/io/ebean/plugin/BeanType.java b/src/main/java/io/ebean/plugin/BeanType.java index 2f00cd0af..bb487de35 100644 --- a/src/main/java/io/ebean/plugin/BeanType.java +++ b/src/main/java/io/ebean/plugin/BeanType.java @@ -9,6 +9,10 @@ import io.ebean.event.BeanQueryAdapter; import io.ebeanservice.docstore.api.mapping.DocumentMapping; import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +import javax.annotation.Nonnull; /** * Information and methods on BeanDescriptors made available to plugins. @@ -18,6 +22,7 @@ public interface BeanType { /** * Return the short name of the bean type. */ + @Nonnull String getName(); /** @@ -28,11 +33,13 @@ public interface BeanType { /** * Return the full name of the bean type. */ + @Nonnull String getFullName(); /** * Return the class type this BeanDescriptor describes. */ + @Nonnull Class getBeanType(); /** @@ -43,6 +50,7 @@ public interface BeanType { /** * Return all the properties for this bean type. */ + @Nonnull Collection allProperties(); /** @@ -197,6 +205,28 @@ public interface BeanType { */ boolean hasInheritance(); + /** + * Return true if this object is the root level object in its entity + * inheritance. + */ + boolean isInheritanceRoot(); + + /** + * Returns all direct children of this beantype + */ + List> getInheritanceChildren(); + + /** + * Returns the parent in inheritance hiearchy + */ + BeanType getInheritanceParent(); + + /** + * Visit all children recursively + * @param visitor + */ + void visitAllInheritanceChildren(Consumer> visitor); + /** * Return the discriminator column. */ diff --git a/src/main/java/io/ebean/plugin/Property.java b/src/main/java/io/ebean/plugin/Property.java index e3e9d2c3e..20d81d7e5 100644 --- a/src/main/java/io/ebean/plugin/Property.java +++ b/src/main/java/io/ebean/plugin/Property.java @@ -1,5 +1,7 @@ package io.ebean.plugin; +import javax.annotation.Nonnull; + /** * Property of a entity bean that can be read. */ @@ -8,8 +10,15 @@ public interface Property { /** * Return the name of the property. */ + @Nonnull String getName(); + /** + * Return the type of the property. + */ + @Nonnull + Class getPropertyType(); + /** * Return the value of the property on the given bean. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 4a0c711d5..f114970f9 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -100,6 +100,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -107,6 +108,8 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.stream.Collectors; /** * Describes Beans including their deployment information. @@ -1133,6 +1136,7 @@ public class BeanDescriptor implements BeanType, STreeType { * Return true if this object is the root level object in its entity * inheritance. */ + @Override public boolean isInheritanceRoot() { return inheritInfo == null || inheritInfo.isRoot(); } @@ -3475,4 +3479,29 @@ public class BeanDescriptor implements BeanType, STreeType { public List getUniqueProps() { return propertiesUnique; } + + @Override + public List> getInheritanceChildren() { + if (hasInheritance()) { + return getInheritInfo().getChildren() + .stream() + .map(InheritInfo::desc) + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + + @Override + public BeanType getInheritanceParent() { + return getInheritInfo() == null ? null : getInheritInfo().getParent().desc(); + } + + @Override + public void visitAllInheritanceChildren(Consumer> visitor) { + if (hasInheritance()) { + getInheritInfo().visitChildren(info -> visitor.accept(info.desc())); + } + } + } diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 7473e01aa..8b0650f54 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -979,6 +979,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { /** * Return the full name of this property. */ + @Override public String getFullBeanName() { return descriptor.getFullName() + "." + name; } @@ -994,6 +995,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { /** * Return the scalarType. */ + @Override @SuppressWarnings(value = "unchecked") public ScalarType getScalarType() { return scalarType; @@ -1369,6 +1371,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty { /** * Return the property type. */ + @Override public Class getPropertyType() { return propertyType; } diff --git a/src/main/java/io/ebeaninternal/server/deploy/DeployParser.java b/src/main/java/io/ebeaninternal/server/deploy/DeployParser.java index a5e71183e..ecbfeb0a8 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/DeployParser.java +++ b/src/main/java/io/ebeaninternal/server/deploy/DeployParser.java @@ -25,6 +25,7 @@ public abstract class DeployParser { protected static final char OPEN_SQUARE_BRACKET = '['; protected static final char CLOSE_SQUARE_BRACKET = ']'; protected static final char DOUBLE_QUOTE = '\"'; + protected static final char BACK_QUOTE = '`'; /** * Used to determine when a column name terminates. @@ -187,10 +188,10 @@ public abstract class DeployParser { wordBuffer.append(ch); return false; } - return Character.isLetterOrDigit(ch) || ch == UNDERSCORE || ch == PERIOD || ch == DOUBLE_QUOTE || ch == CLOSE_SQUARE_BRACKET; + return Character.isLetterOrDigit(ch) || ch == UNDERSCORE || ch == PERIOD || ch == DOUBLE_QUOTE || ch == CLOSE_SQUARE_BRACKET || ch == BACK_QUOTE; } private boolean isWordStart(char ch) { - return Character.isLetter(ch) || ch == UNDERSCORE || ch == DOUBLE_QUOTE || ch == OPEN_SQUARE_BRACKET; + return Character.isLetter(ch) || ch == UNDERSCORE || ch == DOUBLE_QUOTE || ch == OPEN_SQUARE_BRACKET || ch == BACK_QUOTE; } } diff --git a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationBase.java b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationBase.java index f86cac704..3d6e6b7d4 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationBase.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationBase.java @@ -82,12 +82,12 @@ public abstract class AnnotationBase { T a = null; Field field = prop.getField(); if (field != null) { - a = AnnotationUtil.findAnnotation(field, annClass); + a = AnnotationUtil.findAnnotation(field, annClass, platform); } if (a == null) { Method method = prop.getReadMethod(); if (method != null) { - a = AnnotationUtil.findAnnotation(method, annClass); + a = AnnotationUtil.findAnnotation(method, annClass, platform); } } return a; diff --git a/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java b/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java index 91f6742d6..2147af1a2 100644 --- a/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java @@ -92,14 +92,7 @@ class InPairsExpression extends AbstractExpression { return; } - String concat = request.getDbPlatformHandler().getConcatOperator(); - - String concatFormula = "(" + property0 + concat + "'" + separator + "'" + concat + property1; - if (suffix != null && !suffix.isEmpty()) { - concatFormula += concat + "'" + suffix + "'"; - } - concatFormula += ")"; - request.append(concatFormula); + request.append(request.getDbPlatformHandler().concat(property0, separator, property1, suffix)); request.appendInExpression(not, concatBindValues); } diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/BaseDbExpression.java b/src/main/java/io/ebeaninternal/server/expression/platform/BaseDbExpression.java index 584215d4c..40ae04a1d 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/BaseDbExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/BaseDbExpression.java @@ -8,17 +8,6 @@ import io.ebeaninternal.server.expression.BitwiseOp; */ abstract class BaseDbExpression implements DbExpressionHandler { - private final String concatOperator; - - BaseDbExpression(String concatOperator) { - this.concatOperator = concatOperator; - } - - @Override - public String getConcatOperator() { - return concatOperator; - } - @Override public void bitwise(SpiExpressionRequest request, String propName, BitwiseOp operator, long flags, String compare, long match) { @@ -57,4 +46,14 @@ abstract class BaseDbExpression implements DbExpressionHandler { } } + @Override + public String concat(String property0, String separator, String property1, String suffix) { + StringBuilder sb = new StringBuilder(); + sb.append("concat(").append(property0).append(",'").append(separator).append("',").append(property1); + if (suffix != null && !suffix.isEmpty()) { + sb.append(",'").append(suffix).append('\''); + } + sb.append(')'); + return sb.toString(); + } } diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/BasicDbExpression.java b/src/main/java/io/ebeaninternal/server/expression/platform/BasicDbExpression.java index adacca04d..8b5ab8d70 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/BasicDbExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/BasicDbExpression.java @@ -8,10 +8,6 @@ import io.ebeaninternal.server.expression.Op; */ public class BasicDbExpression extends BaseDbExpression { - BasicDbExpression(String concatOperator) { - super(concatOperator); - } - @Override public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) { throw new RuntimeException("JSON expressions only supported on Postgres and Oracle"); diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/DbExpressionHandler.java b/src/main/java/io/ebeaninternal/server/expression/platform/DbExpressionHandler.java index 7cf6d696a..93c9dc18a 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/DbExpressionHandler.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/DbExpressionHandler.java @@ -9,11 +9,6 @@ import io.ebeaninternal.server.expression.Op; */ public interface DbExpressionHandler { - /** - * Return the DB concat operator (Usually SQL standard "||"). - */ - String getConcatOperator(); - /** * Write the db platform specific json expression. */ @@ -33,4 +28,9 @@ public interface DbExpressionHandler { * Add the bitwise expression. */ void bitwise(SpiExpressionRequest request, String propName, BitwiseOp operator, long flags, String compare, long match); + + /** + * Performs a "CONCAT" operation for that platform. + */ + String concat(String property0, String separator, String property1, String suffix); } diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/DbExpressionHandlerFactory.java b/src/main/java/io/ebeaninternal/server/expression/platform/DbExpressionHandlerFactory.java index b7b62eb71..e9696e5b6 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/DbExpressionHandlerFactory.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/DbExpressionHandlerFactory.java @@ -12,20 +12,21 @@ public class DbExpressionHandlerFactory { public static DbExpressionHandler from(DatabasePlatform databasePlatform) { Platform platform = databasePlatform.getPlatform(); - String concatOperator = databasePlatform.getConcatOperator(); switch (platform) { case H2: - return new H2DbExpression(concatOperator); + return new H2DbExpression(); case POSTGRES: - return new PostgresDbExpression(concatOperator); + return new PostgresDbExpression(); case MYSQL: - return new MySqlDbExpression(concatOperator); + return new MySqlDbExpression(); case ORACLE: - return new OracleDbExpression(concatOperator); + return new OracleDbExpression(); + case SQLSERVER16: + case SQLSERVER17: case SQLSERVER: - return new SqlServerDbExpression(concatOperator); + return new SqlServerDbExpression(); default: - return new BasicDbExpression(concatOperator); + return new BasicDbExpression(); } } } diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/H2DbExpression.java b/src/main/java/io/ebeaninternal/server/expression/platform/H2DbExpression.java index 4e0b544f2..fb65c7c03 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/H2DbExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/H2DbExpression.java @@ -8,10 +8,6 @@ import io.ebeaninternal.server.expression.BitwiseOp; */ class H2DbExpression extends BasicDbExpression { - H2DbExpression(String concatOperator) { - super(concatOperator); - } - @Override public void bitwise(SpiExpressionRequest request, String propName, BitwiseOp operator, long flags, String compare, long match) { diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/MySqlDbExpression.java b/src/main/java/io/ebeaninternal/server/expression/platform/MySqlDbExpression.java index e0e04ad1f..dff49e0b2 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/MySqlDbExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/MySqlDbExpression.java @@ -5,8 +5,4 @@ package io.ebeaninternal.server.expression.platform; */ class MySqlDbExpression extends BasicDbExpression { - MySqlDbExpression(String concatOperator) { - super(concatOperator); - } - } diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/OracleDbExpression.java b/src/main/java/io/ebeaninternal/server/expression/platform/OracleDbExpression.java index 4347d2e80..168f01aaf 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/OracleDbExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/OracleDbExpression.java @@ -9,10 +9,6 @@ import io.ebeaninternal.server.expression.Op; */ public class OracleDbExpression extends BaseDbExpression { - OracleDbExpression(String concatOperator) { - super(concatOperator); - } - @Override public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) { diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/PostgresDbExpression.java b/src/main/java/io/ebeaninternal/server/expression/platform/PostgresDbExpression.java index 4af9aa2f7..adac971b5 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/PostgresDbExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/PostgresDbExpression.java @@ -8,10 +8,6 @@ import io.ebeaninternal.server.expression.Op; */ public class PostgresDbExpression extends BaseDbExpression { - PostgresDbExpression(String concatOperator) { - super(concatOperator); - } - @Override public void json(SpiExpressionRequest request, String propName, String path, Op operator, Object value) { @@ -65,4 +61,16 @@ public class PostgresDbExpression extends BaseDbExpression { request.append(" <> 0"); } } + + @Override + public String concat(String property0, String separator, String property1, String suffix) { + StringBuilder sb = new StringBuilder(); + sb.append("(").append(property0).append("||'").append(separator).append("'||").append(property1); + + if (suffix != null && !suffix.isEmpty()) { + sb.append("||'").append(suffix).append('\''); + } + sb.append(')'); + return sb.toString(); + } } diff --git a/src/main/java/io/ebeaninternal/server/expression/platform/SqlServerDbExpression.java b/src/main/java/io/ebeaninternal/server/expression/platform/SqlServerDbExpression.java index dc52066e3..98d76ec43 100644 --- a/src/main/java/io/ebeaninternal/server/expression/platform/SqlServerDbExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/platform/SqlServerDbExpression.java @@ -8,10 +8,6 @@ import io.ebeaninternal.server.expression.Op; */ public class SqlServerDbExpression extends BaseDbExpression { - SqlServerDbExpression(String concatOperator) { - super(concatOperator); - } - @Override public void json(final SpiExpressionRequest request, final String propName, final String path, final Op operator, final Object value) { diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java index cd1dc413c..8fb718024 100644 --- a/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/io/ebeaninternal/server/query/CQueryBuilder.java @@ -417,7 +417,9 @@ class CQueryBuilder { BeanDescriptor desc = request.getBeanDescriptor(); try { - PreparedStatement statement = connection.prepareStatement(sql); + // For SqlServer we need either "selectMethod=cursor" in the connection string or fetch explicitly a cursorable + // statement here by specifying ResultSet.CONCUR_UPDATABLE + PreparedStatement statement = connection.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); predicates.bind(statement, connection); ResultSet resultSet = statement.executeQuery(); diff --git a/src/test/java/io/ebeaninternal/server/deploy/DeployPropertyParserTest.java b/src/test/java/io/ebeaninternal/server/deploy/DeployPropertyParserTest.java index 1c387aa49..bb7d4e20a 100644 --- a/src/test/java/io/ebeaninternal/server/deploy/DeployPropertyParserTest.java +++ b/src/test/java/io/ebeaninternal/server/deploy/DeployPropertyParserTest.java @@ -1,6 +1,8 @@ package io.ebeaninternal.server.deploy; import io.ebean.BaseTestCase; +import io.ebean.annotation.ForPlatform; +import io.ebean.annotation.Platform; import org.junit.Test; import org.tests.model.basic.Address; import org.tests.model.basic.BWithQIdent; @@ -53,11 +55,30 @@ public class DeployPropertyParserTest extends BaseTestCase { } @Test - public void withQuote_when_match() { - assertThat(withQuoteParser().parse("name like ?")).isEqualTo("${}\"Name\" like ?"); + public void withExplicitQuote_all_platforms() { + assertThat(withQuoteParser().parse("t0.`CODE` like ?")).isEqualTo("t0.`CODE` like ?"); + assertThat(withQuoteParser().parse("t0.[CODE] like ?")).isEqualTo("t0.[CODE] like ?"); assertThat(withQuoteParser().parse("t0.\"CODE\" like ?")).isEqualTo("t0.\"CODE\" like ?"); } + @Test + @ForPlatform(value = {Platform.H2, Platform.POSTGRES}) + public void withQuote_when_match_h2() { + assertThat(withQuoteParser().parse("name like ?")).isEqualTo("${}\"Name\" like ?"); + } + + @Test + @ForPlatform(value = Platform.SQLSERVER) + public void withQuote_when_match_sqlserver() { + assertThat(withQuoteParser().parse("name like ?")).isEqualTo("${}[Name] like ?"); + } + + @Test + @ForPlatform(value = Platform.MYSQL) + public void withQuote_when_match_mysql() { + assertThat(withQuoteParser().parse("name like ?")).isEqualTo("${}`Name` like ?"); + } + @Test public void unknown_path() { assertThat(parser().parse(" foo ")).isEqualTo(" foo "); diff --git a/src/test/java/org/tests/changelog/TestChangeLog.java b/src/test/java/org/tests/changelog/TestChangeLog.java index d253c9173..9b906a8fd 100644 --- a/src/test/java/org/tests/changelog/TestChangeLog.java +++ b/src/test/java/org/tests/changelog/TestChangeLog.java @@ -5,11 +5,13 @@ import io.ebean.EbeanServerFactory; import io.ebean.annotation.ChangeLog; import io.ebean.config.ServerConfig; import io.ebean.event.BeanPersistRequest; +import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeLogFilter; import io.ebean.event.changelog.ChangeLogListener; import io.ebean.event.changelog.ChangeLogPrepare; import io.ebean.event.changelog.ChangeLogRegister; import io.ebean.event.changelog.ChangeSet; +import io.ebean.event.changelog.ChangeType; import io.ebeaninternal.api.SpiEbeanServer; import org.tests.model.basic.EBasicChangeLog; import com.fasterxml.jackson.annotation.JsonInclude; @@ -18,8 +20,12 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; +import java.util.ArrayList; +import java.util.List; + public class TestChangeLog extends BaseTestCase { TDChangeLogPrepare changeLogPrepare = new TDChangeLogPrepare(); @@ -47,14 +53,77 @@ public class TestChangeLog extends BaseTestCase { bean.setName("logBean"); bean.setShortDescription("hello"); server.save(bean); + BeanChange change = changeLogListener.changes.getChanges().get(0); + + assertThat(change.getEvent()).isEqualTo(ChangeType.INSERT); + assertThat(change.getData()) + .contains("\"name\":\"logBean\"") + .contains("\"shortDescription\":\"hello\""); + bean.setName("ChangedName"); server.save(bean); + change = changeLogListener.changes.getChanges().get(0); + + assertThat(change.getEvent()).isEqualTo(ChangeType.UPDATE); + assertThat(change.getOldData()).contains("\"name\":\"logBean\""); + assertThat(change.getData()) .contains("\"name\":\"ChangedName\""); + + server.delete(bean); + change = changeLogListener.changes.getChanges().get(0); + + assertThat(change.getEvent()).isEqualTo(ChangeType.DELETE); + assertThat(change.getData()).isNull(); + } + + @Test + public void testWithNull() { + + EBasicChangeLog bean = new EBasicChangeLog(); + bean.setName(null); + bean.setShortDescription("hello"); + server.save(bean); + BeanChange change = changeLogListener.changes.getChanges().get(0); + + assertThat(change.getEvent()).isEqualTo(ChangeType.INSERT); + assertThat(change.getData()) + .doesNotContain("\"name\"") + .contains("\"shortDescription\":\"hello\""); + + + bean.setName("log"); + bean.setName("logBean"); + bean.setShortDescription("world"); + bean.setShortDescription("hello"); + server.save(bean); + + change = changeLogListener.changes.getChanges().get(0); + + assertThat(change.getEvent()).isEqualTo(ChangeType.UPDATE); + assertThat(change.getOldData()) + .contains("\"name\":null") // it was null + .doesNotContain("\"shortDescription\""); // it is unchanged + + assertThat(change.getData()) + .contains("\"name\":\"logBean\"") + .doesNotContain("\"shortDescription\""); // it is unchanged + + + server.delete(bean); + + change = changeLogListener.changes.getChanges().get(0); + + assertThat(change.getEvent()).isEqualTo(ChangeType.DELETE); + assertThat(change.getData()).isNull(); + + } + + private SpiEbeanServer getServer() { System.setProperty("ebean.ignoreExtraDdl", "true"); diff --git a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java index 7f584c378..9a42ea601 100644 --- a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java +++ b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java @@ -4,8 +4,6 @@ import io.ebean.BaseTestCase; import io.ebean.CacheMode; import io.ebean.Ebean; import io.ebean.Pairs; -import io.ebean.annotation.IgnorePlatform; -import io.ebean.annotation.Platform; import io.ebean.cache.ServerCache; import io.ebean.cache.ServerCacheManager; import io.ebean.cache.ServerCacheStatistics; @@ -251,7 +249,6 @@ public class TestCacheViaComplexNaturalKey3 extends BaseTestCase { assertBeanCacheHitMiss(0, 0); } - @IgnorePlatform({Platform.MYSQL, Platform.SQLSERVER}) @Test public void findList_inPairs_standardConcat() { @@ -280,14 +277,15 @@ public class TestCacheViaComplexNaturalKey3 extends BaseTestCase { assertBeanCacheHitMiss(1, 0); if (isH2()) { - assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (t0.sku||'-'||t0.code) in (?, ? ) order by t0.sku desc; --bind(def,Array[2]={2-1000,3-1000})"); - } else { + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and concat(t0.sku,'-',t0.code) in (?, ? ) order by t0.sku desc; --bind(def,Array[2]={2-1000,3-1000})"); + } else if (isPostgres()) { assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (t0.sku||'-'||t0.code)"); + } else { + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and concat(t0.sku,'-',t0.code)"); } } - @IgnorePlatform({Platform.MYSQL, Platform.SQLSERVER}) @Test public void findList_inPairs_userConcat() { @@ -318,9 +316,11 @@ public class TestCacheViaComplexNaturalKey3 extends BaseTestCase { assertBeanCacheHitMiss(1, 0); if (isH2()) { - assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (t0.sku||':'||t0.code||'-foo') in (?, ? ) order by t0.sku desc; --bind(def,Array[2]={2:1000-foo,3:1000-foo})"); - } else { + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and concat(t0.sku,':',t0.code,'-foo') in (?, ? ) order by t0.sku desc; --bind(def,Array[2]={2:1000-foo,3:1000-foo})"); + } else if (isPostgres()){ assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and (t0.sku||':'||t0.code||'-foo')"); + } else { + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and concat(t0.sku,':',t0.code,'-foo')"); } } diff --git a/src/test/java/org/tests/rawsql/nativesql/TestNativeSqlBasic.java b/src/test/java/org/tests/rawsql/nativesql/TestNativeSqlBasic.java index 94f7219a4..1ab49cda3 100644 --- a/src/test/java/org/tests/rawsql/nativesql/TestNativeSqlBasic.java +++ b/src/test/java/org/tests/rawsql/nativesql/TestNativeSqlBasic.java @@ -139,7 +139,7 @@ public class TestNativeSqlBasic extends BaseTestCase { * Oracle does not support getTableName() via JDBC resultSet meta data */ @Test - @IgnorePlatform({Platform.SQLSERVER, Platform.ORACLE}) // does only work in 'cursor' mode! + @IgnorePlatform({Platform.ORACLE}) public void partialAssoc() { ResetBasicData.reset(); diff --git a/src/test/java/org/tests/transaction/TestNestedSubTransaction.java b/src/test/java/org/tests/transaction/TestNestedSubTransaction.java index 05eeed4d8..dc9e3d147 100644 --- a/src/test/java/org/tests/transaction/TestNestedSubTransaction.java +++ b/src/test/java/org/tests/transaction/TestNestedSubTransaction.java @@ -20,7 +20,7 @@ public class TestNestedSubTransaction extends BaseTestCase { /** * MySql only supports named savepoints - review. */ - @IgnorePlatform(Platform.MYSQL) + @IgnorePlatform({Platform.MYSQL, Platform.SQLSERVER}) @Test public void ebeanServer_commitTransaction_expect_sameAsTransactionCommit() { @@ -65,7 +65,7 @@ public class TestNestedSubTransaction extends BaseTestCase { } - @IgnorePlatform(Platform.MYSQL) + @IgnorePlatform({Platform.SQLSERVER, Platform.MYSQL}) @Test public void nestedUseSavepoint_doubleNested_rollbackCommit() { @@ -102,7 +102,7 @@ public class TestNestedSubTransaction extends BaseTestCase { } } - @IgnorePlatform(Platform.MYSQL) + @IgnorePlatform({Platform.SQLSERVER, Platform.MYSQL}) @Test public void nestedUseSavepoint_doubleNested_commitRollback() { @@ -139,7 +139,7 @@ public class TestNestedSubTransaction extends BaseTestCase { } } - @IgnorePlatform(Platform.MYSQL) + @IgnorePlatform({Platform.SQLSERVER, Platform.MYSQL}) @Test public void nestedUseSavepoint_nested_RequiresNew() { @@ -175,7 +175,7 @@ public class TestNestedSubTransaction extends BaseTestCase { assertNull(after); } - @IgnorePlatform(Platform.MYSQL) + @IgnorePlatform({Platform.SQLSERVER, Platform.MYSQL}) @Test public void nestedUseSavepoint() { diff --git a/src/test/java/org/tests/unitinternal/TestOrderByParse.java b/src/test/java/org/tests/unitinternal/TestOrderByParse.java index 5fc876353..37ea83782 100644 --- a/src/test/java/org/tests/unitinternal/TestOrderByParse.java +++ b/src/test/java/org/tests/unitinternal/TestOrderByParse.java @@ -4,7 +4,10 @@ import io.ebean.BaseTestCase; import io.ebean.OrderBy; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; /** * Test the OrderBy object and especially its parsing. @@ -153,4 +156,92 @@ public class TestOrderByParse extends BaseTestCase { assertEquals("id, name", copy.toStringFormat()); } + + @Test + public void testParsingWithCollation() { + + OrderBy o1 = new OrderBy<>(); + o1.asc("id", "latin_1"); + assertTrue(o1.getProperties().size() == 1); + assertTrue(o1.getProperties().get(0).getProperty().equals("id")); + assertTrue(o1.getProperties().get(0).isAscending()); + assertEquals("id collate latin_1", o1.toStringFormat()); + + o1 = new OrderBy<>(); + o1.desc("id", "latin_1"); + assertTrue(o1.getProperties().size() == 1); + assertTrue(o1.getProperties().get(0).getProperty().equals("id")); + assertTrue(!o1.getProperties().get(0).isAscending()); + assertEquals("id collate latin_1 desc", o1.toStringFormat()); + + o1 = new OrderBy<>(); + o1.desc("id", "latin_1"); + o1.asc("date"); + assertTrue(o1.getProperties().size() == 2); + assertTrue(o1.getProperties().get(0).getProperty().equals("id")); + assertTrue(o1.getProperties().get(1).getProperty().equals("date")); + assertTrue(!o1.getProperties().get(0).isAscending()); + assertTrue(o1.getProperties().get(1).isAscending()); + assertEquals("id collate latin_1 desc, date", o1.toStringFormat()); + + o1 = new OrderBy<>(); + o1.desc("id", "latin_1"); + o1.asc("name", "latin_2"); + assertTrue(o1.getProperties().size() == 2); + assertTrue(o1.getProperties().get(0).getProperty().equals("id")); + assertTrue(o1.getProperties().get(1).getProperty().equals("name")); + assertTrue(!o1.getProperties().get(0).isAscending()); + assertTrue(o1.getProperties().get(1).isAscending()); + assertEquals("id collate latin_1 desc, name collate latin_2", o1.toStringFormat()); + + // functional (DB2) syntax + o1 = new OrderBy<>(); + o1.desc("id", "COLLATION_KEY(${}, 'latin_1')"); + assertTrue(o1.getProperties().size() == 1); + assertTrue(o1.getProperties().get(0).getProperty().equals("id")); + assertTrue(!o1.getProperties().get(0).isAscending()); + assertEquals("COLLATION_KEY(id, 'latin_1') desc", o1.toStringFormat()); + + } + + @Test + public void equals_with_nulls() { + + OrderBy o1 = new OrderBy<>("id desc nulls high"); + OrderBy o2 = new OrderBy<>("id desc nulls high"); + OrderBy o3 = new OrderBy<>(); + o3.add("id desc nulls high"); + + assertEquals(o1, o2); + assertEquals(o1, o3); + + + OrderBy o4 = new OrderBy<>("id desc"); + OrderBy o5 = new OrderBy<>("oid desc nulls high"); + OrderBy o6 = new OrderBy<>("id desc nulls low"); + + assertNotEquals(o1, o4); + assertNotEquals(o1, o5); + assertNotEquals(o1, o6); + } + + @Test + public void equals_with_collation() { + + OrderBy o1 = new OrderBy<>(); + o1.asc("name", "latin_1"); + + OrderBy o2 = new OrderBy<>(); + o2.asc("name", null); + + OrderBy o3 = new OrderBy<>(); + o2.asc("name", "bar"); + + assertNotEquals(o1, o2); + assertNotEquals(o1, o3); + + OrderBy o4 = new OrderBy<>(); + o4.asc("name", "latin_1"); + assertEquals(o1, o4); + } }