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 results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) + .collect(Collectors.toList()); + + PlayerStats stats0 = new PlayerStats(); + stats0.setId("0"); + stats0.setLowScore(241); + stats0.setOverallRating("Great"); + + PlayerStats stats1 = new PlayerStats(); + stats1.setId("1"); + stats1.setLowScore(35); + stats1.setOverallRating("Good"); + + assertEquals(2, results.size()); + assertEquals(stats0, results.get(0)); + assertEquals(stats1, results.get(1)); + } + + @Test + public void testAmbiguousFields() { + Map sortMap = new TreeMap<>(); + sortMap.put("lowScore", Sorting.SortOrder.asc); + + Query query = Query.builder() + .analyticView(playerStatsTable) + .groupByDimension(toProjection(playerStatsTable.getDimension("playerName"))) + .groupByDimension(toProjection(playerStatsTable.getDimension("player2Name"))) + .metric(invoke(playerStatsTable.getMetric("lowScore"))) + .sorting(new Sorting(sortMap)) + .build(); + + List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) + .collect(Collectors.toList()); + + PlayerStats stats0 = new PlayerStats(); + stats0.setId("0"); + stats0.setLowScore(35); + stats0.setPlayerName("Jon Doe"); + stats0.setPlayer2Name("Jane Doe"); + + PlayerStats stats1 = new PlayerStats(); + stats1.setId("1"); + stats1.setLowScore(72); + stats1.setPlayerName("Han"); + stats1.setPlayer2Name("Jon Doe"); + + PlayerStats stats2 = new PlayerStats(); + stats2.setId("2"); + stats2.setLowScore(241); + stats2.setPlayerName("Jane Doe"); + stats2.setPlayer2Name("Han"); + + assertEquals(3, results.size()); + assertEquals(stats0, results.get(0)); + assertEquals(stats1, results.get(1)); + assertEquals(stats2, results.get(2)); + } + //TODO - Add Invalid Request Tests } diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SubselectTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SubselectTest.java index f7a14e3482..b0f536c9db 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SubselectTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SubselectTest.java @@ -12,6 +12,7 @@ import com.yahoo.elide.core.sort.Sorting; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; import com.yahoo.elide.datastores.aggregation.example.SubCountry; +import com.yahoo.elide.datastores.aggregation.framework.SQLUnitTest; import com.yahoo.elide.datastores.aggregation.query.Query; import com.yahoo.elide.datastores.aggregation.time.TimeGrain; @@ -25,13 +26,13 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; -public class SubselectTest extends UnitTest { +public class SubselectTest extends SQLUnitTest { private static final SubCountry SUB_HONG_KONG = new SubCountry(); private static final SubCountry SUB_USA = new SubCountry(); @BeforeAll public static void init() { - UnitTest.init(); + SQLUnitTest.init(); SUB_HONG_KONG.setIsoCode("HKG"); SUB_HONG_KONG.setName("Hong Kong"); diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/ViewTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/ViewTest.java index d568794c8a..779029104a 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/ViewTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/ViewTest.java @@ -13,6 +13,7 @@ import com.yahoo.elide.core.exceptions.InvalidPredicateException; import com.yahoo.elide.core.sort.Sorting; import com.yahoo.elide.datastores.aggregation.example.PlayerStatsWithView; +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; @@ -26,12 +27,12 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; -public class ViewTest extends UnitTest { +public class ViewTest extends SQLUnitTest { protected static AnalyticView playerStatsWithViewSchema; @BeforeAll public static void init() { - UnitTest.init(); + SQLUnitTest.init(); playerStatsWithViewSchema = new SQLAnalyticView(PlayerStatsWithView.class, dictionary); } diff --git a/elide-datastore/elide-datastore-aggregation/src/test/resources/META-INF/persistence.xml b/elide-datastore/elide-datastore-aggregation/src/test/resources/META-INF/persistence.xml index 2f4f3a305a..2844a0b784 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/resources/META-INF/persistence.xml +++ b/elide-datastore/elide-datastore-aggregation/src/test/resources/META-INF/persistence.xml @@ -13,6 +13,7 @@ com.yahoo.elide.datastores.aggregation.example.Country com.yahoo.elide.datastores.aggregation.example.SubCountry com.yahoo.elide.datastores.aggregation.example.Player + com.yahoo.elide.datastores.aggregation.example.Continent diff --git a/elide-datastore/elide-datastore-aggregation/src/test/resources/continent.csv b/elide-datastore/elide-datastore-aggregation/src/test/resources/continent.csv new file mode 100644 index 0000000000..b9f40f2ced --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/test/resources/continent.csv @@ -0,0 +1,3 @@ +id,name +1,Asia +2,North America diff --git a/elide-datastore/elide-datastore-aggregation/src/test/resources/country.csv b/elide-datastore/elide-datastore-aggregation/src/test/resources/country.csv index e618f4e629..ae0eeebc02 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/resources/country.csv +++ b/elide-datastore/elide-datastore-aggregation/src/test/resources/country.csv @@ -1,3 +1,3 @@ -id,isoCode,name -344,HKG,Hong Kong -840,USA,United States +id,isoCode,name,continent_id +344,HKG,Hong Kong,1 +840,USA,United States,2 diff --git a/elide-datastore/elide-datastore-aggregation/src/test/resources/create_tables.sql b/elide-datastore/elide-datastore-aggregation/src/test/resources/create_tables.sql index a556e0c5bd..1667f6f061 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/resources/create_tables.sql +++ b/elide-datastore/elide-datastore-aggregation/src/test/resources/create_tables.sql @@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS playerStats country_id VARCHAR(255), sub_country_id VARCHAR(255), player_id BIGINT, + player2_id BIGINT, recordedDate DATETIME ) AS SELECT * FROM CSVREAD('classpath:player_stats.csv'); @@ -13,7 +14,8 @@ CREATE TABLE IF NOT EXISTS countries ( id VARCHAR(255), isoCode VARCHAR(255), - name VARCHAR(255) + name VARCHAR(255), + continent_id VARCHAR(255) ) AS SELECT * FROM CSVREAD('classpath:country.csv'); CREATE TABLE IF NOT EXISTS players @@ -27,3 +29,9 @@ CREATE TABLE IF NOT EXISTS videoGames game_rounds BIGINT, timeSpent BIGINT ) AS SELECT * FROM CSVREAD('classpath:video_games.csv'); + +CREATE TABLE IF NOT EXISTS continents + ( + id BIGINT, + name VARCHAR(255) + ) AS SELECT * FROM CSVREAD('classpath:continent.csv'); diff --git a/elide-datastore/elide-datastore-aggregation/src/test/resources/player_stats.csv b/elide-datastore/elide-datastore-aggregation/src/test/resources/player_stats.csv index 9c48e24cb3..0778a7aa53 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/resources/player_stats.csv +++ b/elide-datastore/elide-datastore-aggregation/src/test/resources/player_stats.csv @@ -1,4 +1,4 @@ -highScore,lowScore,overallRating,country_id,sub_country_id,player_id,recordedDate -1234,35,Good,840,840,1,2019-07-12 00:00:00 -2412,241,Great,840,840,2,2019-07-11 00:00:00 -1000,72,Good,344,344,3,2019-07-13 00:00:00 +highScore,lowScore,overallRating,country_id,sub_country_id,player_id,player2_id,recordedDate +1234,35,Good,840,840,1,2,2019-07-12 00:00:00 +2412,241,Great,840,840,2,3,2019-07-11 00:00:00 +1000,72,Good,344,344,3,1,2019-07-13 00:00:00 diff --git a/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/filter/FilterTranslator.java b/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/filter/FilterTranslator.java index a9604ecaad..2feb77c1ac 100644 --- a/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/filter/FilterTranslator.java +++ b/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/filter/FilterTranslator.java @@ -51,13 +51,10 @@ public class FilterTranslator implements FilterOperation { private static Map operatorGenerators; private static Map, String>, JPQLPredicateGenerator> predicateOverrides; - public static final Function GENERATE_HQL_COLUMN_NO_ALIAS = (predicate) -> { - return predicate.getFieldPath(); - }; + public static final Function GENERATE_HQL_COLUMN_NO_ALIAS = FilterPredicate::getFieldPath; - public static final Function GENERATE_HQL_COLUMN_WITH_ALIAS = (predicate) -> { - return predicate.getAlias() + "." + predicate.getField(); - }; + public static final Function GENERATE_HQL_COLUMN_WITH_ALIAS = + (predicate) -> FilterPredicate.getPathAlias(predicate.getPath()) + "." + predicate.getField(); static { predicateOverrides = new HashMap<>(); diff --git a/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/hibernate/hql/AbstractHQLQueryBuilder.java b/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/hibernate/hql/AbstractHQLQueryBuilder.java index a84756d7d5..194e897bcb 100644 --- a/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/hibernate/hql/AbstractHQLQueryBuilder.java +++ b/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/hibernate/hql/AbstractHQLQueryBuilder.java @@ -5,6 +5,9 @@ */ package com.yahoo.elide.core.hibernate.hql; +import static com.yahoo.elide.core.filter.FilterPredicate.appendAlias; +import static com.yahoo.elide.core.filter.FilterPredicate.getTypeAlias; + import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.Path; import com.yahoo.elide.core.RelationshipType; @@ -155,23 +158,22 @@ private String extractJoinClause(FilterPredicate predicate, Set alreadyJ for (Path.PathElement pathElement : predicate.getPath().getPathElements()) { String fieldName = pathElement.getFieldName(); Class typeClass = dictionary.lookupEntityClass(pathElement.getType()); - String typeAlias = FilterPredicate.getTypeAlias(typeClass); + String typeAlias = getTypeAlias(typeClass); - //Nothing left to join. + // Nothing left to join. if (! dictionary.isRelation(pathElement.getType(), fieldName)) { return joinClause.toString(); } - String alias = typeAlias + UNDERSCORE + fieldName; + String alias = previousAlias == null + ? appendAlias(typeAlias, fieldName) + : appendAlias(previousAlias, fieldName); - String joinFragment; + String joinFragment = previousAlias == null + ? LEFT + JOIN + typeAlias + PERIOD + fieldName + SPACE + alias + SPACE + : LEFT + JOIN + previousAlias + PERIOD + fieldName + SPACE + alias + SPACE; //This is the first path element - if (previousAlias == null) { - joinFragment = LEFT + JOIN + typeAlias + PERIOD + fieldName + SPACE + alias + SPACE; - } else { - joinFragment = LEFT + JOIN + previousAlias + PERIOD + fieldName + SPACE + alias + SPACE; - } if (!alreadyJoined.contains(joinFragment)) { joinClause.append(joinFragment); diff --git a/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/hibernate/hql/RootCollectionPageTotalsQueryBuilder.java b/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/hibernate/hql/RootCollectionPageTotalsQueryBuilder.java index 0f6215fc86..80be6043e3 100644 --- a/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/hibernate/hql/RootCollectionPageTotalsQueryBuilder.java +++ b/elide-datastore/elide-datastore-hibernate/src/main/java/com/yahoo/elide/core/hibernate/hql/RootCollectionPageTotalsQueryBuilder.java @@ -70,7 +70,7 @@ public Query build() { filterClause = WHERE + new FilterTranslator().apply(filterExpression.get(), USE_ALIAS); //Build the JOIN clause - joinClause = getJoinClauseFromFilters(filterExpression.get()); + joinClause = getJoinClauseFromFilters(filterExpression.get()); } else { predicates = new HashSet<>(); diff --git a/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/AbstractHQLQueryBuilderTest.java b/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/AbstractHQLQueryBuilderTest.java index 75a8eb9159..0b33c976c1 100644 --- a/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/AbstractHQLQueryBuilderTest.java +++ b/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/AbstractHQLQueryBuilderTest.java @@ -99,8 +99,8 @@ public void testFilterJoinClause() { String actual = getJoinClauseFromFilters(andExpression); String expected = " LEFT JOIN example_Author.books example_Author_books " - + "LEFT JOIN example_Author_books.chapters example_Book_chapters " - + "LEFT JOIN example_Author_books.publisher example_Book_publisher "; + + "LEFT JOIN example_Author_books.chapters example_Author_books_chapters " + + "LEFT JOIN example_Author_books.publisher example_Author_books_publisher "; assertEquals(expected, actual); } diff --git a/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/RootCollectionFetchQueryBuilderTest.java b/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/RootCollectionFetchQueryBuilderTest.java index c953fcc1a6..e6d37f41f9 100644 --- a/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/RootCollectionFetchQueryBuilderTest.java +++ b/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/RootCollectionFetchQueryBuilderTest.java @@ -115,10 +115,10 @@ public void testRootFetchWithJoinFilter() { String expected = "SELECT example_Author FROM example.Author AS example_Author " + "LEFT JOIN example_Author.books example_Author_books " - + "LEFT JOIN example_Author_books.chapters example_Book_chapters " - + "LEFT JOIN example_Author_books.publisher example_Book_publisher " - + "WHERE (example_Book_chapters.title IN (:books_chapters_title_XXX, :books_chapters_title_XXX) " - + "OR example_Book_publisher.name IN (:books_publisher_name_XXX))"; + + "LEFT JOIN example_Author_books.chapters example_Author_books_chapters " + + "LEFT JOIN example_Author_books.publisher example_Author_books_publisher " + + "WHERE (example_Author_books_chapters.title IN (:books_chapters_title_XXX, :books_chapters_title_XXX) " + + "OR example_Author_books_publisher.name IN (:books_publisher_name_XXX))"; String actual = query.getQueryText(); actual = actual.trim().replaceAll(" +", " "); diff --git a/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/RootCollectionPageTotalsQueryBuilderTest.java b/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/RootCollectionPageTotalsQueryBuilderTest.java index eeeb0ad5e4..3b15499dad 100644 --- a/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/RootCollectionPageTotalsQueryBuilderTest.java +++ b/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/RootCollectionPageTotalsQueryBuilderTest.java @@ -119,10 +119,11 @@ public void testRootFetchWithJoinFilter() { String expected = "SELECT COUNT(DISTINCT example_Author) FROM example.Author AS example_Author " + "LEFT JOIN example_Author.books example_Author_books " - + "LEFT JOIN example_Author_books.chapters example_Book_chapters " - + "LEFT JOIN example_Author_books.publisher example_Book_publisher " - + "WHERE (example_Book_chapters.title IN (:books_chapters_title_XXX, :books_chapters_title_XXX) " - + "OR example_Book_publisher.name IN (:books_publisher_name_XXX))"; + + "LEFT JOIN example_Author_books.chapters example_Author_books_chapters " + + "LEFT JOIN example_Author_books.publisher example_Author_books_publisher " + + "WHERE (example_Author_books_chapters.title IN " + + "(:books_chapters_title_XXX, :books_chapters_title_XXX) " + + "OR example_Author_books_publisher.name IN (:books_publisher_name_XXX))"; String actual = query.getQueryText(); actual = actual.trim().replaceAll(" +", " "); diff --git a/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/SubCollectionPageTotalsQueryBuilderTest.java b/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/SubCollectionPageTotalsQueryBuilderTest.java index e1a78daf35..c2e78c9198 100644 --- a/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/SubCollectionPageTotalsQueryBuilderTest.java +++ b/elide-datastore/elide-datastore-hibernate/src/test/java/com/yahoo/elide/datastores/hibernate/hql/SubCollectionPageTotalsQueryBuilderTest.java @@ -143,8 +143,8 @@ public void testSubCollectionPageTotalsWithJoinFilter() { "SELECT COUNT(DISTINCT example_Author_books) " + "FROM example.Author AS example_Author " + "LEFT JOIN example_Author.books example_Author_books " - + "LEFT JOIN example_Author_books.publisher example_Book_publisher " - + "WHERE (example_Book_publisher.name IN (:books_publisher_name_XXX) " + + "LEFT JOIN example_Author_books.publisher example_Author_books_publisher " + + "WHERE (example_Author_books_publisher.name IN (:books_publisher_name_XXX) " + "AND example_Author.id IN (:id_XXX))"; String actual = query.getQueryText();