diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/FilterPredicate.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/FilterPredicate.java
index 31a99d228e..6921090eb7 100644
--- a/elide-core/src/main/java/com/yahoo/elide/core/filter/FilterPredicate.java
+++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/FilterPredicate.java
@@ -97,21 +97,34 @@ public FilterPredicate scopedBy(PathElement scope) {
}
/**
- * Returns an alias that uniquely identifies the last collection of entities in the path.
- * @return An alias for the path.
+ * Generate alias for representing a relationship path which dose not include the last field name.
+ * The path would start with the class alias of the first element, and then each field would append "_fieldName" to
+ * the result.
+ * The last field would not be included as that's not a part of the relationship path.
+ *
+ * @param path path that represents a relationship chain
+ * @return relationship path alias, i.e. foo.bar.baz
would be foo_bar
*/
- public String getAlias() {
- List elements = path.getPathElements();
+ public static String getPathAlias(Path path) {
+ List elements = path.getPathElements();
+ String alias = getTypeAlias(elements.get(0).getType());
- PathElement last = elements.get(elements.size() - 1);
-
- if (elements.size() == 1) {
- return getTypeAlias(last.getType());
+ for (int i = 0; i < elements.size() - 1; i++) {
+ alias = appendAlias(alias, elements.get(i).getFieldName());
}
- PathElement previous = elements.get(elements.size() - 2);
+ return alias;
+ }
- return getTypeAlias(previous.getType()) + UNDERSCORE + previous.getFieldName();
+ /**
+ * Append a new field to a parent alias to get new alias.
+ *
+ * @param parentAlias parent path alias
+ * @param fieldName field name
+ * @return alias for the field
+ */
+ public static String appendAlias(String parentAlias, String fieldName) {
+ return parentAlias + "_" + fieldName;
}
/**
diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreTransaction.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreTransaction.java
index 38684465fc..fd6daed82b 100644
--- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreTransaction.java
+++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreTransaction.java
@@ -62,10 +62,10 @@ public void close() throws IOException {
}
@VisibleForTesting
- Query buildQuery(EntityProjection entityProjection, RequestScope scope) {
+ private Query buildQuery(EntityProjection entityProjection, RequestScope scope) {
Table table = queryEngine.getTable(entityProjection.getType());
-
- AggregationDataStoreHelper agHelper = new AggregationDataStoreHelper(table, entityProjection);
- return agHelper.getQuery();
+ EntityProjectionTranslator translator = new EntityProjectionTranslator(table,
+ entityProjection, scope.getDictionary());
+ return translator.getQuery();
}
}
diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreHelper.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/EntityProjectionTranslator.java
similarity index 66%
rename from elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreHelper.java
rename to elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/EntityProjectionTranslator.java
index 347dc0ddf7..86ed3dfd7b 100644
--- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreHelper.java
+++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/EntityProjectionTranslator.java
@@ -5,13 +5,9 @@
*/
package com.yahoo.elide.datastores.aggregation;
-import com.yahoo.elide.core.Path;
+import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.core.exceptions.InvalidOperationException;
-import com.yahoo.elide.core.filter.FilterPredicate;
-import com.yahoo.elide.core.filter.expression.AndFilterExpression;
import com.yahoo.elide.core.filter.expression.FilterExpression;
-import com.yahoo.elide.core.filter.expression.NotFilterExpression;
-import com.yahoo.elide.core.filter.expression.OrFilterExpression;
import com.yahoo.elide.datastores.aggregation.filter.visitor.FilterConstraints;
import com.yahoo.elide.datastores.aggregation.filter.visitor.SplitFilterExpressionVisitor;
import com.yahoo.elide.datastores.aggregation.metadata.metric.MetricFunctionInvocation;
@@ -24,6 +20,7 @@
import com.yahoo.elide.datastores.aggregation.query.Query;
import com.yahoo.elide.datastores.aggregation.query.TimeDimensionProjection;
import com.yahoo.elide.request.Argument;
+import com.yahoo.elide.request.Attribute;
import com.yahoo.elide.request.EntityProjection;
import com.yahoo.elide.request.Relationship;
@@ -39,27 +36,28 @@
/**
* Helper for Aggregation Data Store which does the work associated with extracting {@link Query}.
*/
-public class AggregationDataStoreHelper {
+public class EntityProjectionTranslator {
private AnalyticView queriedTable;
+
private EntityProjection entityProjection;
private Set dimensionProjections;
private Set timeDimensions;
private List metrics;
private FilterExpression whereFilter;
private FilterExpression havingFilter;
+ private EntityDictionary dictionary;
- public AggregationDataStoreHelper(Table table, EntityProjection entityProjection) {
+ public EntityProjectionTranslator(Table table, EntityProjection entityProjection, EntityDictionary dictionary) {
if (!(table instanceof AnalyticView)) {
throw new InvalidOperationException("Queried table is not analyticView: " + table.getName());
}
this.queriedTable = (AnalyticView) table;
this.entityProjection = entityProjection;
-
+ this.dictionary = dictionary;
dimensionProjections = resolveNonTimeDimensions();
timeDimensions = resolveTimeDimensions();
metrics = resolveMetrics();
-
splitFilters();
}
@@ -68,7 +66,7 @@ public AggregationDataStoreHelper(Table table, EntityProjection entityProjection
* @return {@link Query} query object with all the parameters provided by user.
*/
public Query getQuery() {
- return Query.builder()
+ Query query = Query.builder()
.analyticView(queriedTable)
.metrics(metrics)
.groupByDimensions(dimensionProjections)
@@ -78,6 +76,9 @@ public Query getQuery() {
.sorting(entityProjection.getSorting())
.pagination(entityProjection.getPagination())
.build();
+ QueryValidator validator = new QueryValidator(query, getAllFields(), dictionary);
+ validator.validate();
+ return query;
}
/**
@@ -94,10 +95,6 @@ private void splitFilters() {
FilterConstraints constraints = filterExpression.accept(visitor);
whereFilter = constraints.getWhereExpression();
havingFilter = constraints.getHavingExpression();
-
- if (havingFilter != null) {
- validateHavingClause(havingFilter);
- }
}
/**
@@ -117,10 +114,10 @@ private Set resolveTimeDimensions() {
.findAny()
.orElse(null);
- TimeDimensionGrain grain;
+ TimeDimensionGrain resolvedGrain;
if (grainArgument == null) {
//The first grain is the default.
- grain = timeDim.getSupportedGrains().stream()
+ resolvedGrain = timeDim.getSupportedGrains().stream()
.findFirst()
.orElseThrow(() -> new IllegalStateException(
String.format("Requested default grain, no grain defined on %s",
@@ -128,15 +125,17 @@ private Set resolveTimeDimensions() {
} else {
String requestedGrainName = grainArgument.getValue().toString();
- grain = timeDim.getSupportedGrains().stream()
- .filter(g ->
- g.getGrain().name().toLowerCase(Locale.ENGLISH).equals(requestedGrainName))
+ resolvedGrain = timeDim.getSupportedGrains().stream()
+ .filter(supportedGrain -> supportedGrain.getGrain().name().toLowerCase(Locale.ENGLISH)
+ .equals(requestedGrainName))
.findFirst()
.orElseThrow(() -> new InvalidOperationException(
- String.format("Invalid grain %s", requestedGrainName)));
+ String.format("Unsupported grain %s for field %s",
+ requestedGrainName,
+ timeDimAttr.getName())));
}
- return ColumnProjection.toProjection(timeDim, grain.getGrain(), timeDimAttr.getAlias());
+ return ColumnProjection.toProjection(timeDim, resolvedGrain.getGrain(), timeDimAttr.getAlias());
})
.collect(Collectors.toCollection(LinkedHashSet::new));
}
@@ -187,55 +186,21 @@ private Set getRelationships() {
}
/**
- * Validate the having clause before execution. Having clause is not as flexible as where clause,
- * the fields in having clause must be either or these two:
- * 1. A grouped by dimension in this query
- * 2. An aggregated metric in this query
- *
- * All grouped by dimensions are defined in the entity bean, so the last entity class of a filter path
- * must match entity class of the query.
- *
- * @param havingClause having clause generated from this query
+ * Gets attribute names from {@link EntityProjection}.
+ * @return relationships list of {@link Attribute} names
*/
- private void validateHavingClause(FilterExpression havingClause) {
- // TODO: support having clause for alias
- if (havingClause instanceof FilterPredicate) {
- Path path = ((FilterPredicate) havingClause).getPath();
- Path.PathElement last = path.lastElement().get();
- Class> cls = last.getType();
- String fieldName = last.getFieldName();
-
- if (cls != queriedTable.getCls()) {
- throw new InvalidOperationException(
- String.format(
- "Classes don't match when try filtering on %s in having clause of %s.",
- cls.getSimpleName(),
- queriedTable.getCls().getSimpleName()));
- }
-
- if (queriedTable.isMetric(fieldName)) {
- if (metrics.stream().noneMatch(m -> m.getAlias().equals(fieldName))) {
- throw new InvalidOperationException(
- String.format(
- "Metric field %s must be aggregated before filtering in having clause.",
- fieldName));
- }
- } else {
- if (dimensionProjections.stream().noneMatch(dim -> dim.getAlias().equals(fieldName))) {
- throw new InvalidOperationException(
- String.format(
- "Dimension field %s must be grouped before filtering in having clause.",
- fieldName));
- }
- }
- } else if (havingClause instanceof AndFilterExpression) {
- validateHavingClause(((AndFilterExpression) havingClause).getLeft());
- validateHavingClause(((AndFilterExpression) havingClause).getRight());
- } else if (havingClause instanceof OrFilterExpression) {
- validateHavingClause(((OrFilterExpression) havingClause).getLeft());
- validateHavingClause(((OrFilterExpression) havingClause).getRight());
- } else if (havingClause instanceof NotFilterExpression) {
- validateHavingClause(((NotFilterExpression) havingClause).getNegated());
- }
+ private Set getAttributes() {
+ return entityProjection.getAttributes().stream()
+ .map(Attribute::getName).collect(Collectors.toCollection(LinkedHashSet::new));
+ }
+
+ /**
+ * Helper method to get all field names from the {@link EntityProjection}.
+ * @return allFields set of all field names
+ */
+ private Set getAllFields() {
+ Set allFields = getAttributes();
+ allFields.addAll(getRelationships());
+ return allFields;
}
}
diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/QueryValidator.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/QueryValidator.java
new file mode 100644
index 0000000000..bc7b00f242
--- /dev/null
+++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/QueryValidator.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2019, Yahoo Inc.
+ * Licensed under the Apache License, Version 2.0
+ * See LICENSE file in project root for terms.
+ */
+package com.yahoo.elide.datastores.aggregation;
+
+import com.yahoo.elide.core.EntityDictionary;
+import com.yahoo.elide.core.Path;
+import com.yahoo.elide.core.exceptions.InvalidOperationException;
+import com.yahoo.elide.core.filter.FilterPredicate;
+import com.yahoo.elide.core.filter.expression.AndFilterExpression;
+import com.yahoo.elide.core.filter.expression.FilterExpression;
+import com.yahoo.elide.core.filter.expression.NotFilterExpression;
+import com.yahoo.elide.core.filter.expression.OrFilterExpression;
+import com.yahoo.elide.core.sort.Sorting;
+import com.yahoo.elide.datastores.aggregation.metadata.metric.MetricFunctionInvocation;
+import com.yahoo.elide.datastores.aggregation.metadata.models.AnalyticView;
+import com.yahoo.elide.datastores.aggregation.query.ColumnProjection;
+import com.yahoo.elide.datastores.aggregation.query.Query;
+
+import org.apache.commons.collections.CollectionUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class that checks whether a constructed {@link Query} object can be executed.
+ * Checks include validate sorting, having clause and make sure there is at least 1 metric queried.
+ */
+public class QueryValidator {
+ private Query query;
+ private Set allFields;
+ private EntityDictionary dictionary;
+ private AnalyticView queriedTable;
+ private Class> queriedClass;
+ private List metrics;
+ private Set dimensionProjections;
+
+ public QueryValidator(Query query, Set allFields, EntityDictionary dictionary) {
+ this.query = query;
+ this.allFields = allFields;
+ this.dictionary = dictionary;
+ this.queriedTable = query.getAnalyticView();
+ this.queriedClass = queriedTable.getCls();
+ this.metrics = query.getMetrics();
+ this.dimensionProjections = query.getDimensions();
+ }
+
+ /**
+ * Method that handles all checks to make sure query is valid before we attempt to execute the query.
+ */
+ public void validate() {
+ validateHavingClause(query.getHavingFilter());
+ validateSorting();
+ validateMetricFunction();
+ }
+
+ /**
+ * Checks to make sure at least one metric is being aggregated on.
+ */
+ private void validateMetricFunction() {
+ if (CollectionUtils.isEmpty(metrics)) {
+ throw new InvalidOperationException("Must provide at least one metric in query");
+ }
+ }
+
+ /**
+ * Validate the having clause before execution. Having clause is not as flexible as where clause,
+ * the fields in having clause must be either or these two:
+ * 1. A grouped by dimension in this query
+ * 2. An aggregated metric in this query
+ *
+ * All grouped by dimensions are defined in the entity bean, so the last entity class of a filter path
+ * must match entity class of the query.
+ *
+ * @param havingClause having clause generated from this query
+ */
+ private void validateHavingClause(FilterExpression havingClause) {
+ // TODO: support having clause for alias
+ if (havingClause instanceof FilterPredicate) {
+ Path path = ((FilterPredicate) havingClause).getPath();
+ Path.PathElement last = path.lastElement().get();
+ Class> cls = last.getType();
+ String fieldName = last.getFieldName();
+
+ if (cls != queriedTable.getCls()) {
+ throw new InvalidOperationException(
+ String.format(
+ "Can't filter on relationship field %s in HAVING clause when querying table %s.",
+ path.toString(),
+ queriedTable.getCls().getSimpleName()));
+ }
+
+ if (queriedTable.isMetric(fieldName)) {
+ if (metrics.stream().noneMatch(m -> m.getAlias().equals(fieldName))) {
+ throw new InvalidOperationException(
+ String.format(
+ "Metric field %s must be aggregated before filtering in having clause.",
+ fieldName));
+ }
+ } else {
+ if (dimensionProjections.stream().noneMatch(dim -> dim.getAlias().equals(fieldName))) {
+ throw new InvalidOperationException(
+ String.format(
+ "Dimension field %s must be grouped before filtering in having clause.",
+ fieldName));
+ }
+ }
+ } else if (havingClause instanceof AndFilterExpression) {
+ validateHavingClause(((AndFilterExpression) havingClause).getLeft());
+ validateHavingClause(((AndFilterExpression) havingClause).getRight());
+ } else if (havingClause instanceof OrFilterExpression) {
+ validateHavingClause(((OrFilterExpression) havingClause).getLeft());
+ validateHavingClause(((OrFilterExpression) havingClause).getRight());
+ } else if (havingClause instanceof NotFilterExpression) {
+ validateHavingClause(((NotFilterExpression) havingClause).getNegated());
+ }
+ }
+
+ /**
+ * Method to verify that all the sorting options provided
+ * by the user are valid and supported.
+ */
+ public void validateSorting() {
+ Sorting sorting = query.getSorting();
+ if (sorting == null) {
+ return;
+ }
+ Map sortClauses = sorting.getValidSortingRules(queriedClass, dictionary);
+ sortClauses.keySet().forEach((path) -> validateSortingPath(path, allFields));
+ }
+
+ /**
+ * Verifies that the current path can be sorted on
+ * @param path The path that we are validating
+ * @param allFields Set of all field names included in initial query
+ */
+ private void validateSortingPath(Path path, Set allFields) {
+ List pathElements = path.getPathElements();
+
+ // TODO: add support for double nested sorting
+ if (pathElements.size() > 2) {
+ throw new UnsupportedOperationException(
+ "Currently sorting on double nested fields is not supported");
+ }
+ Path.PathElement currentElement = pathElements.get(0);
+ String currentField = currentElement.getFieldName();
+ Class> currentClass = currentElement.getType();
+
+ // TODO: support sorting using alias
+ if (allFields.stream().noneMatch(field -> field.equals(currentField))) {
+ throw new InvalidOperationException("Can't sort on " + currentField + " as it is not present in query");
+ }
+ if (dictionary.getIdFieldName(currentClass).equals(currentField)
+ || currentField.equals(EntityDictionary.REGULAR_ID_NAME)) {
+ throw new InvalidOperationException("Sorting on id field is not permitted");
+ }
+ }
+}
diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java
index 50b5fcb612..22b06e3fd8 100644
--- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java
+++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java
@@ -13,6 +13,7 @@
import lombok.EqualsAndHashCode;
import java.util.Arrays;
+import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TimeZone;
import java.util.stream.Collectors;
@@ -38,6 +39,6 @@ public TimeDimension(Class> tableClass, String fieldName, EntityDictionary dic
this.supportedGrains = Arrays.stream(temporal.grains())
.map(grain -> new TimeDimensionGrain(getId(), grain))
- .collect(Collectors.toSet());
+ .collect(Collectors.toCollection(LinkedHashSet::new));
}
}
diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryConstructor.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryConstructor.java
index 2de27eba1f..d422bc93b4 100644
--- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryConstructor.java
+++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryConstructor.java
@@ -5,6 +5,9 @@
*/
package com.yahoo.elide.datastores.aggregation.queryengines.sql;
+import static com.yahoo.elide.core.filter.FilterPredicate.appendAlias;
+import static com.yahoo.elide.core.filter.FilterPredicate.getTypeAlias;
+import static com.yahoo.elide.datastores.aggregation.queryengines.sql.SQLQueryEngine.generateColumnReference;
import static com.yahoo.elide.datastores.aggregation.queryengines.sql.SQLQueryEngine.getClassAlias;
import com.yahoo.elide.core.EntityDictionary;
@@ -32,6 +35,7 @@
import org.hibernate.annotations.Subselect;
import java.util.Collection;
+import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -71,7 +75,7 @@ public SQLQuery resolveTemplate(Query clientQuery,
SQLQuery.SQLQueryBuilder builder = SQLQuery.builder().clientQuery(clientQuery);
- Set joinPredicates = new LinkedHashSet<>();
+ Set joinPaths = new HashSet<>();
String tableStatement = tableCls.isAnnotationPresent(FromSubquery.class)
? "(" + tableCls.getAnnotation(FromSubquery.class).sql() + ")"
@@ -87,34 +91,32 @@ public SQLQuery resolveTemplate(Query clientQuery,
if (!groupByDimensions.isEmpty()) {
builder.groupByClause(constructGroupByWithReference(groupByDimensions, queriedTable));
- joinPredicates.addAll(extractPathElements(groupByDimensions, queriedTable));
+
+ joinPaths.addAll(extractJoinPaths(groupByDimensions, queriedTable));
}
if (whereClause != null) {
- joinPredicates.addAll(extractPathElements(whereClause));
- builder.whereClause("WHERE " + translateFilterExpression(
- whereClause,
- this::generateColumnReference));
+ builder.whereClause("WHERE " + translateFilterExpression(whereClause, this::generatePredicateReference));
+
+ joinPaths.addAll(extractJoinPaths(whereClause));
}
if (havingClause != null) {
- joinPredicates.addAll(extractPathElements(havingClause));
builder.havingClause("HAVING " + translateFilterExpression(
havingClause,
(predicate) -> constructHavingClauseWithReference(predicate, queriedTable, template)));
+
+ joinPaths.addAll(extractJoinPaths(havingClause));
}
if (sorting != null) {
Map sortClauses = sorting.getValidSortingRules(tableCls, dictionary);
- builder.orderByClause(extractOrderBy(tableCls, sortClauses));
- joinPredicates.addAll(extractPathElements(sortClauses));
- }
+ builder.orderByClause(extractOrderBy(sortClauses, template));
- String joinClause = joinPredicates.stream()
- .map(this::extractJoin)
- .collect(Collectors.joining(" "));
+ joinPaths.addAll(extractJoinPaths(sortClauses));
+ }
- builder.joinClause(joinClause);
+ builder.joinClause(extractJoin(joinPaths));
return builder.build();
}
@@ -161,7 +163,7 @@ private String constructHavingClauseWithReference(FilterPredicate predicate,
if (metric != null) {
return metric.getFunctionExpression();
} else {
- return generateColumnReference(predicate);
+ return generatePredicateReference(predicate);
}
}
@@ -203,42 +205,87 @@ private String constructProjectionWithReference(SQLQueryTemplate template, SQLAn
}
/**
- * Given one component of the path taken to reach a particular field, extracts any table
- * joins that are required to perform the traversal to the field.
+ * Build full join clause for all join paths.
*
- * @param pathElement A field or relationship traversal from an entity
- * @return A SQL JOIN expression
+ * @param joinPaths paths that require joins
+ * @return built join clause that contains all needed relationship dimension joins for this query.
*/
- private String extractJoin(Path.PathElement pathElement) {
+ private String extractJoin(Set joinPaths) {
+ Set joinClauses = new LinkedHashSet<>();
+
+ joinPaths.forEach(path -> addJoinClauses(path, joinClauses));
+
+ return String.join(" ", joinClauses);
+ }
+
+ /**
+ * Add a join clause to a set of join clauses.
+ *
+ * @param joinPath join path
+ * @param alreadyJoined A set of joins that have already been computed.
+ */
+ private void addJoinClauses(Path joinPath, Set alreadyJoined) {
+ String parentAlias = getTypeAlias(joinPath.getPathElements().get(0).getType());
+
+ for (Path.PathElement pathElement : joinPath.getPathElements()) {
+ String fieldName = pathElement.getFieldName();
+ Class> parentClass = pathElement.getType();
+
+ // Nothing left to join.
+ if (! dictionary.isRelation(parentClass, fieldName)) {
+ return;
+ }
+
+ String joinFragment = extractJoinClause(
+ parentClass,
+ parentAlias,
+ pathElement.getFieldType(),
+ fieldName);
+
+ alreadyJoined.add(joinFragment);
+
+ parentAlias = appendAlias(parentAlias, fieldName);
+ }
+ }
+
+ /**
+ * Build a single dimension join clause for joining a relationship table to the parent table.
+ *
+ * @param parentClass parent class
+ * @param parentAlias parent table alias
+ * @param relationshipClass relationship class
+ * @param relationshipName relationship field name
+ * @return built join clause i.e. LEFT JOIN table1 AS dimension1 ON table0.dim_id = dimension1.id
+ */
+ private String extractJoinClause(Class> parentClass,
+ String parentAlias,
+ Class> relationshipClass,
+ String relationshipName) {
//TODO - support composite join keys.
//TODO - support joins where either side owns the relationship.
//TODO - Support INNER and RIGHT joins.
//TODO - Support toMany joins.
- Class> entityClass = pathElement.getType();
- String entityAlias = FilterPredicate.getTypeAlias(entityClass);
- Class> relationshipClass = pathElement.getFieldType();
- String relationshipAlias = FilterPredicate.getTypeAlias(relationshipClass);
- String relationshipName = pathElement.getFieldName();
- String relationshipColumnName = dictionary.getAnnotatedColumnName(entityClass, relationshipName);
+ String relationshipAlias = appendAlias(parentAlias, relationshipName);
+ String relationshipColumnName = dictionary.getAnnotatedColumnName(parentClass, relationshipName);
// resolve the right hand side of JOIN
String joinSource = constructTableOrSubselect(relationshipClass);
JoinTo joinTo = dictionary.getAttributeOrRelationAnnotation(
- entityClass,
+ parentClass,
JoinTo.class,
relationshipColumnName);
String joinClause = joinTo == null
? String.format("%s.%s = %s.%s",
- entityAlias,
+ parentAlias,
relationshipColumnName,
relationshipAlias,
dictionary.getAnnotatedColumnName(
relationshipClass,
dictionary.getIdFieldName(relationshipClass)))
- : extractJoinExpression(joinTo.joinClause(), entityAlias, relationshipAlias);
+ : extractJoinExpression(joinTo.joinClause(), parentAlias, relationshipAlias);
return String.format("LEFT JOIN %s AS %s ON %s",
joinSource,
@@ -261,29 +308,36 @@ private String constructTableOrSubselect(Class> cls) {
/**
* Given a list of columns to sort on, constructs an ORDER BY clause in SQL.
- *
- * @param entityClass The class to sort.
* @param sortClauses The list of sort columns and their sort order (ascending or descending).
* @return A SQL expression
*/
- private String extractOrderBy(Class> entityClass, Map sortClauses) {
+ private String extractOrderBy(Map sortClauses, SQLQueryTemplate template) {
if (sortClauses.isEmpty()) {
return "";
}
//TODO - Ensure that order by columns are also present in the group by.
+
return " ORDER BY " + sortClauses.entrySet().stream()
.map((entry) -> {
- Path path = entry.getKey();
- path = expandJoinToPath(path);
+ Path expandedPath = expandJoinToPath(entry.getKey());
Sorting.SortOrder order = entry.getValue();
- Path.PathElement last = path.lastElement().get();
+ Path.PathElement last = expandedPath.lastElement().get();
+
+ MetricFunctionInvocation metric = template.getMetrics().stream()
+ // TODO: filter predicate should support alias
+ .filter(invocation -> invocation.getAlias().equals(last.getFieldName()))
+ .findFirst()
+ .orElse(null);
- return getClassAlias(last.getType()) + "."
- + dictionary.getAnnotatedColumnName(entityClass, last.getFieldName())
- + (order.equals(Sorting.SortOrder.desc) ? " DESC" : " ASC");
- }).collect(Collectors.joining(","));
+ String orderByClause = metric == null
+ ? generateColumnReference(expandedPath, dictionary)
+ : metric.getFunctionExpression();
+
+ return orderByClause + (order.equals(Sorting.SortOrder.desc) ? " DESC" : " ASC");
+ })
+ .collect(Collectors.joining(","));
}
/**
@@ -294,18 +348,18 @@ private String extractOrderBy(Class> entityClass, Map
* @return The expanded path.
*/
private Path expandJoinToPath(Path path) {
- List pathElements = path.getPathElements();
- Path.PathElement pathElement = pathElements.get(0);
+ Path.PathElement pathRoot = path.getPathElements().get(0);
+
+ Class> entityClass = pathRoot.getType();
+ String fieldName = pathRoot.getFieldName();
- Class> type = pathElement.getType();
- String fieldName = pathElement.getFieldName();
- JoinTo joinTo = dictionary.getAttributeOrRelationAnnotation(type, JoinTo.class, fieldName);
+ JoinTo joinTo = dictionary.getAttributeOrRelationAnnotation(entityClass, JoinTo.class, fieldName);
if (joinTo == null || joinTo.path().equals("")) {
return path;
}
- return new Path(pathElement.getType(), dictionary, joinTo.path());
+ return new Path(entityClass, dictionary, joinTo.path());
}
/**
@@ -314,15 +368,13 @@ private Path expandJoinToPath(Path path) {
* @param expression The filter expression
* @return A set of path elements that capture a relationship traversal.
*/
- private Set extractPathElements(FilterExpression expression) {
+ private Set extractJoinPaths(FilterExpression expression) {
Collection predicates = expression.accept(new PredicateExtractionVisitor());
return predicates.stream()
.map(FilterPredicate::getPath)
.map(this::expandJoinToPath)
.filter(path -> path.getPathElements().size() > 1)
- .map(this::extractPathElements)
- .flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
@@ -332,11 +384,9 @@ private Set extractPathElements(FilterExpression expression) {
* @param sortClauses The list of sort columns and their sort order (ascending or descending).
* @return A set of path elements that capture a relationship traversal.
*/
- private Set extractPathElements(Map sortClauses) {
+ private Set extractJoinPaths(Map sortClauses) {
return sortClauses.keySet().stream()
.map(this::expandJoinToPath)
- .map(this::extractPathElements)
- .flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
@@ -348,25 +398,11 @@ private Set extractPathElements(Map s
* @param queriedTable queried analytic view
* @return A set of path elements that capture a relationship traversal.
*/
- private Set extractPathElements(Set groupByDimensions,
- SQLAnalyticView queriedTable) {
+ private Set extractJoinPaths(Set groupByDimensions,
+ SQLAnalyticView queriedTable) {
return resolveSQLColumns(groupByDimensions, queriedTable).stream()
.filter((dim) -> dim.getJoinPath() != null)
.map(SQLColumn::getJoinPath)
- .map(this::extractPathElements)
- .flatMap(Collection::stream)
- .collect(Collectors.toCollection(LinkedHashSet::new));
- }
-
- /**
- * Given a path , extracts any entity relationship traversals that require joins.
- *
- * @param path The path
- * @return A set of path elements that capture a relationship traversal.
- */
- private Set extractPathElements(Path path) {
- return path.getPathElements().stream()
- .filter((p) -> dictionary.isRelation(p.getType(), p.getFieldName()))
.collect(Collectors.toCollection(LinkedHashSet::new));
}
@@ -390,28 +426,8 @@ private String translateFilterExpression(FilterExpression expression,
* @param predicate The predicate to convert
* @return A SQL fragment that references a database column
*/
- private String generateColumnReference(FilterPredicate predicate) {
- return generateColumnReference(predicate.getPath());
- }
-
- /**
- * Converts a filter predicate path into a SQL WHERE/HAVING clause column reference.
- *
- * @param path The predicate path to convert
- * @return A SQL fragment that references a database column
- */
- private String generateColumnReference(Path path) {
- Path.PathElement last = path.lastElement().get();
- Class> lastClass = last.getType();
- String fieldName = last.getFieldName();
-
- JoinTo joinTo = dictionary.getAttributeOrRelationAnnotation(lastClass, JoinTo.class, fieldName);
-
- if (joinTo == null) {
- return getClassAlias(lastClass) + "." + dictionary.getAnnotatedColumnName(lastClass, last.getFieldName());
- } else {
- return generateColumnReference(new Path(lastClass, dictionary, joinTo.path()));
- }
+ private String generatePredicateReference(FilterPredicate predicate) {
+ return generateColumnReference(predicate.getPath(), dictionary);
}
/**
@@ -467,8 +483,7 @@ private static String resolveTableOrSubselect(EntityDictionary dictionary, Class
return dictionary.getAnnotation(cls, Subselect.class).value();
}
} else {
- javax.persistence.Table table =
- dictionary.getAnnotation(cls, javax.persistence.Table.class);
+ javax.persistence.Table table = dictionary.getAnnotation(cls, javax.persistence.Table.class);
if (table != null) {
return resolveTableAnnotation(table);
diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java
index e35f1dba98..cbeb3a5f58 100644
--- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java
+++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java
@@ -5,7 +5,10 @@
*/
package com.yahoo.elide.datastores.aggregation.queryengines.sql;
+import static com.yahoo.elide.core.filter.FilterPredicate.getPathAlias;
+
import com.yahoo.elide.core.EntityDictionary;
+import com.yahoo.elide.core.Path;
import com.yahoo.elide.core.TimedFunction;
import com.yahoo.elide.core.exceptions.InvalidPredicateException;
import com.yahoo.elide.core.filter.FilterPredicate;
@@ -19,6 +22,7 @@
import com.yahoo.elide.datastores.aggregation.query.ColumnProjection;
import com.yahoo.elide.datastores.aggregation.query.Query;
import com.yahoo.elide.datastores.aggregation.query.TimeDimensionProjection;
+import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.JoinTo;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.metadata.SQLAnalyticView;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.metadata.SQLColumn;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.metadata.SQLTable;
@@ -216,7 +220,7 @@ private SQLQuery toPageTotalSQL(SQLQuery sql) {
String groupByDimensions =
extractSQLDimensions(sql.getClientQuery(), (SQLAnalyticView) sql.getClientQuery().getAnalyticView())
.stream()
- .map(SQLColumn::getColumnName)
+ .map(SQLColumn::getReference)
.collect(Collectors.joining(", "));
String projectionClause = String.format("COUNT(DISTINCT(%s))", groupByDimensions);
@@ -243,6 +247,29 @@ private List extractSQLDimensions(Query query, SQLAnalyticView querie
.collect(Collectors.toList());
}
+ /**
+ * Converts a filter predicate path into a SQL column reference.
+ * All other code should use this method to generate sql column reference, no matter where the reference is used (
+ * select statement, group by clause, where clause, having clause or order by clause).
+ *
+ * @param path The predicate path to convert
+ * @param dictionary dictionary to expand joinTo path
+ * @return A SQL fragment that references a database column
+ */
+ public static String generateColumnReference(Path path, EntityDictionary dictionary) {
+ Path.PathElement last = path.lastElement().get();
+ Class> lastClass = last.getType();
+ String fieldName = last.getFieldName();
+
+ JoinTo joinTo = dictionary.getAttributeOrRelationAnnotation(lastClass, JoinTo.class, fieldName);
+
+ if (joinTo == null) {
+ return getPathAlias(path) + "." + dictionary.getAnnotatedColumnName(lastClass, last.getFieldName());
+ } else {
+ return generateColumnReference(new Path(lastClass, dictionary, joinTo.path()), dictionary);
+ }
+ }
+
/**
* Get alias for an entity class.
*
diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLColumn.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLColumn.java
index 821e647e9f..2eac1ea9a2 100644
--- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLColumn.java
+++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLColumn.java
@@ -5,6 +5,7 @@
*/
package com.yahoo.elide.datastores.aggregation.queryengines.sql.metadata;
+import static com.yahoo.elide.datastores.aggregation.queryengines.sql.SQLQueryEngine.generateColumnReference;
import static com.yahoo.elide.datastores.aggregation.queryengines.sql.SQLQueryEngine.getClassAlias;
import com.yahoo.elide.core.EntityDictionary;
@@ -19,10 +20,7 @@
*/
public class SQLColumn extends Column {
@Getter
- private final String columnName;
-
- @Getter
- private final String tableAlias;
+ private final String reference;
@Getter
private final Path joinPath;
@@ -33,43 +31,12 @@ protected SQLColumn(Class> tableClass, String fieldName, EntityDictionary dict
JoinTo joinTo = dictionary.getAttributeOrRelationAnnotation(tableClass, JoinTo.class, fieldName);
if (joinTo == null || joinTo.path().equals("")) {
- this.columnName = dictionary.getAnnotatedColumnName(tableClass, fieldName);
- this.tableAlias = getClassAlias(tableClass);
+ this.reference = getClassAlias(tableClass) + "." + dictionary.getAnnotatedColumnName(tableClass, fieldName);
this.joinPath = null;
} else {
Path path = new Path(tableClass, dictionary, joinTo.path());
- this.columnName = resolveJoinColumn(dictionary, path);
- this.tableAlias = getJoinTableAlias(path);
+ this.reference = generateColumnReference(path, dictionary);
this.joinPath = path;
}
}
-
- private static String getJoinTableAlias(Path path) {
- Path.PathElement last = path.lastElement().get();
- Class> lastClass = last.getType();
-
- return getClassAlias(lastClass);
- }
-
- /**
- * Returns a String that identifies this column in a physical sql table/view.
- *
- * @return e.g. table_alias.column_name
- */
- public String getReference() {
- return getTableAlias() + "." + getColumnName();
- }
-
- /**
- * Maps a logical entity path into a physical SQL column name.
- *
- * @param path entity join path
- * @return joined column name
- */
- public static String resolveJoinColumn(EntityDictionary dictionary, Path path) {
- Path.PathElement last = path.lastElement().get();
- Class> lastClass = last.getType();
-
- return dictionary.getAnnotatedColumnName(lastClass, last.getFieldName());
- }
}
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/EntityProjectionTranslatorTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/EntityProjectionTranslatorTest.java
new file mode 100644
index 0000000000..6078874b1e
--- /dev/null
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/EntityProjectionTranslatorTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2020, Yahoo Inc.
+ * Licensed under the Apache License, Version 2.0
+ * See LICENSE file in project root for terms.
+ */
+package com.yahoo.elide.datastores.aggregation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.yahoo.elide.core.exceptions.InvalidOperationException;
+import com.yahoo.elide.core.filter.dialect.ParseException;
+import com.yahoo.elide.core.filter.expression.FilterExpression;
+import com.yahoo.elide.datastores.aggregation.example.Country;
+import com.yahoo.elide.datastores.aggregation.example.PlayerStats;
+import com.yahoo.elide.datastores.aggregation.filter.visitor.FilterConstraints;
+import com.yahoo.elide.datastores.aggregation.filter.visitor.SplitFilterExpressionVisitor;
+import com.yahoo.elide.datastores.aggregation.framework.SQLUnitTest;
+import com.yahoo.elide.datastores.aggregation.metadata.models.Table;
+import com.yahoo.elide.datastores.aggregation.query.ColumnProjection;
+import com.yahoo.elide.datastores.aggregation.query.Query;
+import com.yahoo.elide.datastores.aggregation.query.TimeDimensionProjection;
+import com.yahoo.elide.datastores.aggregation.time.TimeGrain;
+import com.yahoo.elide.request.Argument;
+import com.yahoo.elide.request.Attribute;
+import com.yahoo.elide.request.EntityProjection;
+import com.yahoo.elide.request.Relationship;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class EntityProjectionTranslatorTest extends SQLUnitTest {
+ private static EntityProjection basicProjection = EntityProjection.builder()
+ .type(PlayerStats.class)
+ .attribute(Attribute.builder()
+ .type(long.class)
+ .name("lowScore")
+ .build())
+ .attribute(Attribute.builder()
+ .type(String.class)
+ .name("overallRating")
+ .build())
+ .relationship(Relationship.builder()
+ .name("country")
+ .projection(EntityProjection.builder()
+ .type(Country.class)
+ .attribute(Attribute.builder()
+ .type(String.class)
+ .name("name")
+ .build())
+ .build())
+ .build())
+ .build();
+
+ @BeforeAll
+ public static void init() {
+ SQLUnitTest.init();
+ }
+
+ @Test
+ public void testBasicTranslation() {
+ EntityProjectionTranslator translator = new EntityProjectionTranslator(
+ playerStatsTable,
+ basicProjection,
+ dictionary
+ );
+
+ Query query = translator.getQuery();
+
+ assertEquals(playerStatsTable, query.getAnalyticView());
+ assertEquals(1, query.getMetrics().size());
+ assertEquals("lowScore", query.getMetrics().get(0).getAlias());
+ assertEquals(2, query.getGroupByDimensions().size());
+
+ List dimensions = new ArrayList<>(query.getGroupByDimensions());
+ assertEquals("overallRating", dimensions.get(0).getColumn().getName());
+ assertEquals("country", dimensions.get(1).getColumn().getName());
+ }
+
+ @Test
+ public void testWherePromotion() throws ParseException {
+ FilterExpression originalFilter = filterParser.parseFilterExpression("overallRating==Good,lowScore<45",
+ PlayerStats.class, false);
+
+ EntityProjection projection = basicProjection.copyOf()
+ .filterExpression(originalFilter)
+ .build();
+
+ EntityProjectionTranslator translator = new EntityProjectionTranslator(
+ playerStatsTable,
+ projection,
+ dictionary
+ );
+
+ Query query = translator.getQuery();
+
+ SplitFilterExpressionVisitor visitor = new SplitFilterExpressionVisitor(playerStatsTable);
+ FilterConstraints constraints = originalFilter.accept(visitor);
+ FilterExpression whereFilter = constraints.getWhereExpression();
+ FilterExpression havingFilter = constraints.getHavingExpression();
+ assertEquals(whereFilter, query.getWhereFilter());
+ assertEquals(havingFilter, query.getHavingFilter());
+ }
+
+ @Test
+ public void testInvalidQueriedTable() {
+ EntityProjection projection = EntityProjection.builder()
+ .type(Country.class)
+ .build();
+
+ assertThrows(InvalidOperationException.class, () -> new EntityProjectionTranslator(
+ new Table(Country.class, dictionary),
+ projection,
+ dictionary
+ ));
+ }
+
+ @Test
+ public void testTimeDimension() {
+ EntityProjection projection = basicProjection.copyOf()
+ .attribute(Attribute.builder()
+ .type(Date.class)
+ .name("recordedDate")
+ .build())
+ .build();
+
+ EntityProjectionTranslator translator = new EntityProjectionTranslator(
+ playerStatsTable,
+ projection,
+ dictionary
+ );
+
+ Query query = translator.getQuery();
+
+ List timeDimensions = new ArrayList<>(query.getTimeDimensions());
+ assertEquals(1, timeDimensions.size());
+ assertEquals("recordedDate", timeDimensions.get(0).getAlias());
+ assertEquals(TimeGrain.DAY, timeDimensions.get(0).getGrain());
+ }
+
+ @Test
+ public void testUnsupportedTimeGrain() {
+ EntityProjection projection = basicProjection.copyOf()
+ .attribute(Attribute.builder()
+ .type(Date.class)
+ .name("recordedDate")
+ .argument(Argument.builder()
+ .name("grain")
+ .value("year")
+ .build())
+ .build())
+ .build();
+
+ assertThrows(InvalidOperationException.class, () -> new EntityProjectionTranslator(
+ playerStatsTable,
+ projection,
+ dictionary
+ ));
+ }
+}
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/QueryValidatorTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/QueryValidatorTest.java
new file mode 100644
index 0000000000..2a616199f3
--- /dev/null
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/QueryValidatorTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2020, Yahoo Inc.
+ * Licensed under the Apache License, Version 2.0
+ * See LICENSE file in project root for terms.
+ */
+package com.yahoo.elide.datastores.aggregation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.yahoo.elide.core.exceptions.InvalidOperationException;
+import com.yahoo.elide.core.filter.dialect.ParseException;
+import com.yahoo.elide.core.filter.expression.FilterExpression;
+import com.yahoo.elide.core.sort.Sorting;
+import com.yahoo.elide.datastores.aggregation.example.PlayerStats;
+import com.yahoo.elide.datastores.aggregation.filter.visitor.FilterConstraints;
+import com.yahoo.elide.datastores.aggregation.filter.visitor.SplitFilterExpressionVisitor;
+import com.yahoo.elide.datastores.aggregation.framework.SQLUnitTest;
+import com.yahoo.elide.datastores.aggregation.query.Query;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+public class QueryValidatorTest extends SQLUnitTest {
+ @BeforeAll
+ public static void init() {
+ SQLUnitTest.init();
+ }
+
+ @Test
+ public void testNoMetricQuery() {
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
+ .build();
+
+ QueryValidator validator = new QueryValidator(query, Collections.singleton("overallRating"), dictionary);
+
+ InvalidOperationException exception = assertThrows(InvalidOperationException.class, validator::validate);
+ assertEquals("Invalid operation: 'Must provide at least one metric in query'", exception.getMessage());
+ }
+
+ @Test
+ public void testSortingOnId() {
+ Map sortMap = new TreeMap<>();
+ sortMap.put("id", Sorting.SortOrder.asc);
+
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .metric(invoke(playerStatsTable.getMetric("lowScore")))
+ .groupByDimension(toProjection(playerStatsTable.getDimension("id")))
+ .groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
+ .sorting(new Sorting(sortMap))
+ .build();
+
+ Set allFields = new HashSet<>(Arrays.asList("id", "overallRating", "lowScore"));
+ QueryValidator validator = new QueryValidator(query, allFields, dictionary);
+
+ InvalidOperationException exception = assertThrows(InvalidOperationException.class, validator::validate);
+ assertEquals("Invalid operation: 'Sorting on id field is not permitted'", exception.getMessage());
+ }
+
+ @Test
+ public void testSortingOnNotQueriedDimension() {
+ Map sortMap = new TreeMap<>();
+ sortMap.put("country.name", Sorting.SortOrder.asc);
+
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .metric(invoke(playerStatsTable.getMetric("lowScore")))
+ .groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
+ .sorting(new Sorting(sortMap))
+ .build();
+
+ Set allFields = new HashSet<>(Arrays.asList("overallRating", "lowScore"));
+ QueryValidator validator = new QueryValidator(query, allFields, dictionary);
+
+ InvalidOperationException exception = assertThrows(InvalidOperationException.class, validator::validate);
+ assertEquals("Invalid operation: 'Can't sort on country as it is not present in query'", exception.getMessage());
+ }
+
+ @Test
+ public void testSortingOnNotQueriedMetric() {
+ Map sortMap = new TreeMap<>();
+ sortMap.put("highScore", Sorting.SortOrder.asc);
+
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .metric(invoke(playerStatsTable.getMetric("lowScore")))
+ .groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
+ .sorting(new Sorting(sortMap))
+ .build();
+
+ Set allFields = new HashSet<>(Arrays.asList("overallRating", "lowScore"));
+ QueryValidator validator = new QueryValidator(query, allFields, dictionary);
+
+ InvalidOperationException exception = assertThrows(InvalidOperationException.class, validator::validate);
+ assertEquals("Invalid operation: 'Can't sort on highScore as it is not present in query'", exception.getMessage());
+ }
+
+ @Test
+ public void testSortingOnNestedDimensionField() {
+ Map sortMap = new TreeMap<>();
+ sortMap.put("country.continent.name", Sorting.SortOrder.asc);
+
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .metric(invoke(playerStatsTable.getMetric("lowScore")))
+ .groupByDimension(toProjection(playerStatsTable.getDimension("country")))
+ .sorting(new Sorting(sortMap))
+ .build();
+
+ Set allFields = new HashSet<>(Arrays.asList("country", "lowScore"));
+ QueryValidator validator = new QueryValidator(query, allFields, dictionary);
+
+ UnsupportedOperationException exception = assertThrows(UnsupportedOperationException.class, validator::validate);
+ assertEquals("Currently sorting on double nested fields is not supported", exception.getMessage());
+ }
+
+ @Test
+ public void testHavingFilterPromotionUngroupedDimension() throws ParseException {
+ FilterExpression originalFilter = filterParser.parseFilterExpression("countryIsoCode==USA,lowScore<45",
+ PlayerStats.class, false);
+ SplitFilterExpressionVisitor visitor = new SplitFilterExpressionVisitor(playerStatsTable);
+ FilterConstraints constraints = originalFilter.accept(visitor);
+ FilterExpression whereFilter = constraints.getWhereExpression();
+ FilterExpression havingFilter = constraints.getHavingExpression();
+
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .metric(invoke(playerStatsTable.getMetric("lowScore")))
+ .whereFilter(whereFilter)
+ .havingFilter(havingFilter)
+ .build();
+
+ Set allFields = new HashSet<>(Collections.singletonList("lowScore"));
+ QueryValidator validator = new QueryValidator(query, allFields, dictionary);
+
+ InvalidOperationException exception = assertThrows(InvalidOperationException.class, validator::validate);
+ assertEquals(
+ "Invalid operation: 'Dimension field countryIsoCode must be grouped before filtering in having clause.'",
+ exception.getMessage());
+ }
+
+ @Test
+ public void testHavingFilterNoAggregatedMetric() throws ParseException {
+ FilterExpression originalFilter = filterParser.parseFilterExpression("lowScore<45", PlayerStats.class, false);
+ SplitFilterExpressionVisitor visitor = new SplitFilterExpressionVisitor(playerStatsTable);
+ FilterConstraints constraints = originalFilter.accept(visitor);
+ FilterExpression whereFilter = constraints.getWhereExpression();
+ FilterExpression havingFilter = constraints.getHavingExpression();
+
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .metric(invoke(playerStatsTable.getMetric("highScore")))
+ .whereFilter(whereFilter)
+ .havingFilter(havingFilter)
+ .build();
+
+ Set allFields = new HashSet<>(Collections.singletonList("highScore"));
+ QueryValidator validator = new QueryValidator(query, allFields, dictionary);
+
+ InvalidOperationException exception = assertThrows(InvalidOperationException.class, validator::validate);
+ assertEquals(
+ "Invalid operation: 'Metric field lowScore must be aggregated before filtering in having clause.'",
+ exception.getMessage());
+ }
+
+ @Test
+ public void testHavingFilterOnDimensionTable() throws ParseException {
+ FilterExpression originalFilter = filterParser.parseFilterExpression("country.isoCode==USA,lowScore<45",
+ PlayerStats.class, false);
+ SplitFilterExpressionVisitor visitor = new SplitFilterExpressionVisitor(playerStatsTable);
+ FilterConstraints constraints = originalFilter.accept(visitor);
+ FilterExpression whereFilter = constraints.getWhereExpression();
+ FilterExpression havingFilter = constraints.getHavingExpression();
+
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .metric(invoke(playerStatsTable.getMetric("lowScore")))
+ .whereFilter(whereFilter)
+ .havingFilter(havingFilter)
+ .build();
+
+ Set allFields = new HashSet<>(Collections.singletonList("lowScore"));
+ QueryValidator validator = new QueryValidator(query, allFields, dictionary);
+
+ InvalidOperationException exception = assertThrows(InvalidOperationException.class, validator::validate);
+ assertEquals(
+ "Invalid operation: 'Can't filter on relationship field [PlayerStats].country/[Country].isoCode in HAVING clause when querying table PlayerStats.'",
+ exception.getMessage());
+ }
+}
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Continent.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Continent.java
new file mode 100644
index 0000000000..0a557a9b2f
--- /dev/null
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Continent.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019, Yahoo Inc.
+ * Licensed under the Apache License, Version 2.0
+ * See LICENSE file in project root for terms.
+ */
+package com.yahoo.elide.datastores.aggregation.example;
+
+import com.yahoo.elide.annotation.Include;
+import com.yahoo.elide.datastores.aggregation.annotation.Cardinality;
+import com.yahoo.elide.datastores.aggregation.annotation.CardinalitySize;
+import lombok.Data;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Data
+@Entity
+@Include(rootLevel = true)
+@Table(name = "continents")
+@Cardinality(size = CardinalitySize.SMALL)
+public class Continent {
+
+ @Id
+ private String id;
+
+ private String name;
+}
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Country.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Country.java
index 5c4d55bb97..4e43e7b0e0 100644
--- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Country.java
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Country.java
@@ -9,11 +9,12 @@
import com.yahoo.elide.datastores.aggregation.annotation.Cardinality;
import com.yahoo.elide.datastores.aggregation.annotation.CardinalitySize;
import com.yahoo.elide.datastores.aggregation.annotation.FriendlyName;
-
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
import javax.persistence.Table;
/**
@@ -32,6 +33,8 @@ public class Country {
private String name;
+ private Continent continent;
+
@Id
public String getId() {
return id;
@@ -57,4 +60,14 @@ public String getName() {
public void setName(final String name) {
this.name = name;
}
+
+ @ManyToOne
+ @JoinColumn(name = "continent_id")
+ public Continent getContinent() {
+ return continent;
+ }
+
+ public void setContinent(Continent continent) {
+ this.continent = continent;
+ }
}
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStats.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStats.java
index 68f0b5b61d..e5157c49fe 100644
--- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStats.java
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStats.java
@@ -28,7 +28,6 @@
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
-
/**
* A root level entity for testing AggregationDataStore.
*/
@@ -90,6 +89,15 @@ public class PlayerStats {
*/
private Player player;
+ /**
+ * A table dimension.
+ */
+ private Player player2;
+
+ private String playerName;
+
+ private String player2Name;
+
private Date recordedDate;
@Id
@@ -196,4 +204,32 @@ public String getSubCountryIsoCode() {
public void setSubCountryIsoCode(String isoCode) {
this.subCountryIsoCode = isoCode;
}
+
+ @JoinColumn(name = "player2_id")
+ @ManyToOne
+ public Player getPlayer2() {
+ return player2;
+ }
+
+ public void setPlayer2(Player player2) {
+ this.player2 = player2;
+ }
+
+ @JoinTo(path = "player.name")
+ public String getPlayerName() {
+ return playerName;
+ }
+
+ public void setPlayerName(String playerName) {
+ this.playerName = playerName;
+ }
+
+ @JoinTo(path = "player2.name")
+ public String getPlayer2Name() {
+ return player2Name;
+ }
+
+ public void setPlayer2Name(String player2Name) {
+ this.player2Name = player2Name;
+ }
}
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStatsView.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStatsView.java
index 1f90811d01..22562b204a 100644
--- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStatsView.java
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStatsView.java
@@ -9,7 +9,6 @@
import com.yahoo.elide.datastores.aggregation.annotation.MetricAggregation;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromSubquery;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.metric.functions.SqlMax;
-
import lombok.Data;
import javax.persistence.Id;
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreTestHarness.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/framework/AggregationDataStoreTestHarness.java
similarity index 92%
rename from elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreTestHarness.java
rename to elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/framework/AggregationDataStoreTestHarness.java
index d531466f74..486dde0269 100644
--- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreTestHarness.java
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/framework/AggregationDataStoreTestHarness.java
@@ -3,10 +3,11 @@
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
-package com.yahoo.elide.datastores.aggregation;
+package com.yahoo.elide.datastores.aggregation.framework;
import com.yahoo.elide.core.DataStore;
import com.yahoo.elide.core.datastore.test.DataStoreTestHarness;
+import com.yahoo.elide.datastores.aggregation.AggregationDataStore;
import com.yahoo.elide.datastores.aggregation.metadata.MetaDataStore;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.SQLQueryEngineFactory;
import com.yahoo.elide.datastores.jpa.JpaDataStore;
@@ -23,6 +24,7 @@ public AggregationDataStoreTestHarness(SQLQueryEngineFactory queryEngineFactory)
@Override
public DataStore getDataStore() {
MetaDataStore metaDataStore = new MetaDataStore();
+
AggregationDataStore aggregationDataStore = new AggregationDataStore(queryEngineFactory, metaDataStore);
DataStore jpaStore = new JpaDataStore(
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/UnitTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/framework/SQLUnitTest.java
similarity index 86%
rename from elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/UnitTest.java
rename to elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/framework/SQLUnitTest.java
index 6d148ade10..0ad213230b 100644
--- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/UnitTest.java
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/framework/SQLUnitTest.java
@@ -3,12 +3,12 @@
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
-
-package com.yahoo.elide.datastores.aggregation.queryengines.sql;
+package com.yahoo.elide.datastores.aggregation.framework;
import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.core.filter.dialect.RSQLFilterDialect;
import com.yahoo.elide.datastores.aggregation.QueryEngine;
+import com.yahoo.elide.datastores.aggregation.example.Continent;
import com.yahoo.elide.datastores.aggregation.example.Country;
import com.yahoo.elide.datastores.aggregation.example.CountryView;
import com.yahoo.elide.datastores.aggregation.example.CountryViewNested;
@@ -25,6 +25,7 @@
import com.yahoo.elide.datastores.aggregation.metadata.models.TimeDimension;
import com.yahoo.elide.datastores.aggregation.query.ColumnProjection;
import com.yahoo.elide.datastores.aggregation.query.TimeDimensionProjection;
+import com.yahoo.elide.datastores.aggregation.queryengines.sql.SQLQueryEngine;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.metadata.SQLAnalyticView;
import com.yahoo.elide.datastores.aggregation.time.TimeGrain;
@@ -33,7 +34,7 @@
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
-public abstract class UnitTest {
+public abstract class SQLUnitTest {
protected static EntityManagerFactory emf;
protected static AnalyticView playerStatsTable;
protected static EntityDictionary dictionary;
@@ -42,6 +43,8 @@ public abstract class UnitTest {
protected static final Country HONG_KONG = new Country();
protected static final Country USA = new Country();
+ protected static final Continent ASIA = new Continent();
+ protected static final Continent NA = new Continent();
protected static QueryEngine engine;
@@ -56,6 +59,7 @@ public static void init() {
dictionary.bindEntity(Player.class);
dictionary.bindEntity(CountryView.class);
dictionary.bindEntity(CountryViewNested.class);
+ dictionary.bindEntity(Continent.class);
filterParser = new RSQLFilterDialect(dictionary);
playerStatsTable = new SQLAnalyticView(PlayerStats.class, dictionary);
@@ -64,13 +68,21 @@ public static void init() {
engine = new SQLQueryEngine(emf, metaDataStore);
+ ASIA.setName("Asia");
+ ASIA.setId("1");
+
+ NA.setName("North America");
+ NA.setId("2");
+
HONG_KONG.setIsoCode("HKG");
HONG_KONG.setName("Hong Kong");
HONG_KONG.setId("344");
+ HONG_KONG.setContinent(ASIA);
USA.setIsoCode("USA");
USA.setName("United States");
USA.setId("840");
+ USA.setContinent(NA);
}
public static ColumnProjection toProjection(Dimension dimension) {
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreIntegrationTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java
similarity index 76%
rename from elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreIntegrationTest.java
rename to elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java
index a7c413e641..3ba415ff43 100644
--- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreIntegrationTest.java
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
-package com.yahoo.elide.datastores.aggregation;
+package com.yahoo.elide.datastores.aggregation.integration;
import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.argument;
import static com.yahoo.elide.contrib.testhelpers.graphql.GraphQLDSL.arguments;
@@ -19,6 +19,8 @@
import com.yahoo.elide.core.HttpStatus;
import com.yahoo.elide.core.datastore.test.DataStoreTestHarness;
+import com.yahoo.elide.datastores.aggregation.AggregationDataStore;
+import com.yahoo.elide.datastores.aggregation.framework.AggregationDataStoreTestHarness;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.SQLQueryEngineFactory;
import com.yahoo.elide.initialization.IntegrationTest;
@@ -114,6 +116,38 @@ public void basicAggregationTest() throws Exception {
runQueryWithExpectedResult(graphQLRequest, expected);
}
+ @Test
+ public void whereFilterTest() throws Exception {
+ String graphQLRequest = document(
+ selection(
+ field(
+ "playerStats",
+ arguments(
+ argument("filter", "\"overallRating==\\\"Good\\\"\"")
+ ),
+ selections(
+ field("highScore"),
+ field("overallRating")
+ )
+ )
+ )
+ ).toQuery();
+
+ String expected = document(
+ selections(
+ field(
+ "playerStats",
+ selections(
+ field("highScore", 1234),
+ field("overallRating", "Good")
+ )
+ )
+ )
+ ).toResponse();
+
+ runQueryWithExpectedResult(graphQLRequest, expected);
+ }
+
@Test
public void havingFilterTest() throws Exception {
String graphQLRequest = document(
@@ -126,13 +160,6 @@ public void havingFilterTest() throws Exception {
selections(
field("lowScore"),
field("overallRating"),
- field(
- "country",
- selections(
- field("name"),
- field("id")
- )
- ),
field(
"player",
selections(
@@ -151,13 +178,6 @@ public void havingFilterTest() throws Exception {
selections(
field("lowScore", 35),
field("overallRating", "Good"),
- field(
- "country",
- selections(
- field("name", "United States"),
- field("id", "840")
- )
- ),
field(
"player",
selections(
@@ -174,7 +194,7 @@ public void havingFilterTest() throws Exception {
/**
* Test the case that a where clause is promoted into having clause.
- * @throws Exception
+ * @throws Exception exception
*/
@Test
public void wherePromotionTest() throws Exception {
@@ -232,7 +252,7 @@ public void wherePromotionTest() throws Exception {
/**
* Test the case that a where clause, which requires dimension join, is promoted into having clause.
- * @throws Exception
+ * @throws Exception exception
*/
@Test
public void havingClauseJoinTest() throws Exception {
@@ -247,13 +267,6 @@ public void havingClauseJoinTest() throws Exception {
selections(
field("lowScore"),
field("countryIsoCode"),
- field(
- "country",
- selections(
- field("name"),
- field("id")
- )
- ),
field(
"player",
selections(
@@ -272,13 +285,6 @@ public void havingClauseJoinTest() throws Exception {
selections(
field("lowScore", 35),
field("countryIsoCode", "USA"),
- field(
- "country",
- selections(
- field("name", "United States"),
- field("id", "840")
- )
- ),
field(
"player",
selections(
@@ -289,13 +295,6 @@ public void havingClauseJoinTest() throws Exception {
selections(
field("lowScore", 241),
field("countryIsoCode", "USA"),
- field(
- "country",
- selections(
- field("name", "United States"),
- field("id", "840")
- )
- ),
field(
"player",
selections(
@@ -312,7 +311,7 @@ public void havingClauseJoinTest() throws Exception {
/**
* Test invalid where promotion on a dimension field that is not grouped.
- * @throws Exception
+ * @throws Exception exception
*/
@Test
public void ungroupedHavingDimensionTest() throws Exception {
@@ -338,7 +337,7 @@ public void ungroupedHavingDimensionTest() throws Exception {
/**
* Test invalid having clause on a metric field that is not aggregated.
- * @throws Exception
+ * @throws Exception exception
*/
@Test
public void nonAggregatedHavingMetricTest() throws Exception {
@@ -364,7 +363,7 @@ public void nonAggregatedHavingMetricTest() throws Exception {
/**
* Test invalid where promotion on a different class than the queried class.
- * @throws Exception
+ * @throws Exception exception
*/
@Test
public void invalidHavingClauseClassTest() throws Exception {
@@ -383,28 +382,63 @@ public void invalidHavingClauseClassTest() throws Exception {
).toQuery();
String errorMessage = "\"Exception while fetching data (/playerStats) : Invalid operation: "
- + "'Classes don't match when try filtering on Country in having clause of PlayerStats.'\"";
+ + "'Can't filter on relationship field [PlayerStats].country/[Country].isoCode in HAVING clause "
+ + "when querying table PlayerStats.'\"";
runQueryWithExpectedError(graphQLRequest, errorMessage);
}
@Test
- public void whereFilterTest() throws Exception {
+ public void dimensionSortingTest() throws Exception {
String graphQLRequest = document(
selection(
field(
"playerStats",
arguments(
- argument("filter", "\"overallRating==\\\"Good\\\"\"")
+ argument("sort", "\"overallRating\"")
+ ),
+ selections(
+ field("lowScore"),
+ field("overallRating")
+ )
+ )
+ )
+ ).toQuery();
+
+ String expected = document(
+ selections(
+ field(
+ "playerStats",
+ selections(
+ field("lowScore", 35),
+ field("overallRating", "Good")
+ ),
+ selections(
+ field("lowScore", 241),
+ field("overallRating", "Great")
+ )
+ )
+ )
+ ).toResponse();
+
+ runQueryWithExpectedResult(graphQLRequest, expected);
+ }
+
+ @Test
+ public void metricSortingTest() throws Exception {
+ String graphQLRequest = document(
+ selection(
+ field(
+ "playerStats",
+ arguments(
+ argument("sort", "\"-highScore\"")
),
selections(
field("highScore"),
- field("overallRating"),
field(
"country",
selections(
- field("name"),
- field("id")
+ field("name")
)
)
)
@@ -417,24 +451,20 @@ public void whereFilterTest() throws Exception {
field(
"playerStats",
selections(
- field("highScore", 1234),
- field("overallRating", "Good"),
+ field("highScore", 2412),
field(
"country",
selections(
- field("name", "United States"),
- field("id", "840")
+ field("name", "United States")
)
)
),
selections(
field("highScore", 1000),
- field("overallRating", "Good"),
field(
"country",
selections(
- field("name", "Hong Kong"),
- field("id", "344")
+ field("name", "Hong Kong")
)
)
)
@@ -446,18 +476,23 @@ public void whereFilterTest() throws Exception {
}
@Test
- @Disabled
- //FIXME Needs metric computation support for test case to be valid.
- public void aggregationComputedMetricTest() throws Exception {
+ public void multipleColumnsSortingTest() throws Exception {
String graphQLRequest = document(
selection(
field(
- "videoGame",
+ "playerStats",
+ arguments(
+ argument("sort", "\"overallRating,player.name\"")
+ ),
selections(
- field("timeSpent"),
- field("sessions"),
- field("timeSpentPerSession"),
- field("timeSpentPerGame")
+ field("lowScore"),
+ field("overallRating"),
+ field(
+ "player",
+ selections(
+ field("name")
+ )
+ )
)
)
)
@@ -466,12 +501,36 @@ public void aggregationComputedMetricTest() throws Exception {
String expected = document(
selections(
field(
- "videoGame",
+ "playerStats",
selections(
- field("timeSpent", 1400),
- field("sessions", 70),
- field("timeSpentPerSession", 20),
- field("timeSpentPerGame", 14)
+ field("lowScore", 72),
+ field("overallRating", "Good"),
+ field(
+ "player",
+ selections(
+ field("name", "Han")
+ )
+ )
+ ),
+ selections(
+ field("lowScore", 35),
+ field("overallRating", "Good"),
+ field(
+ "player",
+ selections(
+ field("name", "Jon Doe")
+ )
+ )
+ ),
+ selections(
+ field("lowScore", 241),
+ field("overallRating", "Great"),
+ field(
+ "player",
+ selections(
+ field("name", "Jane Doe")
+ )
+ )
)
)
)
@@ -481,16 +540,144 @@ public void aggregationComputedMetricTest() throws Exception {
}
@Test
- public void timeGrainAggregationTest() throws Exception {
+ public void idSortingTest() throws Exception {
String graphQLRequest = document(
selection(
field(
"playerStats",
+ arguments(
+ argument("sort", "\"id\"")
+ ),
selections(
- field("highScore"),
- field("recordedDate", arguments(
- argument("grain", "\"month\"")
- ))
+ field("lowScore"),
+ field("id")
+ )
+ )
+ )
+ ).toQuery();
+
+ String expected = "\"Exception while fetching data (/playerStats) : Invalid operation: 'Sorting on id field is not permitted'\"";
+
+ runQueryWithExpectedError(graphQLRequest, expected);
+ }
+
+ @Test
+ public void nestedDimensionNotInQuerySortingTest() throws Exception {
+ String graphQLRequest = document(
+ selection(
+ field(
+ "playerStats",
+ arguments(
+ argument("sort", "\"-country.name,lowScore\"")
+ ),
+ selections(
+ field("lowScore")
+ )
+ )
+ )
+ ).toQuery();
+
+ String expected = "\"Exception while fetching data (/playerStats) : Invalid operation: 'Can't sort on country as it is not present in query'\"";
+
+ runQueryWithExpectedError(graphQLRequest, expected);
+ }
+
+ @Test
+ public void sortingOnMetricNotInQueryTest() throws Exception {
+ String graphQLRequest = document(
+ selection(
+ field(
+ "playerStats",
+ arguments(
+ argument("sort", "\"highScore\"")
+ ),
+ selections(
+ field("lowScore"),
+ field(
+ "country",
+ selections(
+ field("name")
+ )
+ )
+ )
+ )
+ )
+ ).toQuery();
+
+ String expected = "\"Exception while fetching data (/playerStats) : Invalid operation: 'Can't sort on highScore as it is not present in query'\"";
+
+ runQueryWithExpectedError(graphQLRequest, expected);
+ }
+
+ @Test
+ public void noMetricQueryTest() throws Exception {
+ String graphQLRequest = document(
+ selection(
+ field(
+ "playerStats",
+ selections(
+ field(
+ "country",
+ selections(
+ field("name")
+ )
+ )
+ )
+ )
+ )
+ ).toQuery();
+
+ String expected = "\"Exception while fetching data (/playerStats) : Invalid operation: 'Must provide at least one metric in query'\"";
+
+ runQueryWithExpectedError(graphQLRequest, expected);
+ }
+
+ @Test
+ public void sortingMultipleLevelNesting() throws Exception {
+ String graphQLRequest = document(
+ selection(
+ field(
+ "playerStats",
+ arguments(
+ argument("sort", "\"country.continent.name\"")
+ ),
+ selections(
+ field("lowScore"),
+ field(
+ "country",
+ selections(
+ field("name"),
+ field(
+ "continent",
+ selections(
+ field("name")
+ )
+ )
+ )
+ )
+ )
+ )
+ )
+ ).toQuery();
+
+ String expected = "\"Exception while fetching data (/playerStats) : Currently sorting on double nested fields is not supported\"";
+
+ runQueryWithExpectedError(graphQLRequest, expected);
+ }
+
+ @Test
+ @Disabled
+ //FIXME Needs metric computation support for test case to be valid.
+ public void aggregationComputedMetricTest() throws Exception {
+ String graphQLRequest = document(
+ selection(
+ field(
+ "videoGame",
+ selections(
+ field("timeSpent"),
+ field("sessions"),
+ field("timeSpentPerSession"),
+ field("timeSpentPerGame")
)
)
)
@@ -499,13 +686,16 @@ public void timeGrainAggregationTest() throws Exception {
String expected = document(
selections(
field(
- "playerStats",
+ "videoGame",
selections(
- field("highScore", 2412),
- field("recordedDate", "2019-07-01T00:00Z")
+ field("timeSpent", 1400),
+ field("sessions", 70),
+ field("timeSpentPerSession", 20),
+ field("timeSpentPerGame", 14)
)
)
- )).toResponse();
+ )
+ ).toResponse();
runQueryWithExpectedResult(graphQLRequest, expected);
}
diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/QueryEngineTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/QueryEngineTest.java
index f181ff6d39..6ba6d1f548 100644
--- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/QueryEngineTest.java
+++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/QueryEngineTest.java
@@ -16,6 +16,7 @@
import com.yahoo.elide.core.sort.Sorting;
import com.yahoo.elide.datastores.aggregation.example.PlayerStats;
import com.yahoo.elide.datastores.aggregation.example.PlayerStatsView;
+import com.yahoo.elide.datastores.aggregation.framework.SQLUnitTest;
import com.yahoo.elide.datastores.aggregation.metadata.models.AnalyticView;
import com.yahoo.elide.datastores.aggregation.query.Query;
import com.yahoo.elide.datastores.aggregation.queryengines.sql.metadata.SQLAnalyticView;
@@ -32,12 +33,12 @@
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
-public class QueryEngineTest extends UnitTest {
+public class QueryEngineTest extends SQLUnitTest {
private static AnalyticView playerStatsViewTable;
@BeforeAll
public static void init() {
- UnitTest.init();
+ SQLUnitTest.init();
playerStatsViewTable = new SQLAnalyticView(PlayerStatsView.class, dictionary);
}
@@ -91,7 +92,6 @@ public void testDegenerateDimensionFilter() throws Exception {
Query query = Query.builder()
.analyticView(playerStatsTable)
.metric(invoke(playerStatsTable.getMetric("lowScore")))
- .metric(invoke(playerStatsTable.getMetric("highScore")))
.groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
.timeDimension(toProjection(playerStatsTable.getTimeDimension("recordedDate"), TimeGrain.DAY))
.whereFilter(filterParser.parseFilterExpression("overallRating==Great",
@@ -104,7 +104,6 @@ public void testDegenerateDimensionFilter() throws Exception {
PlayerStats stats1 = new PlayerStats();
stats1.setId("0");
stats1.setLowScore(241);
- stats1.setHighScore(2412);
stats1.setOverallRating("Great");
stats1.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00"));
@@ -122,10 +121,7 @@ public void testFilterJoin() throws Exception {
Query query = Query.builder()
.analyticView(playerStatsTable)
.metric(invoke(playerStatsTable.getMetric("lowScore")))
- .metric(invoke(playerStatsTable.getMetric("highScore")))
- .groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
.groupByDimension(toProjection(playerStatsTable.getDimension("country")))
- .timeDimension(toProjection(playerStatsTable.getTimeDimension("recordedDate"), TimeGrain.DAY))
.whereFilter(filterParser.parseFilterExpression("country.name=='United States'",
PlayerStats.class, false))
.build();
@@ -135,25 +131,13 @@ public void testFilterJoin() throws Exception {
PlayerStats usa0 = new PlayerStats();
usa0.setId("0");
- usa0.setLowScore(241);
- usa0.setHighScore(2412);
- usa0.setOverallRating("Great");
+ usa0.setLowScore(35);
usa0.setCountry(USA);
- usa0.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00"));
- PlayerStats usa1 = new PlayerStats();
- usa1.setId("1");
- usa1.setLowScore(35);
- usa1.setHighScore(1234);
- usa1.setOverallRating("Good");
- usa1.setCountry(USA);
- usa1.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00"));
-
- assertEquals(2, results.size());
+ assertEquals(1, results.size());
assertEquals(usa0, results.get(0));
- assertEquals(usa1, results.get(1));
- // test join
+ // test relationship hydration
PlayerStats actualStats1 = (PlayerStats) results.get(0);
assertNotNull(actualStats1.getCountry());
}
@@ -185,11 +169,9 @@ public void testSubqueryFilterJoin() throws Exception {
/**
* Test a view which filters on "stats.overallRating = 'Great'".
- *
- * @throws Exception exception
*/
@Test
- public void testSubqueryLoad() throws Exception {
+ public void testSubqueryLoad() {
Query query = Query.builder()
.analyticView(playerStatsViewTable)
.metric(invoke(playerStatsTable.getMetric("highScore")))
@@ -259,7 +241,6 @@ public void testPagination() {
Query query = Query.builder()
.analyticView(playerStatsTable)
.metric(invoke(playerStatsTable.getMetric("lowScore")))
- .metric(invoke(playerStatsTable.getMetric("highScore")))
.groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
.timeDimension(toProjection(playerStatsTable.getTimeDimension("recordedDate"), TimeGrain.DAY))
.pagination(pagination)
@@ -272,7 +253,6 @@ public void testPagination() {
PlayerStats stats1 = new PlayerStats();
stats1.setId("0");
stats1.setLowScore(35);
- stats1.setHighScore(1234);
stats1.setOverallRating("Good");
stats1.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00"));
@@ -351,7 +331,7 @@ public void testHavingClauseJoin() throws Exception {
* @throws Exception exception
*/
@Test
- public void testTheEverythingQuery() throws Exception {
+ public void testEdgeCasesQuery() throws Exception {
Map sortMap = new TreeMap<>();
sortMap.put("player.name", Sorting.SortOrder.asc);
@@ -429,14 +409,13 @@ public void testSortByMultipleColumns() {
public void testRelationshipHydration() {
Map sortMap = new TreeMap<>();
sortMap.put("country.name", Sorting.SortOrder.desc);
+ sortMap.put("overallRating", Sorting.SortOrder.desc);
Query query = Query.builder()
.analyticView(playerStatsTable)
.metric(invoke(playerStatsTable.getMetric("lowScore")))
- .metric(invoke(playerStatsTable.getMetric("highScore")))
.groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
.groupByDimension(toProjection(playerStatsTable.getDimension("country")))
- .timeDimension(toProjection(playerStatsTable.getTimeDimension("recordedDate"), TimeGrain.DAY))
.sorting(new Sorting(sortMap))
.build();
@@ -446,26 +425,20 @@ public void testRelationshipHydration() {
PlayerStats usa0 = new PlayerStats();
usa0.setId("0");
usa0.setLowScore(241);
- usa0.setHighScore(2412);
usa0.setOverallRating("Great");
usa0.setCountry(USA);
- usa0.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00"));
PlayerStats usa1 = new PlayerStats();
usa1.setId("1");
usa1.setLowScore(35);
- usa1.setHighScore(1234);
usa1.setOverallRating("Good");
usa1.setCountry(USA);
- usa1.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00"));
PlayerStats hk2 = new PlayerStats();
hk2.setId("2");
hk2.setLowScore(72);
- hk2.setHighScore(1000);
hk2.setOverallRating("Good");
hk2.setCountry(HONG_KONG);
- hk2.setRecordedDate(Timestamp.valueOf("2019-07-13 00:00:00"));
assertEquals(3, results.size());
assertEquals(usa0, results.get(0));
@@ -479,11 +452,9 @@ public void testRelationshipHydration() {
/**
* Test grouping by a dimension with a JoinTo annotation.
- *
- * @throws Exception exception
*/
@Test
- public void testJoinToGroupBy() throws Exception {
+ public void testJoinToGroupBy() {
Query query = Query.builder()
.analyticView(playerStatsTable)
.metric(invoke(playerStatsTable.getMetric("highScore")))
@@ -543,11 +514,9 @@ public void testJoinToFilter() throws Exception {
/**
* Test grouping by a dimension with a JoinTo annotation.
- *
- * @throws Exception exception
*/
@Test
- public void testJoinToSort() throws Exception {
+ public void testJoinToSort() {
Map sortMap = new TreeMap<>();
sortMap.put("countryIsoCode", Sorting.SortOrder.asc);
@@ -613,7 +582,7 @@ public void testTotalScoreByMonth() {
* Test filter by time dimension.
*/
@Test
- public void testFilterByTemporalDimension() throws Exception {
+ public void testFilterByTemporalDimension() {
FilterPredicate predicate = new FilterPredicate(
new Path(PlayerStats.class, dictionary, "recordedDate"),
Operator.IN,
@@ -638,5 +607,75 @@ public void testFilterByTemporalDimension() throws Exception {
assertEquals(stats0, results.get(0));
}
+ @Test
+ public void testSortAggregatedMetric() {
+ Map sortMap = new TreeMap<>();
+ sortMap.put("lowScore", Sorting.SortOrder.desc);
+
+ Query query = Query.builder()
+ .analyticView(playerStatsTable)
+ .groupByDimension(toProjection(playerStatsTable.getDimension("overallRating")))
+ .metric(invoke(playerStatsTable.getMetric("lowScore")))
+ .sorting(new Sorting(sortMap))
+ .build();
+
+ List