diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiExpression.java b/src/main/java/com/avaje/ebeaninternal/api/SpiExpression.java index a500974dd..4969e186f 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiExpression.java @@ -18,8 +18,13 @@ public interface SpiExpression extends Expression { */ void writeDocQuery(DocQueryContext context) throws IOException; - /** - * Process "Many" properties populating ManyWhereJoins. + /** + * Return the nested path for this expression. + */ + String nestedPath(BeanDescriptor desc); + + /** + * Process "Many" properties populating ManyWhereJoins. *

* Predicates on Many properties require an extra independent join clause. *

diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java index 6db2c9041..b41d51e0a 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiQuery.java @@ -301,6 +301,11 @@ public interface SpiQuery extends Query { */ NaturalKeyBindParam getNaturalKeyBindParam(); + /** + * Prepare the query for docstore execution with nested paths. + */ + void prepareDocNested(); + /** * Set the query to be a delete query. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java index 53fd10b96..374596ace 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/AbstractExpression.java @@ -8,6 +8,7 @@ import com.avaje.ebeaninternal.api.SpiExpressionValidation; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.el.ElPropertyDeploy; import com.avaje.ebeaninternal.server.el.ElPropertyValue; +import com.avaje.ebeaninternal.server.query.SplitName; /** * Base class for simple expressions. @@ -25,6 +26,21 @@ public abstract class AbstractExpression implements SpiExpression { return this; } + @Override + public String nestedPath(BeanDescriptor desc) { + return propertyNestedPath(propName, desc); + } + + protected String propertyNestedPath(String propertyName, BeanDescriptor desc) { + if (propertyName != null) { + ElPropertyDeploy elProp = desc.getElPropertyDeploy(propertyName); + if (elProp != null && elProp.containsMany()) { + return SplitName.begin(propName); + } + } + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/AllEqualsExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/AllEqualsExpression.java index 2883412e4..bef1ee0bc 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/AllEqualsExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/AllEqualsExpression.java @@ -30,6 +30,11 @@ class AllEqualsExpression extends NonPrepareExpression { context.writeAllEquals(propMap); } + @Override + public String nestedPath(BeanDescriptor desc) { + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { if (propMap != null) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenPropertyExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenPropertyExpression.java index d121437b6..58a50efc3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenPropertyExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/BetweenPropertyExpression.java @@ -7,6 +7,7 @@ import com.avaje.ebeaninternal.api.SpiExpressionRequest; import com.avaje.ebeaninternal.api.SpiExpressionValidation; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.el.ElPropertyDeploy; +import com.avaje.ebeaninternal.server.query.SplitName; import java.io.IOException; @@ -39,6 +40,16 @@ class BetweenPropertyExpression extends NonPrepareExpression { context.endBool(); } + @Override + public String nestedPath(BeanDescriptor desc) { + ElPropertyDeploy elProp = desc.getElPropertyDeploy(name(lowProperty)); + if (elProp != null && elProp.containsMany()) { + // assumes highProperty is also nested property which seems reasonable + return SplitName.begin(lowProperty); + } + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java index 1b36411bc..07d296abf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExampleExpression.java @@ -109,6 +109,11 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio return new DefaultExampleExpression(list); } + @Override + public String nestedPath(BeanDescriptor desc) { + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins whereManyJoins) { list = buildExpressions(desc); diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java index 039bf277e..63ae797fe 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DefaultExpressionList.java @@ -31,7 +31,7 @@ public class DefaultExpressionList implements SpiExpressionList { private static final String AND = " and "; - protected final List list; + protected List list; protected final Query query; @@ -39,6 +39,8 @@ public class DefaultExpressionList implements SpiExpressionList { protected transient ExpressionFactory expr; + protected String allDocNestedPath; + /** * Set to true for the "Text" root expression list. */ @@ -75,6 +77,26 @@ public class DefaultExpressionList implements SpiExpressionList { this(null, null, null, new ArrayList()); } + /** + * Wrap the expression list as a Junction or top level DefaultExpressionList. + * + * @param list The list of expressions grouped by nested path + * @param nestedPath The doc store nested path + * @param type The junction type (or null for top level expression list). + * @return A single SpiExpression that has the nestedPath set + */ + SpiExpression wrap(List list, String nestedPath, Junction.Type type) { + + DefaultExpressionList wrapper = new DefaultExpressionList(query, expr, null, list, false); + wrapper.setAllDocNested(nestedPath); + + if (type != null) { + return new JunctionExpression(type, wrapper); + } else { + return wrapper; + } + } + /** * Write being aware if it is the Top level "text" expressions. *

@@ -94,6 +116,7 @@ public class DefaultExpressionList implements SpiExpressionList { // this is a Top level "text" expressions so we may need to wrap in Bool SHOULD etc. if (list.isEmpty()) throw new IllegalStateException("empty expression list?"); + if (allDocNestedPath!=null) context.startNested(allDocNestedPath); int size = list.size(); SpiExpression first = list.get(0); @@ -124,11 +147,13 @@ public class DefaultExpressionList implements SpiExpressionList { if (implicitBool || explicitBool) { context.endBoolGroup(); } + if (allDocNestedPath!=null) context.endNested(); } } public void writeDocQuery(DocQueryContext context, SpiExpression idEquals) throws IOException { + if (allDocNestedPath!=null) context.startNested(allDocNestedPath); int size = list.size(); if (size == 1 && idEquals == null) { // only 1 expression - skip bool @@ -147,6 +172,7 @@ public class DefaultExpressionList implements SpiExpressionList { } context.endBool(); } + if (allDocNestedPath!=null) context.endNested(); } @Override @@ -975,4 +1001,30 @@ public class DefaultExpressionList implements SpiExpressionList { return junction(Junction.Type.MUST_NOT); } + @Override + public String nestedPath(BeanDescriptor desc) { + // effectively handled by JunctionExpression + return null; + } + + /** + * Set the nested path that all contained expressions share. + */ + public void setAllDocNested(String allDocNestedPath) { + this.allDocNestedPath = allDocNestedPath; + } + + /** + * Replace the underlying expression list with one organised by nested path. + */ + public void setUnderlying(List groupedByNesting) { + this.list = groupedByNesting; + } + + /** + * Prepare expressions for document store nested path handling. + */ + public void prepareDocNested(BeanDescriptor beanDescriptor) { + PrepareDocNested.prepare(this, beanDescriptor); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/DocQueryContext.java b/src/main/java/com/avaje/ebeaninternal/server/expression/DocQueryContext.java index 123efb830..c2b9b620f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/DocQueryContext.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/DocQueryContext.java @@ -147,4 +147,14 @@ public interface DocQueryContext { * Return the expression path for the given property path. */ ExpressionPath getExpressionPath(String propName); + + /** + * Start nested path expressions. + */ + void startNested(String nestedPath) throws IOException; + + /** + * End nested path expressions. + */ + void endNested() throws IOException; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/ExistsQueryExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/ExistsQueryExpression.java index ec937239d..a34110abf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/ExistsQueryExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/ExistsQueryExpression.java @@ -118,6 +118,11 @@ class ExistsQueryExpression implements SpiExpression, UnsupportedDocStoreExpress return true; } + @Override + public String nestedPath(BeanDescriptor desc) { + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins whereManyJoins) { // Nothing to do for exists expression diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/IdExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/IdExpression.java index 73692a5f5..c74767df2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/IdExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/IdExpression.java @@ -25,6 +25,11 @@ class IdExpression extends NonPrepareExpression implements SpiExpression { context.writeId(value); } + @Override + public String nestedPath(BeanDescriptor desc) { + return null; + } + /** * Always returns false. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/IdInExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/IdInExpression.java index b9c7e09e5..3c153b9ad 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/IdInExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/IdInExpression.java @@ -22,6 +22,11 @@ public class IdInExpression extends NonPrepareExpression { this.idList = idList; } + @Override + public String nestedPath(BeanDescriptor desc) { + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java index 47f1631f0..55fa7ce29 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/JunctionExpression.java @@ -39,7 +39,7 @@ import java.util.Set; */ class JunctionExpression implements SpiJunction, SpiExpression, ExpressionList { - private final DefaultExpressionList exprList; + protected final DefaultExpressionList exprList; protected final Junction.Type type; @@ -790,4 +790,17 @@ class JunctionExpression implements SpiJunction, SpiExpression, Expression public ExpressionList endNot() { return endJunction(); } + + @Override + public String nestedPath(BeanDescriptor desc) { + + PrepareDocNested.prepare(exprList, desc, type); + String nestedPath = exprList.allDocNestedPath; + if (nestedPath != null) { + // push the nestedPath up to parent + exprList.setAllDocNested(null); + return nestedPath; + } + return null; + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/LogicExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/LogicExpression.java index b94bfb1d3..3efb7cb65 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/LogicExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/LogicExpression.java @@ -45,9 +45,9 @@ abstract class LogicExpression implements SpiExpression { } } - protected final SpiExpression expOne; + protected SpiExpression expOne; - protected final SpiExpression expTwo; + protected SpiExpression expTwo; private final String joinType; @@ -67,6 +67,27 @@ abstract class LogicExpression implements SpiExpression { context.endBool(); } + @Override + public String nestedPath(BeanDescriptor desc) { + + String pathOne = expOne.nestedPath(desc); + String pathTwo = expTwo.nestedPath(desc); + + if (pathOne == null && pathTwo == null) { + return null; + } + if (pathOne != null && pathOne.equals(pathTwo)) { + return pathOne; + } + if (pathOne != null) { + expOne = new NestedPathWrapperExpression(pathOne, expOne); + } + if (pathTwo != null) { + expTwo = new NestedPathWrapperExpression(pathTwo, expTwo); + } + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { expOne.containsMany(desc, manyWhereJoin); diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/NestedPathWrapperExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/NestedPathWrapperExpression.java new file mode 100644 index 000000000..92a730a20 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/NestedPathWrapperExpression.java @@ -0,0 +1,93 @@ +package com.avaje.ebeaninternal.server.expression; + +import com.avaje.ebean.event.BeanQueryRequest; +import com.avaje.ebeaninternal.api.HashQueryPlanBuilder; +import com.avaje.ebeaninternal.api.ManyWhereJoins; +import com.avaje.ebeaninternal.api.SpiExpression; +import com.avaje.ebeaninternal.api.SpiExpressionRequest; +import com.avaje.ebeaninternal.api.SpiExpressionValidation; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; + +import java.io.IOException; + +/** + * Wraps a single expression with nestedPath for document queries. + */ +class NestedPathWrapperExpression implements SpiExpression { + + protected final String nestedPath; + + protected final SpiExpression delegate; + + NestedPathWrapperExpression(String nestedPath, SpiExpression delegate) { + this.nestedPath = nestedPath; + this.delegate = delegate; + } + + @Override + public void writeDocQuery(DocQueryContext context) throws IOException { + context.startNested(nestedPath); + delegate.writeDocQuery(context); + context.endNested(); + } + + @Override + public String nestedPath(BeanDescriptor desc) { + return nestedPath; + } + + @Override + public void containsMany(BeanDescriptor desc, ManyWhereJoins whereManyJoins) { + delegate.containsMany(desc, whereManyJoins); + } + + @Override + public void prepareExpression(BeanQueryRequest request) { + delegate.prepareExpression(request); + } + + @Override + public void queryPlanHash(HashQueryPlanBuilder builder) { + delegate.queryPlanHash(builder); + } + + @Override + public int queryBindHash() { + return delegate.queryBindHash(); + } + + @Override + public boolean isSameByPlan(SpiExpression other) { + if (other instanceof NestedPathWrapperExpression) { + NestedPathWrapperExpression that = (NestedPathWrapperExpression)other; + return nestedPath.equals(that.nestedPath) + && delegate.isSameByPlan(that.delegate); + } + return false; + } + + @Override + public boolean isSameByBind(SpiExpression other) { + return delegate.isSameByBind(other); + } + + @Override + public void addSql(SpiExpressionRequest request) { + delegate.addSql(request); + } + + @Override + public void addBindValues(SpiExpressionRequest request) { + delegate.addBindValues(request); + } + + @Override + public void validate(SpiExpressionValidation validation) { + delegate.validate(validation); + } + + @Override + public SpiExpression copyForPlanKey() { + return new NestedPathWrapperExpression(nestedPath, delegate.copyForPlanKey()); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/NoopExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/NoopExpression.java index 1c9bc39fa..e771e7916 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/NoopExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/NoopExpression.java @@ -26,6 +26,11 @@ class NoopExpression implements SpiExpression { public void writeDocQuery(DocQueryContext context) throws IOException { } + @Override + public String nestedPath(BeanDescriptor desc) { + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins whereManyJoins) { // nothing to do diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/NotExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/NotExpression.java index 5e4b1c09a..4024a43c9 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/NotExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/NotExpression.java @@ -34,6 +34,11 @@ final class NotExpression implements SpiExpression { return new NotExpression(exp.copyForPlanKey()); } + @Override + public String nestedPath(BeanDescriptor desc) { + return exp.nestedPath(desc); + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { exp.containsMany(desc, manyWhereJoin); diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/PrepareDocNested.java b/src/main/java/com/avaje/ebeaninternal/server/expression/PrepareDocNested.java new file mode 100644 index 000000000..ee917f830 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/PrepareDocNested.java @@ -0,0 +1,163 @@ +package com.avaje.ebeaninternal.server.expression; + +import com.avaje.ebean.Junction; +import com.avaje.ebeaninternal.api.SpiExpression; +import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Prepare nested path expressions for + */ +class PrepareDocNested { + + /** + * Prepare the top level expressions for nested path handling. + */ + static void prepare(DefaultExpressionList expressions, BeanDescriptor beanDescriptor) { + new PrepareDocNested(expressions, beanDescriptor, null).process(); + } + + /** + * Prepare the Junction expressions for nested path handling. + */ + static void prepare(DefaultExpressionList expressions, BeanDescriptor beanDescriptor, Junction.Type type) { + new PrepareDocNested(expressions, beanDescriptor, type).process(); + } + + enum Mode { + NONE, + SINGLE, + MIXED + } + + private final Junction.Type type; + private final DefaultExpressionList original; + private final BeanDescriptor beanDescriptor; + private final List origUnderlying; + private final int origSize; + + private boolean hasNesting; + private boolean hasMixedNesting; + private String firstNestedPath; + + + PrepareDocNested(DefaultExpressionList original, BeanDescriptor beanDescriptor, Junction.Type type) { + this.type = type; + this.beanDescriptor = beanDescriptor; + this.original = original; + this.origUnderlying = original.getUnderlyingList(); + this.origSize = origUnderlying.size(); + } + + void process() { + + PrepareDocNested.Mode mode = determineMode(); + if (mode == PrepareDocNested.Mode.SINGLE) { + original.setAllDocNested(firstNestedPath); + + } else if (mode == PrepareDocNested.Mode.MIXED) { + original.setUnderlying(group()); + } + + } + + /** + * Reorganise the flat list of expressions into a tree grouping expressions by nested path. + * + * Returns the new top level list of expressions. + */ + private List group() { + + Map groups = new LinkedHashMap(); + + // organise expressions by nestedPath + for (int i = 0; i < origSize; i++) { + SpiExpression expr = origUnderlying.get(i); + String nestedPath = expr.nestedPath(beanDescriptor); + Group group = groups.get(nestedPath); + if (group == null) { + group = new Group(nestedPath); + groups.put(nestedPath, group); + } + group.list.add(expr); + } + + List newList = new ArrayList(); + Collection values = groups.values(); + for (Group group : values) { + group.addTo(newList); + } + return newList; + } + + /** + * Determined the nested path mode. + */ + private Mode determineMode() { + + if (!hasNesting()) { + // no nested paths at all + return Mode.NONE; + } + if (!hasMixedNesting) { + // single nested path for all expressions + return Mode.SINGLE; + } + // mixed nested paths to underlying expression list needs re-organising by nested path + return Mode.MIXED; + } + + /** + * Return true if the expressions have nested paths. + */ + private boolean hasNesting() { + + for (int i = 0; i < origSize; i++) { + SpiExpression expr = origUnderlying.get(i); + String nestedPath = expr.nestedPath(beanDescriptor); + if (nestedPath == null) { + hasMixedNesting = true; + + } if (nestedPath != null) { + hasNesting = true; + if (firstNestedPath == null) { + firstNestedPath = nestedPath; + } else if (hasMixedNesting || !firstNestedPath.equals(nestedPath)) { + hasMixedNesting = true; + return true; + } + } + } + + return hasNesting; + } + + + /** + * List of SpiExpression grouped by nested path. + */ + class Group { + + final String nestedPath; + + final List list = new ArrayList(); + + Group(String nestedPath) { + this.nestedPath = nestedPath; + } + + void addTo(List newList) { + if (nestedPath == null) { + newList.addAll(list); + } else { + newList.add(original.wrap(list, nestedPath, type)); + } + } + } + +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/RawExpression.java b/src/main/java/com/avaje/ebeaninternal/server/expression/RawExpression.java index bbb48f21c..3632086cf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/RawExpression.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/RawExpression.java @@ -25,6 +25,11 @@ class RawExpression extends NonPrepareExpression { context.writeRaw(sql, values); } + @Override + public String nestedPath(BeanDescriptor desc) { + return null; + } + @Override public void containsMany(BeanDescriptor desc, ManyWhereJoins manyWhereJoin) { diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/SplitName.java b/src/main/java/com/avaje/ebeaninternal/server/query/SplitName.java index 8d9e66cbb..77f82d039 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/SplitName.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/SplitName.java @@ -1,9 +1,15 @@ package com.avaje.ebeaninternal.server.query; +/** + * Helper for dot notation property paths. + */ public class SplitName { private static final char PERIOD = '.'; + /** + * Add the two name sections together in dot notation. + */ public static String add(String prefix, String name) { if (prefix != null) { return prefix + "." + name; @@ -38,10 +44,20 @@ public class SplitName { } } + /** + * Return the name split by last. + */ public static String[] split(String name) { return split(name, true); } + /** + * Return the first part of the name. + */ + public static String begin(String name) { + return splitBegin(name)[0]; + } + public static String[] splitBegin(String name) { return split(name, false); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 983076e55..aea04264f 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -484,6 +484,16 @@ public class DefaultOrmQuery implements SpiQuery { } } + @Override + public void prepareDocNested() { + if (textExpressions != null) { + textExpressions.prepareDocNested(beanDescriptor); + } + if (whereExpressions != null) { + whereExpressions.prepareDocNested(beanDescriptor); + } + } + /** * Setup to be a delete query. */ diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/LogicExpressionTest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/LogicExpressionTest.java index 742a6ebd0..099584e0f 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/expression/LogicExpressionTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/LogicExpressionTest.java @@ -2,6 +2,7 @@ package com.avaje.ebeaninternal.server.expression; import com.avaje.ebean.Expr; import com.avaje.ebean.Expression; +import com.avaje.tests.model.basic.Order; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -75,4 +76,54 @@ public class LogicExpressionTest extends BaseExpressionTest { assertThat(and(eq("a", 10), eq("b", 10)) .isSameByBind(and(eq("a", 10), eq("c", 20)))).isFalse(); } + + @Test + public void nestedPath_when_notNested() { + + LogicExpression and = and(eq("orderDate", 10), eq("shipDate", 10)); + + and.nestedPath(getBeanDescriptor(Order.class)); + + assertThat(and.expOne).isInstanceOf(SimpleExpression.class); + assertThat(and.expTwo).isInstanceOf(SimpleExpression.class); + } + + @Test + public void nestedPath_when_nestedSame() { + + LogicExpression and = and(eq("details.orderQty", 10), eq("details.unitPrice", 10)); + + String path = and.nestedPath(getBeanDescriptor(Order.class)); + + assertThat(path).isEqualTo("details"); + assertThat(and.expOne).isInstanceOf(SimpleExpression.class); + assertThat(and.expTwo).isInstanceOf(SimpleExpression.class); + } + + @Test + public void nestedPath_when_nestedDifferent() { + + LogicExpression and = and(eq("details.orderQty", 10), eq("shipments.shipTime", 10)); + + String path = and.nestedPath(getBeanDescriptor(Order.class)); + + assertThat(path).isNull(); + assertThat(and.expOne).isInstanceOf(NestedPathWrapperExpression.class); + assertThat(((NestedPathWrapperExpression)and.expOne).nestedPath).isEqualTo("details"); + assertThat(and.expTwo).isInstanceOf(NestedPathWrapperExpression.class); + assertThat(((NestedPathWrapperExpression)and.expTwo).nestedPath).isEqualTo("shipments"); + } + + @Test + public void nestedPath_when_oneNested() { + + LogicExpression and = and(eq("details.orderQty", 10), eq("orderDate", 10)); + + String path = and.nestedPath(getBeanDescriptor(Order.class)); + + assertThat(path).isNull(); + assertThat(and.expOne).isInstanceOf(NestedPathWrapperExpression.class); + assertThat(((NestedPathWrapperExpression)and.expOne).nestedPath).isEqualTo("details"); + assertThat(and.expTwo).isInstanceOf(SimpleExpression.class); + } } \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/expression/PrepareDocNestedTest.java b/src/test/java/com/avaje/ebeaninternal/server/expression/PrepareDocNestedTest.java new file mode 100644 index 000000000..977bd01aa --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/expression/PrepareDocNestedTest.java @@ -0,0 +1,148 @@ +package com.avaje.ebeaninternal.server.expression; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.ExpressionList; +import com.avaje.ebeaninternal.api.SpiExpression; +import com.avaje.tests.model.basic.Order; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class PrepareDocNestedTest extends BaseTestCase { + @Test + public void prepare() throws Exception { + + ExpressionList where = Ebean.find(Order.class) + .where() + .gt("details.orderQty", 1) + .query().where(); + + + DefaultExpressionList exp = (DefaultExpressionList)where; + PrepareDocNested.prepare(exp, getBeanDescriptor(Order.class)); + + List underlyingList = exp.getUnderlyingList(); + assertEquals(underlyingList.size(), 1); + assertEquals(exp.allDocNestedPath, "details"); + } + + @Test + public void prepare_when_multipleOfSamePath() throws Exception { + + ExpressionList where = Ebean.find(Order.class) + .where() + .gt("details.orderQty", 1) + .gt("details.unitPrice", 1) + .query().where(); + + + DefaultExpressionList exp = (DefaultExpressionList)where; + PrepareDocNested.prepare(exp, getBeanDescriptor(Order.class)); + + List underlyingList = exp.getUnderlyingList(); + assertEquals(underlyingList.size(), 2); + assertEquals(exp.allDocNestedPath, "details"); + } + + @Test + public void prepare_when_mixed() throws Exception { + + ExpressionList where = Ebean.find(Order.class) + .where() + .gt("customer.id", 1) + .gt("details.orderQty", 1) + .gt("details.unitPrice", 1) + .query().where(); + + + DefaultExpressionList exp = (DefaultExpressionList)where; + PrepareDocNested.prepare(exp, getBeanDescriptor(Order.class)); + + List underlyingList = exp.getUnderlyingList(); + assertEquals(underlyingList.size(), 2); + assertNull(exp.allDocNestedPath); + + DefaultExpressionList second = (DefaultExpressionList)underlyingList.get(1); + assertEquals(second.allDocNestedPath, "details"); + } + + @Test + public void prepare_when_nestedJunction() throws Exception { + + ExpressionList where = Ebean.find(Order.class) + .where() + .not() + .gt("customer.id", 1) + .gt("details.orderQty", 1) + .gt("details.unitPrice", 1) + .query().where(); + + DefaultExpressionList exp = (DefaultExpressionList)where; + PrepareDocNested.prepare(exp, getBeanDescriptor(Order.class)); + + List underlyingList = exp.getUnderlyingList(); + assertEquals(underlyingList.size(), 1); + assertNull(exp.allDocNestedPath); + + JunctionExpression junction = (JunctionExpression)underlyingList.get(0); + List junctionUnderlying = junction.exprList.getUnderlyingList(); + JunctionExpression nestedNestedPath = (JunctionExpression)junctionUnderlying.get(1); + assertEquals(nestedNestedPath.exprList.allDocNestedPath, "details"); + } + + @Test + public void prepare_when_nestedMultiple() throws Exception { + + ExpressionList where = Ebean.find(Order.class) + .where() + .isNotNull("shipments.shipTime") + .gt("details.orderQty", 1) + .gt("details.unitPrice", 1) + .query().where(); + + DefaultExpressionList exp = (DefaultExpressionList)where; + PrepareDocNested.prepare(exp, getBeanDescriptor(Order.class)); + + List underlyingList = exp.getUnderlyingList(); + assertEquals(underlyingList.size(), 2); + assertNull(exp.allDocNestedPath); + + DefaultExpressionList shipExpr = (DefaultExpressionList)underlyingList.get(0); + assertEquals(shipExpr.allDocNestedPath, "shipments"); + + DefaultExpressionList detailsExpr = (DefaultExpressionList)underlyingList.get(1); + assertEquals(detailsExpr.allDocNestedPath, "details"); + } + + + @Test + public void prepare_when_manyMixed() throws Exception { + + ExpressionList where = Ebean.find(Order.class) + .where() + .gt("customer.id", 1) // 0 + .isNotNull("shipments.shipTime") // shipments 0 + .isNotNull("status") // 1 + .gt("details.orderQty", 1) // details 0 + .isNotNull("orderDate") // 2 + .gt("details.unitPrice", 1) // details 1 + .query().where(); + + DefaultExpressionList exp = (DefaultExpressionList)where; + PrepareDocNested.prepare(exp, getBeanDescriptor(Order.class)); + + List underlyingList = exp.getUnderlyingList(); + assertEquals(underlyingList.size(), 5); + assertNull(exp.allDocNestedPath); + + DefaultExpressionList shipExpr = (DefaultExpressionList)underlyingList.get(3); + assertEquals(shipExpr.allDocNestedPath, "shipments"); + + DefaultExpressionList detailsExpr = (DefaultExpressionList)underlyingList.get(4); + assertEquals(detailsExpr.allDocNestedPath, "details"); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/query/SplitNameTest.java b/src/test/java/com/avaje/ebeaninternal/server/query/SplitNameTest.java new file mode 100644 index 000000000..b5070b11d --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/query/SplitNameTest.java @@ -0,0 +1,82 @@ +package com.avaje.ebeaninternal.server.query; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SplitNameTest { + + @Test + public void add() throws Exception { + + assertEquals(SplitName.add("a","b"), "a.b"); + assertEquals(SplitName.add("a","b.c"), "a.b.c"); + } + + @Test + public void count() throws Exception { + + assertEquals(SplitName.count("a"), 0); + assertEquals(SplitName.count("a.b"), 1); + assertEquals(SplitName.count("a.b.c"), 2); + assertEquals(SplitName.count("a.b.c.foo"), 3); + } + + @Test + public void parent() throws Exception { + assertNull(SplitName.parent("a")); + assertEquals(SplitName.parent("a.b"), "a"); + assertEquals(SplitName.parent("a.b.c"), "a.b"); + assertNull(SplitName.parent(null)); + } + + @Test + public void split() throws Exception { + + String[] split = SplitName.split("a.b.c"); + assertEquals(split[0], "a.b"); + assertEquals(split[1], "c"); + } + + @Test + public void begin_when_one() throws Exception { + + assertEquals(SplitName.begin("a"), "a"); + } + + @Test + public void begin_when_both() throws Exception { + + assertEquals(SplitName.begin("a.b"), "a"); + } + + @Test + public void begin_when_multi() throws Exception { + + assertEquals(SplitName.begin("a.b.c"), "a"); + } + + @Test + public void splitBegin_when_both() throws Exception { + + String[] split = SplitName.splitBegin("a.b"); + assertEquals(split[0], "a"); + assertEquals(split[1], "b"); + } + + @Test + public void splitBegin_when_bothPlus() throws Exception { + + String[] split = SplitName.splitBegin("a.b.c"); + assertEquals(split[0], "a"); + assertEquals(split[1], "b.c"); + } + + @Test + public void splitBegin_when_one() throws Exception { + + String[] split = SplitName.splitBegin("a"); + assertEquals(split[0], "a"); + assertNull(split[1]); + } +} \ No newline at end of file