From 2fcaab44c65668e93bf5c2bd80d3bf497f37c066 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Wed, 17 Feb 2016 10:52:55 +1300 Subject: [PATCH] #566 - Refactor internals - Initial tidy on OrmQueryDetail and OrmQueryDetailProperties --- .../com/avaje/ebean/text/PathProperties.java | 10 +- .../api/HashQueryPlanBuilder.java | 17 ++ .../server/core/DefaultServer.java | 2 +- .../server/deploy/BeanDescriptor.java | 6 +- .../deploy/meta/DeployBeanDescriptor.java | 5 +- .../ebeaninternal/server/expression/Same.java | 23 ++ .../server/query/CQueryBuilder.java | 4 +- .../server/querydefn/OrmQueryDetail.java | 94 +++++-- .../querydefn/OrmQueryDetailParser.java | 4 +- .../server/querydefn/OrmQueryProperties.java | 259 ++++++------------ .../querydefn/OrmQueryPropertiesParser.java | 183 +++++++++++++ .../server/querydefn/SimpleTextParser.java | 4 + ...faultServer_createOrmQueryRequestTest.java | 126 +++++++++ .../server/querydefn/DefaultOrmQueryTest.java | 18 -- .../querydefn/OrmQueryDetailParserTest.java | 73 +++-- .../server/querydefn/OrmQueryDetailTest.java | 62 ++++- .../OrmQueryPropertiesParserTest.java | 132 +++++++++ .../querydefn/OrmQueryPropertiesTest.java | 50 ++++ .../server/querydefn/TestQueryLanguage.java | 23 +- .../tests/autofetch/TunedQueryInfoTest.java | 2 +- .../avaje/tests/query/TestQueryParsing.java | 49 ++-- 21 files changed, 833 insertions(+), 313 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesParser.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/core/DefaultServer_createOrmQueryRequestTest.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesParserTest.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesTest.java diff --git a/src/main/java/com/avaje/ebean/text/PathProperties.java b/src/main/java/com/avaje/ebean/text/PathProperties.java index ad0d96f6a..5dac251b4 100644 --- a/src/main/java/com/avaje/ebean/text/PathProperties.java +++ b/src/main/java/com/avaje/ebean/text/PathProperties.java @@ -92,7 +92,7 @@ public class PathProperties { /** * Get the properties for a given path. */ - public Set get(String path) { + public LinkedHashSet get(String path) { Props props = pathMap.get(path); return props == null ? null : props.getProperties(); } @@ -109,7 +109,7 @@ public class PathProperties { /** * Set the properties for a given path. */ - public void put(String path, Set properties) { + public void put(String path, LinkedHashSet properties) { pathMap.put(path, new Props(this, null, path, properties)); } @@ -160,9 +160,9 @@ public class PathProperties { private final String parentPath; private final String path; - private final Set propSet; + private final LinkedHashSet propSet; - private Props(PathProperties owner, String parentPath, String path, Set propSet) { + private Props(PathProperties owner, String parentPath, String path, LinkedHashSet propSet) { this.owner = owner; this.path = path; this.parentPath = parentPath; @@ -195,7 +195,7 @@ public class PathProperties { /** * Return the properties for this property set. */ - public Set getProperties() { + public LinkedHashSet getProperties() { return propSet; } diff --git a/src/main/java/com/avaje/ebeaninternal/api/HashQueryPlanBuilder.java b/src/main/java/com/avaje/ebeaninternal/api/HashQueryPlanBuilder.java index 9365d6b20..c1f2ad696 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/HashQueryPlanBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/api/HashQueryPlanBuilder.java @@ -1,5 +1,7 @@ package com.avaje.ebeaninternal.api; +import java.util.Set; + /** * Used to build HashQueryPlan instances. */ @@ -33,6 +35,21 @@ public class HashQueryPlanBuilder { return this; } + /** + * Add the set with order being important. + */ + public HashQueryPlanBuilder addOrdered(Set set) { + if (set == null) { + add(false); + } else { + add(true); + for (Object o : set) { + add(o); + } + } + return this; + } + /** * Add an integer to the hash calculation. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index 65b6382f6..db55f32cf 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -1038,7 +1038,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return findId(query, t); } - private SpiOrmQueryRequest createQueryRequest(Type type, Query query, Transaction t) { + SpiOrmQueryRequest createQueryRequest(Type type, Query query, Transaction t) { SpiQuery spiQuery = (SpiQuery) query; spiQuery.setType(type); diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 85c5f06e5..fada56d65 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -73,9 +73,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -347,7 +347,7 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { private final BeanDescriptorJsonHelp jsonHelp; private final String defaultSelectClause; - private final Set defaultSelectClauseSet; + private final LinkedHashSet defaultSelectClauseSet; private SpiEbeanServer ebeanServer; @@ -856,7 +856,7 @@ public class BeanDescriptor implements MetaBeanInfo, SpiBeanType { /** * Return the default select clause already parsed into an ordered Set. */ - public Set getDefaultSelectClauseSet() { + public LinkedHashSet getDefaultSelectClauseSet() { return defaultSelectClauseSet; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index d8d630781..e8564a6e0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -33,7 +33,6 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; /** * Describes Beans including their deployment information. @@ -763,7 +762,7 @@ public class DeployBeanDescriptor { /** * Parse the include separating by comma or semicolon. */ - public Set parseDefaultSelectClause(String rawList) { + public LinkedHashSet parseDefaultSelectClause(String rawList) { if (rawList == null) { return null; @@ -780,7 +779,7 @@ public class DeployBeanDescriptor { set.add(temp); } } - return Collections.unmodifiableSet(set); + return set; } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/expression/Same.java b/src/main/java/com/avaje/ebeaninternal/server/expression/Same.java index 5f46e954b..05e483d94 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/expression/Same.java +++ b/src/main/java/com/avaje/ebeaninternal/server/expression/Same.java @@ -1,5 +1,8 @@ package com.avaje.ebeaninternal.server.expression; +import java.util.Collection; +import java.util.Iterator; + /** * Utility to help isSame methods. */ @@ -19,6 +22,26 @@ public class Same { return v1 == null ? v2 == null : v1.equals(v2); } + /** + * Return true if both collections are the same by value and order is taken into account. + */ + public static boolean sameByValue(Collection v1, Collection v2) { + if (v1 == null) { + return v2 == null; + } + if (v2 == null || v1.size() != v2.size()) { + return false; + } + Iterator thisIt = v1.iterator(); + Iterator thatIt = v2.iterator(); + while (thisIt.hasNext() && thatIt.hasNext()) { + if (!thisIt.next().equals(thatIt.next())) { + return false; + } + } + return true; + } + /** * Null safe check by sameByValue or sameByNull based on byValue. */ diff --git a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java index 2919e12d3..9f5bf3181 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/query/CQueryBuilder.java @@ -25,7 +25,6 @@ import com.avaje.ebeaninternal.server.querydefn.OrmQueryLimitRequest; import javax.persistence.PersistenceException; import java.util.Iterator; import java.util.List; -import java.util.Set; /** * Generates the SQL SELECT statements taking into account the physical @@ -354,8 +353,7 @@ public class CQueryBuilder { // transfer PathProperties into OrmQueryDetail for (String path : pathProps.getPaths()) { - Set props = pathProps.get(path); - detail.getChunk(path, true).setDefaultProperties(null, props); + detail.getChunk(path, true).setDefaultProperties(null, pathProps.get(path)); } // check if @Id property included in RawSql diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetail.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetail.java index bce3000e8..b9d643b67 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetail.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetail.java @@ -7,6 +7,7 @@ import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssoc; import com.avaje.ebeaninternal.server.el.ElPropertyDeploy; import com.avaje.ebeaninternal.server.el.ElPropertyValue; import com.avaje.ebeaninternal.server.query.SplitName; +import org.jetbrains.annotations.NotNull; import javax.persistence.PersistenceException; import java.io.Serializable; @@ -18,7 +19,6 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; /** * Represents the internal structure of an Object Relational query. @@ -38,6 +38,7 @@ public class OrmQueryDetail implements Serializable { /** * Root level properties. */ + @NotNull private OrmQueryProperties baseProps = new OrmQueryProperties(); /** @@ -60,17 +61,17 @@ public class OrmQueryDetail implements Serializable { return copy; } + public int queryPlanHash() { + HashQueryPlanBuilder builder = new HashQueryPlanBuilder(); + queryPlanHash(builder); + return builder.getPlanHash(); + } + /** * Calculate the hash for the query plan. */ public void queryPlanHash(HashQueryPlanBuilder builder) { - if (baseProps == null) { - builder.add(false); - } else { - builder.add(true); - baseProps.queryPlanHash(builder); - } - + baseProps.queryPlanHash(builder); if (fetchPaths != null) { for (OrmQueryProperties p : fetchPaths.values()) { p.queryPlanHash(builder); @@ -79,11 +80,10 @@ public class OrmQueryDetail implements Serializable { } /** - * Return true if equal in terms of autoTune (select and fetch). + * Return true if the details are the same for query plan purposes. */ - public boolean isAutoTuneEqual(OrmQueryDetail otherDetail) { - - if (!isSame(baseProps, otherDetail.baseProps)) { + public boolean isSameByPlan(OrmQueryDetail otherDetail) { + if (!isSameByPlan(baseProps, otherDetail.baseProps)) { return false; } if (fetchPaths == null) { @@ -92,10 +92,16 @@ public class OrmQueryDetail implements Serializable { if (fetchPaths.size() != otherDetail.fetchPaths.size()) { return false; } - Set> entries = fetchPaths.entrySet(); - for (Map.Entry entry : entries) { - OrmQueryProperties chunk = otherDetail.getChunk(entry.getKey(), false); - if (!isSame(entry.getValue(), chunk)) { + // check with ordering being important + Iterator> thisIt = fetchPaths.entrySet().iterator(); + Iterator> thatIt = otherDetail.fetchPaths.entrySet().iterator(); + while (thisIt.hasNext() && thatIt.hasNext()) { + Map.Entry thisEntry = thisIt.next(); + Map.Entry thatEntry = thatIt.next(); + if (!thisEntry.getKey().equals(thatEntry.getKey())) { + return false; + } + if (!thisEntry.getValue().isSameByPlan(thatEntry.getValue())) { return false; } } @@ -103,21 +109,54 @@ public class OrmQueryDetail implements Serializable { return true; } - private boolean isSame(OrmQueryProperties p1, OrmQueryProperties p2) { - if (p1 == null) { - return p2 == null; + /** + * Return true if equal in terms of autoTune (select and fetch without property ordering). + */ + public boolean isAutoTuneEqual(OrmQueryDetail otherDetail) { + + if (!isSameByAutoTune(baseProps, otherDetail.baseProps)) { + return false; } - return p1.isSame(p2); + if (fetchPaths == null) { + return otherDetail.fetchPaths == null; + } + if (fetchPaths.size() != otherDetail.fetchPaths.size()) { + return false; + } + // check without regard to ordering + for (Map.Entry entry : fetchPaths.entrySet()) { + OrmQueryProperties chunk = otherDetail.getChunk(entry.getKey(), false); + if (!isSameByAutoTune(entry.getValue(), chunk)) { + return false; + } + } + + return true; + } + + private boolean isSameByAutoTune(OrmQueryProperties p1, OrmQueryProperties p2) { + return p1 == null ? p2 == null : p1.isSameByAutoTune(p2); + } + + private boolean isSameByPlan(OrmQueryProperties p1, OrmQueryProperties p2) { + return p1 == null ? p2 == null : p1.isSameByPlan(p2); } public String toString() { + return asString(); + } + + /** + * Return the detail in string form. + */ + public String asString() { StringBuilder sb = new StringBuilder(); - if (baseProps != null) { - sb.append("select ").append(baseProps); + if (!baseProps.isEmpty()) { + baseProps.append("select ", sb); } if (fetchPaths != null) { for (OrmQueryProperties join : fetchPaths.values()) { - sb.append(" fetch ").append(join); + join.append(" fetch ", sb); } } return sb.toString(); @@ -135,7 +174,7 @@ public class OrmQueryDetail implements Serializable { } public boolean containsProperty(String property) { - return baseProps == null || baseProps.isIncluded(property); + return baseProps.isIncluded(property); } /** @@ -399,9 +438,6 @@ public class OrmQueryDetail implements Serializable { public void setDefaultSelectClause(BeanDescriptor desc) { if (desc.hasDefaultSelectClause() && !hasSelectClause()) { - if (baseProps == null) { - baseProps = new OrmQueryProperties(); - } baseProps.setDefaultProperties(desc.getDefaultSelectClause(), desc.getDefaultSelectClauseSet()); } @@ -417,14 +453,14 @@ public class OrmQueryDetail implements Serializable { } public boolean hasSelectClause() { - return (baseProps != null && baseProps.hasSelectClause()); + return (baseProps.hasSelectClause()); } /** * Return true if the query detail has neither select properties specified or any joins defined. */ public boolean isEmpty() { - return fetchPaths.isEmpty() && (baseProps == null || !baseProps.hasProperties()); + return fetchPaths.isEmpty() && (!baseProps.hasProperties()); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailParser.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailParser.java index 186e0ad90..a54d56da7 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailParser.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailParser.java @@ -29,6 +29,8 @@ public class OrmQueryDetailParser { public OrmQueryDetail parse() throws PersistenceException { + if (parser.isEmpty()) return detail; + parser.nextWord(); processInitial(); return detail; @@ -155,7 +157,7 @@ public class OrmQueryDetailParser { private void readSelect() { String path = null; String props = parser.nextWord(); - if (props.startsWith("(")) { + if (props != null && props.startsWith("(")) { props = props.substring(1, props.length() - 1); OrmQueryProperties base = new OrmQueryProperties(path, props); detail.setBase(base); diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryProperties.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryProperties.java index 467c99468..2d2b12c09 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryProperties.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryProperties.java @@ -10,13 +10,12 @@ import com.avaje.ebeaninternal.api.SpiExpressionFactory; import com.avaje.ebeaninternal.api.SpiExpressionList; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.expression.FilterExprPath; -import com.avaje.ebeaninternal.server.lib.util.StringHelper; -import com.avaje.ebeaninternal.server.query.SplitName; import com.avaje.ebeaninternal.server.expression.FilterExpressionList; +import com.avaje.ebeaninternal.server.expression.Same; +import com.avaje.ebeaninternal.server.query.SplitName; import java.io.Serializable; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -32,8 +31,7 @@ public class OrmQueryProperties implements Serializable { private String parentPath; private String path; - private String properties; - + private String rawProperties; private String trimmedProperties; /** @@ -57,7 +55,7 @@ public class OrmQueryProperties implements Serializable { * Note this SHOULD be a LinkedHashSet to preserve order of the properties. This is to make using * SqlSelect easier with predictable property/column ordering. */ - private Set included; + private LinkedHashSet included; /** * Included bean joins. @@ -92,8 +90,10 @@ public class OrmQueryProperties implements Serializable { this.parentPath = SplitName.parent(path); } + /** + * Construct for root so path (and parentPath) are null. + */ public OrmQueryProperties() { - this(null); } /** @@ -137,25 +137,17 @@ public class OrmQueryProperties implements Serializable { * This can include the +query and +lazy type hints. *

*/ - public void setProperties(String properties) { - this.properties = properties; - this.trimmedProperties = properties; - parseProperties(); + public void setProperties(String rawProperties) { - if (!isAllProperties()) { - Set parsed = parseIncluded(trimmedProperties); - if (parsed.contains("*")) { - this.included = null; - } else { - this.included = parsed; - } - } else { - this.included = null; - } - } + OrmQueryPropertiesParser.Response response = OrmQueryPropertiesParser.parse(rawProperties); - private boolean isAllProperties() { - return (trimmedProperties == null) || (trimmedProperties.length() == 0) || "*".equals(trimmedProperties); + this.rawProperties = rawProperties; + this.trimmedProperties = response.properties; + this.included = response.included; + this.lazyFetchBatch = response.lazyFetchBatch; + this.queryFetchBatch = response.queryFetchBatch; + this.cache = response.cache; + this.readOnly = response.readOnly; } /** @@ -204,8 +196,8 @@ public class OrmQueryProperties implements Serializable { /** * Set the properties from deployment default FetchTypes. */ - public void setDefaultProperties(String properties, Set included) { - this.properties = properties; + public void setDefaultProperties(String properties, LinkedHashSet included) { + this.rawProperties = properties; this.trimmedProperties = properties; this.included = included; } @@ -215,7 +207,7 @@ public class OrmQueryProperties implements Serializable { */ public void setTunedProperties(OrmQueryProperties tunedProperties) { if (tunedProperties.hasProperties()) { - this.properties = tunedProperties.properties; + this.rawProperties = tunedProperties.rawProperties; this.trimmedProperties = tunedProperties.trimmedProperties; this.included = tunedProperties.included; this.queryFetchBatch = Math.max(queryFetchBatch, tunedProperties.queryFetchBatch); @@ -264,7 +256,7 @@ public class OrmQueryProperties implements Serializable { OrmQueryProperties copy = new OrmQueryProperties(); copy.parentPath = parentPath; copy.path = path; - copy.properties = properties; + copy.rawProperties = rawProperties; copy.trimmedProperties = trimmedProperties; copy.cache = cache; copy.readOnly = readOnly; @@ -273,7 +265,7 @@ public class OrmQueryProperties implements Serializable { copy.lazyFetchBatch = lazyFetchBatch; copy.filterMany = filterMany; if (included != null) { - copy.included = new HashSet(included); + copy.included = new LinkedHashSet(included); } if (includedBeanJoin != null) { copy.includedBeanJoin = new HashSet(includedBeanJoin); @@ -290,17 +282,28 @@ public class OrmQueryProperties implements Serializable { return included != null; } + /** + * Return true if the properties and configuration are empty. + */ + public boolean isEmpty() { + return rawProperties == null || rawProperties.isEmpty(); + } + public String toString() { - String s = ""; + StringBuilder sb = new StringBuilder(40); + append("", sb); + return sb.toString(); + } + + public String append(String prefix, StringBuilder sb) { + sb.append(prefix); if (path != null) { - s += path + " "; + sb.append(path).append(" "); } - if (properties != null) { - s += "(" + properties + ") "; - } else if (included != null) { - s += "(" + included + ") "; + if (!isEmpty()) { + sb.append("(").append(rawProperties).append(") "); } - return s; + return sb.toString(); } public boolean isChild(OrmQueryProperties possibleChild) { @@ -318,35 +321,22 @@ public class OrmQueryProperties implements Serializable { } /** - * Calculate the query plan hash. + * Return the raw properties. */ - @SuppressWarnings("unchecked") - public void queryPlanHash(HashQueryPlanBuilder builder) { - - builder.add(path); - if (properties != null) { - builder.add(properties); - } else { - builder.add(included); - } - builder.add(filterMany != null); - if (filterMany != null) { - filterMany.queryPlanHash(builder); - } - builder.add(lazyFetchBatch); - builder.add(queryFetchBatch); - builder.add(queryFetchAll); - } - public String getProperties() { - return properties; + return rawProperties; } /** - * Return true if this has properties. + * Return true if this has properties. Returning false means this part of the + * path is a partial object. */ public boolean hasProperties() { - return properties != null || included != null; + return included != null; + } + + public boolean allProperties() { + return included == null; } /** @@ -370,10 +360,6 @@ public class OrmQueryProperties implements Serializable { includedBeanJoin.add(propertyName); } - public boolean allProperties() { - return included == null; - } - /** * This excludes the bean joined properties. *

@@ -386,7 +372,7 @@ public class OrmQueryProperties implements Serializable { return included; } - LinkedHashSet temp = new LinkedHashSet(secondaryQueryJoins.size() + included.size()); + LinkedHashSet temp = new LinkedHashSet(2 * (secondaryQueryJoins.size() + included.size())); temp.addAll(included); temp.addAll(secondaryQueryJoins); return temp; @@ -400,30 +386,10 @@ public class OrmQueryProperties implements Serializable { } /** - * Return all the properties including the bean joins. This is the set that will be used by - * EntityBeanIntercept to determine if a property needs to be lazy loaded. + * Return the property set. */ - public Set getAllIncludedProperties() { - - if (included == null) { - return null; - } - - if (includedBeanJoin == null && secondaryQueryJoins == null) { - return new LinkedHashSet(included); - } - - LinkedHashSet s = new LinkedHashSet(2 * (included.size() + 5)); - if (included != null) { - s.addAll(included); - } - if (includedBeanJoin != null) { - s.addAll(includedBeanJoin); - } - if (secondaryQueryJoins != null) { - s.addAll(secondaryQueryJoins); - } - return s; + protected Set getIncluded() { + return included; } public boolean isIncluded(String propName) { @@ -480,96 +446,47 @@ public class OrmQueryProperties implements Serializable { return path; } - private void parseProperties() { - if (trimmedProperties == null) { - return; - } - int pos = trimmedProperties.indexOf("+readonly"); - if (pos > -1) { - trimmedProperties = StringHelper.replaceString(trimmedProperties, "+readonly", ""); - this.readOnly = true; - } - pos = trimmedProperties.indexOf("+cache"); - if (pos > -1) { - trimmedProperties = StringHelper.replaceString(trimmedProperties, "+cache", ""); - this.cache = true; - } - pos = trimmedProperties.indexOf("+query"); - if (pos > -1) { - queryFetchBatch = parseBatchHint(pos, "+query"); - } - pos = trimmedProperties.indexOf("+lazy"); - if (pos > -1) { - lazyFetchBatch = parseBatchHint(pos, "+lazy"); - } - - trimmedProperties = trimmedProperties.trim(); - while (trimmedProperties.startsWith(",")) { - trimmedProperties = trimmedProperties.substring(1).trim(); - } - } - - private int parseBatchHint(int pos, String option) { - - int startPos = pos + option.length(); - - int endPos = findEndPos(startPos, trimmedProperties); - if (endPos == -1) { - trimmedProperties = StringHelper.replaceString(trimmedProperties, option, ""); - return 0; - - } else { - - String batchParam = trimmedProperties.substring(startPos + 1, endPos); - - if (endPos + 1 >= trimmedProperties.length()) { - trimmedProperties = trimmedProperties.substring(0, pos); - } else { - trimmedProperties = trimmedProperties.substring(0, pos) + trimmedProperties.substring(endPos + 1); - } - return Integer.parseInt(batchParam); - } - } - - private int findEndPos(int pos, String props) { - - if (pos < props.length()) { - if (props.charAt(pos) == '(') { - int endPara = props.indexOf(')', pos + 1); - if (endPara == -1) { - String m = "Error could not find ')' in " + props + " after position " + pos; - throw new RuntimeException(m); - } - return endPara; - } - } - return -1; - } - /** - * Parse the include separating by comma or semicolon. + * Return true if the properties are the same for autoTune purposes. */ - private static Set parseIncluded(String rawList) { - - String[] res = rawList.split(","); - - LinkedHashSet set = new LinkedHashSet(res.length + 3); - - String temp; - for (int i = 0; i < res.length; i++) { - temp = res[i].trim(); - if (temp.length() > 0) { - set.add(temp); - } - } - - return Collections.unmodifiableSet(set); - } - - public boolean isSame(OrmQueryProperties p2) { + public boolean isSameByAutoTune(OrmQueryProperties p2) { if (included == null) { return p2.included == null; } return included.equals(p2.included); } + + /** + * Properties are the same for query plan purposes. + */ + public boolean isSameByPlan(OrmQueryProperties p2) { + + if (!Same.sameByValue(secondaryQueryJoins, p2.secondaryQueryJoins)) return false; + if (!Same.sameByValue(included, p2.included)) return false; + if (!Same.sameByNull(filterMany, p2.filterMany)) return false; + if (filterMany != null && !filterMany.isSameByPlan(p2.filterMany)) return false; + + return lazyFetchBatch == p2.lazyFetchBatch + && queryFetchBatch == p2.queryFetchBatch + && queryFetchAll == p2.queryFetchAll; + } + + /** + * Calculate the query plan hash. + */ + public void queryPlanHash(HashQueryPlanBuilder builder) { + + builder.add(path); + builder.addOrdered(included); + builder.add(secondaryQueryJoins); + + builder.add(filterMany != null); + if (filterMany != null) { + filterMany.queryPlanHash(builder); + } + builder.add(lazyFetchBatch); + builder.add(queryFetchBatch); + builder.add(queryFetchAll); + } + } diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesParser.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesParser.java new file mode 100644 index 000000000..fce8b056d --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesParser.java @@ -0,0 +1,183 @@ +package com.avaje.ebeaninternal.server.querydefn; + +import com.avaje.ebeaninternal.server.lib.util.StringHelper; + +import java.util.LinkedHashSet; + +/** + * Parses the path properties string. + */ +public class OrmQueryPropertiesParser { + + private static Response EMPTY = new Response(); + + /** + * Immutable response of the parsed properties and options. + */ + public static class Response { + + final boolean readOnly; + final boolean cache; + final int queryFetchBatch; + final int lazyFetchBatch; + final String properties; + final LinkedHashSet included; + + public Response(boolean readOnly, boolean cache, int queryFetchBatch, int lazyFetchBatch, String properties, LinkedHashSet included) { + this.readOnly = readOnly; + this.cache = cache; + this.queryFetchBatch = queryFetchBatch; + this.lazyFetchBatch = lazyFetchBatch; + this.properties = properties; + this.included = included; + } + + public Response() { + this.readOnly = false; + this.cache = false; + this.queryFetchBatch = -1; + this.lazyFetchBatch = -1; + this.properties = ""; + this.included = null; + } + } + + /** + * Parses the path properties string returning the parsed properties and options. + * In general it is comma delimited with some special strings like +lazy(20). + */ + public static Response parse(String rawProperties) { + return new OrmQueryPropertiesParser(rawProperties).parse(); + } + + private String inputProperties; + + private String outputProperties = ""; + private boolean allProperties; + private boolean readOnly; + private boolean cache; + private int queryFetchBatch = -1; + private int lazyFetchBatch = -1; + + private OrmQueryPropertiesParser(String inputProperties) { + this.inputProperties = inputProperties; + } + + /** + * Parse the raw string properties input. + */ + private Response parse() { + + if (inputProperties == null || inputProperties.isEmpty()) { + return EMPTY; + } + int pos = inputProperties.indexOf("+readonly"); + if (pos > -1) { + inputProperties = StringHelper.replaceString(inputProperties, "+readonly", ""); + readOnly = true; + } + pos = inputProperties.indexOf("+cache"); + if (pos > -1) { + inputProperties = StringHelper.replaceString(inputProperties, "+cache", ""); + cache = true; + } + pos = inputProperties.indexOf("+query"); + if (pos > -1) { + queryFetchBatch = parseBatchHint(pos, "+query"); + } + pos = inputProperties.indexOf("+lazy"); + if (pos > -1) { + lazyFetchBatch = parseBatchHint(pos, "+lazy"); + } + + LinkedHashSet included = parseIncluded(); + String properties = (allProperties) ? "*" : outputProperties; + return new Response(readOnly, cache, queryFetchBatch, lazyFetchBatch, properties, included); + } + + /** + * Parse the include separating by comma or semicolon. + */ + private LinkedHashSet parseIncluded() { + + inputProperties = inputProperties.trim(); + if (inputProperties.isEmpty()) { + // default properties + return null; + } + if (inputProperties.equals("*")) { + // explicit all properties + allProperties = true; + return null; + } + + String[] res = inputProperties.split(","); + + StringBuilder sb = new StringBuilder(70); + LinkedHashSet propertySet = new LinkedHashSet(res.length * 2); + + int count = 0; + String temp; + for (int i = 0; i < res.length; i++) { + temp = res[i].trim(); + if (temp.length() > 0) { + if (count > 0) { + sb.append(","); + } + sb.append(temp); + propertySet.add(temp); + count++; + } + } + + if (propertySet.isEmpty()) { + // default properties + return null; + } + + if (propertySet.contains("*")) { + // explicit all properties + allProperties = true; + return null; + } + + // partial properties + outputProperties = sb.toString(); + return propertySet; + } + + private int parseBatchHint( int pos, String option) { + + int startPos = pos + option.length(); + int endPos = findEndPos(startPos, inputProperties); + if (endPos == -1) { + inputProperties = StringHelper.replaceString(inputProperties, option, ""); + return 0; + + } else { + + String batchParam = inputProperties.substring(startPos + 1, endPos); + + if (endPos + 1 >= inputProperties.length()) { + inputProperties = inputProperties.substring(0, pos); + } else { + inputProperties = inputProperties.substring(0, pos) + inputProperties.substring(endPos + 1); + } + return Integer.parseInt(batchParam); + } + } + + private int findEndPos(int pos, String props) { + + if (pos < props.length()) { + if (props.charAt(pos) == '(') { + int endPara = props.indexOf(')', pos + 1); + if (endPara == -1) { + throw new RuntimeException("Error could not find ')' in " + props + " after position " + pos); + } + return endPara; + } + } + return -1; + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/querydefn/SimpleTextParser.java b/src/main/java/com/avaje/ebeaninternal/server/querydefn/SimpleTextParser.java index d65a24f97..345503966 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/querydefn/SimpleTextParser.java +++ b/src/main/java/com/avaje/ebeaninternal/server/querydefn/SimpleTextParser.java @@ -16,6 +16,10 @@ public class SimpleTextParser { this.eof = oql.length(); } + public boolean isEmpty() { + return oql.isEmpty(); + } + public int getPos() { return pos; } diff --git a/src/test/java/com/avaje/ebeaninternal/server/core/DefaultServer_createOrmQueryRequestTest.java b/src/test/java/com/avaje/ebeaninternal/server/core/DefaultServer_createOrmQueryRequestTest.java new file mode 100644 index 000000000..fb98c62d8 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/core/DefaultServer_createOrmQueryRequestTest.java @@ -0,0 +1,126 @@ +package com.avaje.ebeaninternal.server.core; + + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.FetchConfig; +import com.avaje.ebean.Query; +import com.avaje.ebeaninternal.api.SpiQuery; +import com.avaje.ebeaninternal.server.querydefn.DefaultOrmQuery; +import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail; +import com.avaje.tests.model.basic.Order; +import org.junit.Test; + +import static org.assertj.core.api.StrictAssertions.assertThat; + +public class DefaultServer_createOrmQueryRequestTest extends BaseTestCase { + + static DefaultServer defaultServer = (DefaultServer)Ebean.getDefaultServer(); + + Query query() { + return defaultServer.find(Order.class); + } + + OrmQueryRequest queryRequest(Query query) { + return (OrmQueryRequest)defaultServer.createQueryRequest(SpiQuery.Type.LIST, query, null); + } + + OrmQueryDetail detail(Query query) { + return queryRequest(query).getQuery().getDetail(); + } + + @Test + public void when_empty_then_same() { + + assertSame(detail(query()), detail(query())); + } + + @Test + public void select_all_then_same() { + + assertSame(detail(query().select("*")), detail(query().select("*"))); + } + + @Test + public void when_propertiesInOrder_then_same() { + + assertSame(detail(query().select("id,name")), detail(query().select("id,name"))); + } + + @Test + public void when_propertiesInDifferentOrder_then_different() { + + assertDifferent(detail(query().select("id,name")), detail(query().select("name, id"))); + + // but same from autotune perspective + assertThat(detail(query().select("id,name")).isAutoTuneEqual(detail(query().select("name, id")))).isTrue(); + } + + @Test + public void when_additional_fetch_then_different() { + + assertDifferent(detail(query().select("id,name")), detail(query().select("id,name").fetch("details"))); + } + + @Test + public void when_same_fetch_then_same() { + + assertSame(detail(query().select("id,name").fetch("details")), detail(query().select("id,name").fetch("details"))); + } + + @Test + public void when_fetch_order_different_then_different() { + + assertDifferent(detail(query().select("id,name").fetch("details").fetch("customer")), + detail(query().select("id,name").fetch("customer").fetch("details"))); + } + + @Test + public void when_extra_queryFetchToMany_then_same() { + + assertDifferent(detail(query().select("id,name").fetch("customer")), + detail(query().select("id,name").fetch("customer").fetch("details", new FetchConfig().query()))); + } + + @Test + public void when_extra_queryToOne_fetch_then_different() { + + // with the fetch of customer the foreign key must be added to the root query + assertDifferent(detail(query().select("id,name")), + detail(query().select("id,name").fetch("customer",new FetchConfig().query()))); + } + + @Test + public void when_additional_fetch_V2_then_different() { + + assertDifferent(detail(query().select("id,name")), detail(query().select("id,name").fetch("customer","id"))); + } + + + @Test + public void when_fetchConfig_then_differentPlan() throws Exception { + + DefaultOrmQuery query1 = (DefaultOrmQuery)Ebean.find(Order.class) + .select("status, shipDate") + .fetch("details", "orderQty, unitPrice", new FetchConfig().query()) + .fetch("details.product", "sku, name"); + + + DefaultOrmQuery query2 = (DefaultOrmQuery)Ebean.find(Order.class) + .select("status, shipDate") + .fetch("details", "orderQty, unitPrice") + .fetch("details.product", "sku, name"); + + assertDifferent(detail(query1), detail(query2)); + } + + private void assertSame(OrmQueryDetail detail1, OrmQueryDetail detail2) { + assertThat(detail1.isSameByPlan(detail2)).isTrue(); + assertThat(detail1.queryPlanHash()).isEqualTo(detail2.queryPlanHash()); + } + + private void assertDifferent(OrmQueryDetail detail1, OrmQueryDetail detail2) { + assertThat(detail1.isSameByPlan(detail2)).isFalse(); + assertThat(detail1.queryPlanHash()).isNotEqualTo(detail2.queryPlanHash()); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java b/src/test/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java index 50b4f558a..d3016e706 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java @@ -40,24 +40,6 @@ public class DefaultOrmQueryTest { assertThat(q1.queryBindHash()).isEqualTo(q2.queryBindHash()); } - - @Test - public void when_FetchConfig_then_differentPlan() throws Exception { - - DefaultOrmQuery query1 = (DefaultOrmQuery)Ebean.find(Order.class) - .select("status, shipDate") - .fetch("details", "orderQty, unitPrice", new FetchConfig().query()) - .fetch("details.product", "sku, name"); - - - DefaultOrmQuery query2 = (DefaultOrmQuery)Ebean.find(Order.class) - .select("status, shipDate") - .fetch("details", "orderQty, unitPrice") - .fetch("details.product", "sku, name"); - - assertThat(query1.createQueryPlanKey()).isNotEqualTo(query2.createQueryPlanKey()); - } - @Test public void when_diffFirstMaxRows_then_differentPlan() throws Exception { diff --git a/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailParserTest.java b/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailParserTest.java index 0e877728c..3be9f7943 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailParserTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailParserTest.java @@ -5,114 +5,111 @@ import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; public class OrmQueryDetailParserTest extends BaseTestCase { + OrmQueryDetail parse(String query) { + return new OrmQueryDetailParser(query).parse(); + } + + @Test(expected = NullPointerException.class) + public void parse_when_nullString() throws Exception { + parse(null); + } + + @Test + public void parse_when_emptyString() throws Exception { + assertTrue(parse("").isEmpty()); + } + @Test public void testParseBasic() throws Exception { + OrmQueryDetail detail = parse("select (id,name)"); - OrmQueryDetail other = new OrmQueryDetail(); - other.select("id,name"); - - OrmQueryProperties root = other.getChunk(null, false); + OrmQueryProperties root = detail.getChunk(null, false); assertNull(root.getPath()); - assertThat(root.getAllIncludedProperties()).contains("id", "name"); - - OrmQueryDetailParser p = new OrmQueryDetailParser("select (id,name)"); - OrmQueryDetail detail = p.parse(); - - root = detail.getChunk(null, false); - assertNull(root.getPath()); - assertThat(root.getAllIncludedProperties()).contains("id", "name"); + assertThat(root.getIncluded()).containsExactly("id", "name"); } @Test public void testParseEmptySelect() throws Exception { - OrmQueryDetailParser p = new OrmQueryDetailParser("select fetch customer (email)"); - OrmQueryDetail detail = p.parse(); + OrmQueryDetail detail = parse("select fetch customer (email)"); OrmQueryProperties root = detail.getChunk(null, false); assertNull(root.getPath()); - assertThat(root.getAllIncludedProperties()).isNull(); + assertThat(root.getIncluded()).isNull(); OrmQueryProperties chunk = detail.getChunk("customer", false); assertThat(chunk.getPath()).isEqualTo("customer"); - assertThat(chunk.getAllIncludedProperties()).contains("email"); + assertThat(chunk.getIncluded()).contains("email"); } @Test public void testParseSelectFetch() throws Exception { - OrmQueryDetailParser p = new OrmQueryDetailParser("select (id,name) fetch customer (email)"); - OrmQueryDetail detail = p.parse(); + OrmQueryDetail detail = parse("select (id,name) fetch customer (email)"); OrmQueryProperties root = detail.getChunk(null, false); assertNull(root.getPath()); - assertThat(root.getAllIncludedProperties()).contains("id", "name"); + assertThat(root.getIncluded()).contains("id", "name"); OrmQueryProperties chunk = detail.getChunk("customer", false); assertThat(chunk.getPath()).isEqualTo("customer"); - assertThat(chunk.getAllIncludedProperties()).contains("email"); + assertThat(chunk.getIncluded()).contains("email"); } @Test public void testParseSelectFetchMore() throws Exception { - OrmQueryDetailParser p = new OrmQueryDetailParser("select (id,name) fetch customer (email) fetch details.product (sku,description)"); - OrmQueryDetail detail = p.parse(); + OrmQueryDetail detail = parse("select (id,name) fetch customer (email) fetch details.product (sku,description)"); OrmQueryProperties root = detail.getChunk(null, false); assertNull(root.getPath()); - assertThat(root.getAllIncludedProperties()).contains("id", "name"); + assertThat(root.getIncluded()).contains("id", "name"); OrmQueryProperties chunk = detail.getChunk("customer", false); assertThat(chunk.getPath()).isEqualTo("customer"); - assertThat(chunk.getAllIncludedProperties()).contains("email"); + assertThat(chunk.getIncluded()).contains("email"); chunk = detail.getChunk("details.product", false); assertThat(chunk.getPath()).isEqualTo("details.product"); - assertThat(chunk.getAllIncludedProperties()).contains("sku","description"); + assertThat(chunk.getIncluded()).contains("sku","description"); } @Test public void testParseWithPlusQuery() throws Exception { - OrmQueryDetailParser p = new OrmQueryDetailParser("select (id,name) fetch customer (+query,id,name,email)"); - OrmQueryDetail detail = p.parse(); + OrmQueryDetail detail = parse("select (id,name) fetch customer (+query,id,name,email)"); OrmQueryProperties root = detail.getChunk(null, false); assertNull(root.getPath()); - assertThat(root.getAllIncludedProperties()).contains("id", "name"); + assertThat(root.getIncluded()).contains("id", "name"); OrmQueryProperties chunk = detail.getChunk("customer", false); assertThat(chunk.getPath()).isEqualTo("customer"); - assertThat(chunk.getAllIncludedProperties()).contains("id", "name", "email"); + assertThat(chunk.getIncluded()).contains("id", "name", "email"); assertThat(chunk.isQueryFetch()).isTrue(); - } @Test public void testTuneApply() { - OrmQueryDetailParser p = new OrmQueryDetailParser("select (status) fetch customer (email)"); - OrmQueryDetail detail = p.parse(); - - OrmQueryDetailParser p2 = new OrmQueryDetailParser("select (id,name) fetch customer (+query,id,name,email)"); - OrmQueryDetail tune = p2.parse(); - + OrmQueryDetail detail = parse("select (status) fetch customer (email)"); + OrmQueryDetail tune = parse("select (id,name) fetch customer (+query,id,name,email)"); detail.tuneFetchProperties(tune); OrmQueryProperties root = detail.getChunk(null, false); assertNull(root.getPath()); - assertThat(root.getAllIncludedProperties()).contains("id", "name"); + assertThat(root.getIncluded()).contains("id", "name"); OrmQueryProperties chunk = detail.getChunk("customer", false); assertThat(chunk.getPath()).isEqualTo("customer"); - assertThat(chunk.getAllIncludedProperties()).contains("id", "name", "email"); + assertThat(chunk.getIncluded()).contains("id", "name", "email"); assertThat(chunk.isQueryFetch()).isTrue(); } diff --git a/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailTest.java b/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailTest.java index 3fa127b73..b12516431 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryDetailTest.java @@ -2,20 +2,70 @@ package com.avaje.ebeaninternal.server.querydefn; import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; public class OrmQueryDetailTest { + OrmQueryDetail parse(String query) { + return new OrmQueryDetailParser(query).parse(); + } @Test - public void test_isAutoTuneEqual() { + public void isAutoTuneEqual_when_fetchOrderIsDifferent_then_stillEqual() { - OrmQueryDetailParser parser1 = new OrmQueryDetailParser("select (id,name) fetch customer (name) fetch details (code)"); - OrmQueryDetail detail1 = parser1.parse(); - - OrmQueryDetailParser parser2 = new OrmQueryDetailParser("select (id,name) fetch details (code) fetch customer (name)"); - OrmQueryDetail detail2 = parser2.parse(); + OrmQueryDetail detail1 = parse("select (id,name) fetch customer (name) fetch details (code)"); + OrmQueryDetail detail2 = parse("select (id,name) fetch details (code) fetch customer (name)"); assertTrue(detail1.isAutoTuneEqual(detail2)); } + + @Test + public void isAutoTuneEqual_when_different_select() { + + OrmQueryDetail detail1 = parse("select (id,name) fetch customer (name) fetch details (code)"); + OrmQueryDetail detail2 = parse("select (id) fetch details (code) fetch customer (name)"); + + assertFalse(detail1.isAutoTuneEqual(detail2)); + } + + @Test + public void isAutoTuneEqual_when_different_selectInFetch() { + + OrmQueryDetail detail1 = parse("select (id,name) fetch customer (name) fetch details (code)"); + OrmQueryDetail detail2 = parse("select (id,name) fetch customer (id,name) fetch details (code)"); + + assertFalse(detail1.isAutoTuneEqual(detail2)); + } + + @Test + public void isAutoTuneEqual_when_different_additionalFetch() { + + OrmQueryDetail detail1 = parse("select (id,name) fetch customer (name) fetch details (code)"); + OrmQueryDetail detail2 = parse("select (id,name) fetch customer (name) fetch details (code) fetch customer.contacts"); + + assertFalse(detail1.isAutoTuneEqual(detail2)); + } + + @Test + public void select_whenMultiple() throws Exception { + + OrmQueryDetail other = new OrmQueryDetail(); + other.select("id,name"); + + OrmQueryProperties root = other.getChunk(null, false); + assertNull(root.getPath()); + assertThat(root.getIncluded()).containsExactly("id", "name"); + } + + @Test + public void select_whenOne() throws Exception { + + OrmQueryDetail other = new OrmQueryDetail(); + other.select("name"); + + OrmQueryProperties root = other.getChunk(null, false); + assertNull(root.getPath()); + assertThat(root.getIncluded()).containsExactly("name"); + } } \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesParserTest.java b/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesParserTest.java new file mode 100644 index 000000000..ab519e1bb --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesParserTest.java @@ -0,0 +1,132 @@ +package com.avaje.ebeaninternal.server.querydefn; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OrmQueryPropertiesParserTest { + + @Test + public void when_null() { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse(null); + assertAllDefaults(res); + assertThat(res.properties).isEqualTo(""); + } + + @Test + public void when_empty() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse(""); + assertAllDefaults(res); + assertThat(res.properties).isEqualTo(""); + } + + @Test + public void when_hasStar() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("*"); + assertAllDefaults(res); + assertThat(res.properties).isEqualTo("*"); + } + + @Test + public void when_hasCache() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("+cache"); + assertThat(res.cache).isTrue(); + assertThat(res.included).isNull(); + } + + @Test + public void when_hasCache_first() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("+cache,id"); + assertThat(res.cache).isTrue(); + assertThat(res.included).containsExactly("id"); + } + + @Test + public void when_hasCache_last() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("name,+cache"); + assertThat(res.cache).isTrue(); + assertThat(res.included).containsExactly("name"); + } + + @Test + public void when_hasCache_middle() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("name,+cache, id"); + assertThat(res.cache).isTrue(); + assertThat(res.included).containsExactly("name", "id"); + } + + @Test + public void when_hasReadOnly() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("+readonly"); + assertThat(res.readOnly).isTrue(); + assertThat(res.included).isNull(); + } + + @Test + public void when_hasLazy() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("+lazy"); + assertThat(res.lazyFetchBatch).isEqualTo(0); + assertThat(res.included).isNull(); + } + + @Test + public void when_hasLazyValue() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("+lazy(20)"); + assertThat(res.lazyFetchBatch).isEqualTo(20); + assertThat(res.included).isNull(); + } + + @Test + public void when_hasLazyValue_last() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("name,+lazy(20)"); + assertThat(res.lazyFetchBatch).isEqualTo(20); + assertThat(res.included).containsExactly("name"); + } + + @Test + public void when_hasLazyValue_first() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("+lazy(20),id,name"); + assertThat(res.lazyFetchBatch).isEqualTo(20); + assertThat(res.included).containsExactly("id", "name"); + } + + @Test + public void when_allProperties() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("+query(4),+lazy(5)"); + assertThat(res.lazyFetchBatch).isEqualTo(5); + assertThat(res.included).isNull(); + } + + + @Test + public void when_everything_set() throws Exception { + + OrmQueryPropertiesParser.Response res = OrmQueryPropertiesParser.parse("id, name +readonly +lazy(20) +query(30) +cache"); + assertThat(res.lazyFetchBatch).isEqualTo(20); + assertThat(res.queryFetchBatch).isEqualTo(30); + assertThat(res.readOnly).isTrue(); + assertThat(res.cache).isTrue(); + assertThat(res.included).containsExactly("id", "name"); + } + + private void assertAllDefaults(OrmQueryPropertiesParser.Response res) { + assertThat(res.cache).isFalse(); + assertThat(res.readOnly).isFalse(); + assertThat(res.lazyFetchBatch).isEqualTo(-1); + assertThat(res.queryFetchBatch).isEqualTo(-1); + assertThat(res.included).isNull(); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesTest.java b/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesTest.java new file mode 100644 index 000000000..b1cc03494 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/querydefn/OrmQueryPropertiesTest.java @@ -0,0 +1,50 @@ +package com.avaje.ebeaninternal.server.querydefn; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OrmQueryPropertiesTest { + + String append(String prefix, OrmQueryProperties p1) { + StringBuilder sb = new StringBuilder(); + p1.append(prefix, sb); + return sb.toString(); + } + + @Test + public void append_when_empty() { + + OrmQueryProperties p1 = new OrmQueryProperties(); + assertThat(append("select ",p1)).isEqualTo("select "); + } + + @Test + public void append_when_someProperties() { + + OrmQueryProperties p1 = new OrmQueryProperties(null, "id,name"); + assertThat(append("select ",p1)).isEqualTo("select (id,name) "); + } + + @Test + public void append_when_somePropertiesWithOptions() { + + OrmQueryProperties p1 = new OrmQueryProperties(null, "id,name +cache"); + assertThat(append("select ",p1)).isEqualTo("select (id,name +cache) "); + } + + @Test + public void append_when_path_and_emptyProperties() { + + OrmQueryProperties p1 = new OrmQueryProperties("customer", ""); + assertThat(append("fetch ",p1)).isEqualTo("fetch customer "); + } + + @Test + public void append_when_path_and_somePropertiesWithOptions() { + + OrmQueryProperties p1 = new OrmQueryProperties("customer", "id,name +cache"); + assertThat(append("fetch ",p1)).isEqualTo("fetch customer (id,name +cache) "); + } + +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/querydefn/TestQueryLanguage.java b/src/test/java/com/avaje/ebeaninternal/server/querydefn/TestQueryLanguage.java index fb0cc9a26..36f9abb03 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/querydefn/TestQueryLanguage.java +++ b/src/test/java/com/avaje/ebeaninternal/server/querydefn/TestQueryLanguage.java @@ -1,15 +1,14 @@ package com.avaje.ebeaninternal.server.querydefn; -import java.util.Set; - -import org.junit.Assert; -import org.junit.Test; - import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; import com.avaje.ebean.EbeanServer; import com.avaje.ebeaninternal.server.expression.DefaultExpressionFactory; import com.avaje.tests.model.basic.Order; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Set; public class TestQueryLanguage extends BaseTestCase { @@ -19,7 +18,7 @@ public class TestQueryLanguage extends BaseTestCase { DefaultOrmQuery q = check("find order join customer (id, name)"); OrmQueryDetail detail = q.getDetail(); OrmQueryProperties chunk = detail.getChunk("customer", false); - Set props = chunk.getAllIncludedProperties(); + Set props = chunk.getIncluded(); Assert.assertTrue(props.contains("id")); Assert.assertTrue(props.contains("name")); @@ -27,7 +26,7 @@ public class TestQueryLanguage extends BaseTestCase { q = check("find order join customer(id, name)"); detail = q.getDetail(); chunk = detail.getChunk("customer", false); - props = chunk.getAllIncludedProperties(); + props = chunk.getIncluded(); Assert.assertTrue(props.contains("id")); Assert.assertTrue(props.contains("name")); @@ -37,7 +36,7 @@ public class TestQueryLanguage extends BaseTestCase { q = check("find order join customer(+cache +readonly, id, name)"); detail = q.getDetail(); chunk = detail.getChunk("customer", false); - props = chunk.getAllIncludedProperties(); + props = chunk.getIncluded(); Assert.assertTrue(props.contains("id")); Assert.assertTrue(props.contains("name")); Assert.assertTrue(chunk.isCache()); @@ -46,7 +45,7 @@ public class TestQueryLanguage extends BaseTestCase { q = check("find order join customer(+cache +readonly,id,name)"); detail = q.getDetail(); chunk = detail.getChunk("customer", false); - props = chunk.getAllIncludedProperties(); + props = chunk.getIncluded(); Assert.assertTrue(props.contains("id")); Assert.assertTrue(props.contains("name")); Assert.assertTrue(chunk.isCache()); @@ -55,14 +54,14 @@ public class TestQueryLanguage extends BaseTestCase { q = check("find order(id,status) join customer(+cache +readonly,id,name)"); detail = q.getDetail(); chunk = detail.getChunk("customer", false); - props = chunk.getAllIncludedProperties(); + props = chunk.getIncluded(); Assert.assertTrue(props.contains("id")); Assert.assertTrue(props.contains("name")); Assert.assertTrue(chunk.isCache()); Assert.assertTrue(chunk.isReadOnly()); chunk = detail.getChunk(null, false); - props = chunk.getAllIncludedProperties(); + props = chunk.getIncluded(); Assert.assertTrue(props.contains("id")); Assert.assertTrue(props.contains("status")); Assert.assertFalse(props.contains("orderDate")); @@ -70,7 +69,7 @@ public class TestQueryLanguage extends BaseTestCase { q = check("find order(id,status) join customer(+cache +readonly,id,name) where id > :minId order by status"); detail = q.getDetail(); chunk = detail.getChunk("customer", false); - props = chunk.getAllIncludedProperties(); + props = chunk.getIncluded(); Assert.assertTrue(props.contains("id")); Assert.assertTrue(props.contains("name")); Assert.assertTrue(chunk.isCache()); diff --git a/src/test/java/com/avaje/tests/autofetch/TunedQueryInfoTest.java b/src/test/java/com/avaje/tests/autofetch/TunedQueryInfoTest.java index 99a0ead5f..2d0cc5fcb 100644 --- a/src/test/java/com/avaje/tests/autofetch/TunedQueryInfoTest.java +++ b/src/test/java/com/avaje/tests/autofetch/TunedQueryInfoTest.java @@ -101,7 +101,7 @@ public class TunedQueryInfoTest extends BaseTestCase { @NotNull private TunedQueryInfo createTunedQueryInfo(OrmQueryDetail tunedDetail) { Origin origin = new Origin(); - origin.setDetail(tunedDetail.toString()); + origin.setDetail(tunedDetail.asString()); return new TunedQueryInfo(origin); } diff --git a/src/test/java/com/avaje/tests/query/TestQueryParsing.java b/src/test/java/com/avaje/tests/query/TestQueryParsing.java index 3b7c35d68..626dec929 100644 --- a/src/test/java/com/avaje/tests/query/TestQueryParsing.java +++ b/src/test/java/com/avaje/tests/query/TestQueryParsing.java @@ -1,37 +1,40 @@ package com.avaje.tests.query; -import org.junit.Assert; -import org.junit.Test; - import com.avaje.ebean.BaseTestCase; import com.avaje.ebean.Ebean; import com.avaje.ebean.Query; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.querydefn.OrmQueryDetail; import com.avaje.tests.model.basic.Order; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class TestQueryParsing extends BaseTestCase { @Test public void test() { - String oq = "find order join customer join customer.contacts join details (+query(4),+lazy(5))"; + String oq = "find order join customer (id, name) join customer.contacts join details (+query(4),+lazy(5))"; Query q = Ebean.createQuery(Order.class, oq); checkQuery(q); - String oq1 = "find order join customer join customer.contacts join details ( +query(4), +lazy(5) )"; + String oq1 = "find order join customer (id, name) join customer.contacts join details ( +query(4), +lazy(5) )"; SpiQuery q1 = (SpiQuery) Ebean.createQuery(Order.class, oq1); checkQuery(q1); - String oq2 = "find order join customer join customer.contacts join details ( +query(4), +lazy(5) , *)"; + String oq2 = "find order join customer (id, name) join customer.contacts join details ( +query(4), +lazy(5) , *)"; SpiQuery q2 = (SpiQuery) Ebean.createQuery(Order.class, oq2); checkQuery(q2); - String oq3 = "find order join customer join customer.contacts join details (+query(4),+lazy(5),*)"; + String oq3 = "find order join customer (id, name) join customer.contacts join details (+query(4),+lazy(5),*)"; SpiQuery q3 = (SpiQuery) Ebean.createQuery(Order.class, oq3); checkQuery(q3); - String oq4 = "find order join customer join customer.contacts join details (+query(4) +lazy(5) *)"; + String oq4 = "find order join customer (id, name) join customer.contacts join details (+query(4) +lazy(5) *)"; SpiQuery q4 = (SpiQuery) Ebean.createQuery(Order.class, oq4); checkQuery(q4); @@ -41,23 +44,25 @@ public class TestQueryParsing extends BaseTestCase { SpiQuery sq = (SpiQuery) q; OrmQueryDetail detail = sq.getDetail(); + assertTrue(detail.getChunk(null, false).allProperties()); - Assert.assertNotNull(detail.getChunk("customer", false)); - Assert.assertFalse(detail.getChunk("customer", false).isQueryFetch()); - Assert.assertFalse(detail.getChunk("customer", false).isLazyFetch()); + assertNotNull(detail.getChunk("customer", false)); + assertFalse(detail.getChunk("customer", false).isQueryFetch()); + assertFalse(detail.getChunk("customer", false).isLazyFetch()); + assertFalse(detail.getChunk("customer", false).allProperties()); + assertEquals("id, name",detail.getChunk("customer", false).getProperties()); - Assert.assertNotNull(detail.getChunk("customer.contacts", false)); - Assert.assertFalse(detail.getChunk("customer.contacts", false).isQueryFetch()); - Assert.assertFalse(detail.getChunk("customer.contacts", false).isLazyFetch()); + assertNotNull(detail.getChunk("customer.contacts", false)); + assertFalse(detail.getChunk("customer.contacts", false).isQueryFetch()); + assertFalse(detail.getChunk("customer.contacts", false).isLazyFetch()); + assertTrue(detail.getChunk("customer.contacts", false).allProperties()); - Assert.assertNotNull(detail.getChunk("details", false)); - Assert.assertTrue(detail.getChunk("details", false).isQueryFetch()); - Assert.assertTrue(detail.getChunk("details", false).isLazyFetch()); - - Assert.assertEquals(4, detail.getChunk("details", false).getQueryFetchBatch()); - Assert.assertEquals(5, detail.getChunk("details", false).getLazyFetchBatch()); - - Assert.assertTrue(detail.getChunk("details", false).allProperties()); + assertNotNull(detail.getChunk("details", false)); + assertTrue(detail.getChunk("details", false).isQueryFetch()); + assertTrue(detail.getChunk("details", false).isLazyFetch()); + assertEquals(4, detail.getChunk("details", false).getQueryFetchBatch()); + assertEquals(5, detail.getChunk("details", false).getLazyFetchBatch()); + assertTrue(detail.getChunk("details", false).allProperties()); }