From 2bc77c31a469ddce37e51e748ec1c42a7a9be461 Mon Sep 17 00:00:00 2001 From: Han Chen Date: Fri, 27 Sep 2019 16:56:27 -0500 Subject: [PATCH] Hydrate Relationship (#987) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron * Added basic H2 DB test harness * Started breaking out projections * Moved getValue and setValue from PersistentResource to EntityDictionary * Added basic logic to hydrate entities * Added FromTable and FromSubquery annotations. Add explicit exclusion of entity relationship hydration * Refactored HQLFilterOperation to take an alias generator * Added test support for RSQL filter generation. Some cleanup * Added basic support for WHERE clause filtering on the fact table * Added working test for subquery SQL * Added basic join logic for filters * Added a test with a subquery and a filter join * Refactored Schema classes and Query to support metric aggregation SQL expansion * Added group by support * Added logic for ID generation * Added sorting logic and test * Added pagination support and testing * All column references use proper name now for SQL * Removed calcite as a query engine * Refactored HQLFilterOperation so it can be used for Having and Where clause generaiton * Added HAVING clause support * Changed Query to take schema instead of entityClass * First pass at cleanup * Fixed checkstyles * Cleanup * Hydrate Relationship * Cleanup * Added a complex SQL expression test and fixed bugs * Fixed merge issues. Added another test. Added better logging * Self-review * Self-review * Self-review * Self-review * Self-review * Address comments from @aklish * Refactor EntityHydrator (#893) * rebase * keep Jiaqi's changes * fix id * fix maven verify * Remove HQLFilterOperation * fix dictionary * fix SqlEngineTest * remove unused part * make codacy happy * should use getParametrizedType * address comments --- .../yahoo/elide/core/EntityDictionary.java | 4 +- .../elide-datastore-aggregation/pom.xml | 27 +- .../dimension/EntityDimension.java | 2 +- .../engine/AbstractEntityHydrator.java | 204 ++++++++++++ .../aggregation/engine/SQLEntityHydrator.java | 74 +++++ .../aggregation/engine/SQLQueryEngine.java | 106 ++----- .../aggregation/engine/StitchList.java | 159 ++++++++++ .../aggregation/metric/AggregatedMetric.java | 10 +- .../datastores/aggregation/metric/Metric.java | 4 +- .../datastores/aggregation/SchemaTest.java | 34 +- .../aggregation/dimension/DimensionTest.java | 32 +- .../dimension/EntityDimensionTest.java | 52 ++-- .../engine/SQLQueryEngineTest.java | 291 +++++++++++++----- .../aggregation/example/Country.java | 3 + .../filter/visitor/FilterConstraintsTest.java | 49 +-- .../SplitFilterExpressionVisitorTest.java | 133 ++++---- .../metric/AggregatedMetricTest.java | 35 ++- .../src/test/resources/country.csv | 2 +- .../src/test/resources/player_stats.csv | 1 + .../elide/core/filter/FilterTranslator.java | 4 +- pom.xml | 8 +- 21 files changed, 890 insertions(+), 344 deletions(-) create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/AbstractEntityHydrator.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLEntityHydrator.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/StitchList.java diff --git a/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java b/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java index c5006c457b..d7b805cb46 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java @@ -246,8 +246,8 @@ public ParseTree getPermissionsForClass(Class resourceClass, Class resourceClass, - String field, - Class annotationClass) { + String field, + Class annotationClass) { EntityBinding binding = getEntityBinding(resourceClass); return binding.entityPermissions.getFieldChecksForPermission(field, annotationClass); } diff --git a/elide-datastore/elide-datastore-aggregation/pom.xml b/elide-datastore/elide-datastore-aggregation/pom.xml index 3104bf2097..e8ebfadbb6 100644 --- a/elide-datastore/elide-datastore-aggregation/pom.xml +++ b/elide-datastore/elide-datastore-aggregation/pom.xml @@ -14,7 +14,7 @@ com.yahoo.elide elide-datastore-parent-pom - 4.4.5-SNAPSHOT + 4.5.2-SNAPSHOT @@ -38,6 +38,10 @@ HEAD + + 5.4.1 + + com.yahoo.elide @@ -71,9 +75,25 @@ + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + - org.testng - testng + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} test @@ -92,7 +112,6 @@ test - diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/dimension/EntityDimension.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/dimension/EntityDimension.java index f95726862f..70d76531b6 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/dimension/EntityDimension.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/dimension/EntityDimension.java @@ -140,13 +140,13 @@ public EntityDimension( /** * Constructor. * + * @param schema The schema this {@link Column} belongs to. * @param dimensionField The entity field or relation that this {@link Dimension} represents * @param annotation Provides static meta data about this {@link Dimension} * @param fieldType The Java type for this entity field or relation * @param dimensionType The physical storage structure backing this {@link Dimension}, such as a table or a column * @param cardinality The estimated cardinality of this {@link Dimension} in SQL table * @param friendlyName A human-readable name representing this {@link Dimension} - * * @throws NullPointerException any argument, except for {@code annotation}, is {@code null} */ protected EntityDimension( diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/AbstractEntityHydrator.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/AbstractEntityHydrator.java new file mode 100644 index 0000000000..fa9c9ca507 --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/AbstractEntityHydrator.java @@ -0,0 +1,204 @@ +/* + * 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.engine; + +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.datastores.aggregation.Query; +import com.yahoo.elide.datastores.aggregation.QueryEngine; +import com.yahoo.elide.datastores.aggregation.dimension.Dimension; +import com.yahoo.elide.datastores.aggregation.dimension.DimensionType; +import com.yahoo.elide.datastores.aggregation.metric.Metric; + +import com.google.common.base.Preconditions; + +import org.apache.commons.lang3.mutable.MutableInt; + +import lombok.AccessLevel; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * {@link AbstractEntityHydrator} hydrates the entity loaded by {@link QueryEngine#executeQuery(Query)}. + *

+ * {@link AbstractEntityHydrator} is not thread-safe and should be accessed by only 1 thread in this application, + * because it uses {@link StitchList}. See {@link StitchList} for more details. + */ +public abstract class AbstractEntityHydrator { + + @Getter(AccessLevel.PROTECTED) + private final EntityDictionary entityDictionary; + + @Getter(AccessLevel.PRIVATE) + private final StitchList stitchList; + + @Getter(AccessLevel.PROTECTED) + private final List> results = new ArrayList<>(); + + @Getter(AccessLevel.PRIVATE) + private final Query query; + + /** + * Constructor. + * + * @param results The loaded objects from {@link QueryEngine#executeQuery(Query)} + * @param query The query passed to {@link QueryEngine#executeQuery(Query)} to load the objects + * @param entityDictionary An object that sets entity instance values and provides entity metadata info + */ + public AbstractEntityHydrator(List results, Query query, EntityDictionary entityDictionary) { + this.stitchList = new StitchList(entityDictionary); + this.query = query; + this.entityDictionary = entityDictionary; + + //Get all the projections from the client query. + List projections = this.query.getMetrics().keySet().stream() + .map(Metric::getName) + .collect(Collectors.toList()); + + projections.addAll(this.query.getDimensions().stream() + .map(Dimension::getName) + .collect(Collectors.toList())); + + + results.forEach(result -> { + Map row = new HashMap<>(); + + Object[] resultValues = result instanceof Object[] ? (Object[]) result : new Object[] { result }; + + Preconditions.checkArgument(projections.size() == resultValues.length); + + for (int idx = 0; idx < resultValues.length; idx++) { + Object value = resultValues[idx]; + String fieldName = projections.get(idx); + row.put(fieldName, value); + } + + this.results.add(row); + }); + } + + /** + * Loads a map of relationship object ID to relationship object instance. + *

+ * Note the relationship cannot be toMany. This method will be invoked for every relationship field of the + * requested entity. Its implementation should return the result of the following query + *

+ * Given a relationship with type {@code relationshipType} in an entity, loads all relationship + * objects whose foreign keys are one of the specified list, {@code joinFieldIds}. + *

+ * For example, when the relationship is loaded from SQL and we have the following example identity: + *

+     * public class PlayerStats {
+     *     private String id;
+     *     private Country country;
+     *
+     *     @OneToOne
+     *     @JoinColumn(name = "country_id")
+     *     public Country getCountry() {
+     *         return country;
+     *     }
+     * }
+     * 
+ * In this case {@code relationshipType = Country.class}. If {@code country} is + * requested in {@code PlayerStats} query and 3 stats, for example, are found in database whose country ID's are + * {@code joinFieldIds = [840, 344, 840]}, then this method should effectively run the following query (JPQL as + * example) + *
+     * {@code
+     *     SELECT e FROM country_table e WHERE country_id IN (840, 344);
+     * }
+     * 
+ * and returns the map of [840: Country(id:840), 344: Country(id:344)] + * + * @param relationshipType The type of relationship + * @param joinFieldIds The specified list of join ID's against the relationship + * + * @return a list of hydrating values + */ + protected abstract Map getRelationshipValues( + Class relationshipType, + List joinFieldIds + ); + + public Iterable hydrate() { + //Coerce the results into entity objects. + MutableInt counter = new MutableInt(0); + + List queryResults = getResults().stream() + .map((result) -> coerceObjectToEntity(result, counter)) + .collect(Collectors.toList()); + + if (getStitchList().shouldStitch()) { + // relationship is requested, stitch relationship then + populateObjectLookupTable(); + getStitchList().stitch(); + } + + return queryResults; + } + + /** + * Coerces results from a {@link Query} into an Object. + * + * @param result a fieldName-value map + * @param counter Monotonically increasing number to generate IDs. + * @return A hydrated entity object. + */ + protected Object coerceObjectToEntity(Map result, MutableInt counter) { + Class entityClass = query.getSchema().getEntityClass(); + + //Construct the object. + Object entityInstance; + try { + entityInstance = entityClass.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + + result.forEach((fieldName, value) -> { + Dimension dim = query.getSchema().getDimension(fieldName); + + if (dim != null && dim.getDimensionType() == DimensionType.ENTITY) { + getStitchList().todo(entityInstance, fieldName, value); // We don't hydrate relationships here. + } else { + getEntityDictionary().setValue(entityInstance, fieldName, value); + } + }); + + //Set the ID (it must be coerced from an integer) + getEntityDictionary().setValue( + entityInstance, + getEntityDictionary().getIdFieldName(entityClass), + counter.getAndIncrement() + ); + + return entityInstance; + } + + /** + * Foe each requested relationship, run a single query to load all relationship objects whose ID's are involved in + * the request. + */ + private void populateObjectLookupTable() { + // mapping: relationship field name -> join ID's + Map> hydrationIdsByRelationship = getStitchList().getHydrationMapping(); + + // hydrate each relationship + for (Map.Entry> entry : hydrationIdsByRelationship.entrySet()) { + String joinField = entry.getKey(); + List joinFieldIds = entry.getValue(); + Class relationshipType = getEntityDictionary().getParameterizedType( + getQuery().getSchema().getEntityClass(), + joinField); + + getStitchList().populateLookup(relationshipType, getRelationshipValues(relationshipType, joinFieldIds)); + } + } +} diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLEntityHydrator.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLEntityHydrator.java new file mode 100644 index 0000000000..257c7c40a0 --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLEntityHydrator.java @@ -0,0 +1,74 @@ +/* + * 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.engine; + +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.datastores.aggregation.Query; +import lombok.AccessLevel; +import lombok.Getter; + +import java.util.AbstractMap; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; + +/** + * {@link SQLEntityHydrator} hydrates the entity loaded by {@link SQLQueryEngine#executeQuery(Query)}. + */ +public class SQLEntityHydrator extends AbstractEntityHydrator { + + @Getter(AccessLevel.PRIVATE) + private final EntityManager entityManager; + + /** + * Constructor. + * + * @param results The loaded objects from {@link SQLQueryEngine#executeQuery(Query)} + * @param query The query passed to {@link SQLQueryEngine#executeQuery(Query)} to load the objects + * @param entityDictionary An object that sets entity instance values and provides entity metadata info + * @param entityManager An service that issues JPQL queries to load relationship objects + */ + public SQLEntityHydrator( + List results, + Query query, + EntityDictionary entityDictionary, + EntityManager entityManager + ) { + super(results, query, entityDictionary); + this.entityManager = entityManager; + } + + @Override + protected Map getRelationshipValues( + Class relationshipType, + List joinFieldIds + ) { + if (joinFieldIds.isEmpty()) { + return Collections.emptyMap(); + } + + List uniqueIds = joinFieldIds.stream().distinct().collect(Collectors.toCollection(LinkedList::new)); + + List loaded = getEntityManager() + .createQuery( + String.format( + "SELECT e FROM %s e WHERE %s IN (:idList)", + relationshipType.getCanonicalName(), + getEntityDictionary().getIdFieldName(relationshipType) + ) + ) + .setParameter("idList", uniqueIds) + .getResultList(); + + return loaded.stream() + .map(obj -> new AbstractMap.SimpleImmutableEntry<>((Object) getEntityDictionary().getId(obj), obj)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLQueryEngine.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLQueryEngine.java index 9b01f2af39..170da7a723 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLQueryEngine.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/SQLQueryEngine.java @@ -19,7 +19,6 @@ import com.yahoo.elide.datastores.aggregation.Query; import com.yahoo.elide.datastores.aggregation.QueryEngine; import com.yahoo.elide.datastores.aggregation.dimension.Dimension; -import com.yahoo.elide.datastores.aggregation.dimension.DimensionType; import com.yahoo.elide.datastores.aggregation.engine.annotation.FromSubquery; import com.yahoo.elide.datastores.aggregation.engine.annotation.FromTable; import com.yahoo.elide.datastores.aggregation.engine.schema.SQLSchema; @@ -29,7 +28,7 @@ import com.yahoo.elide.utils.coerce.CoerceUtil; import com.google.common.base.Preconditions; -import org.apache.commons.lang3.mutable.MutableInt; + import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -39,7 +38,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -69,7 +67,7 @@ public SQLQueryEngine(EntityManager entityManager, EntityDictionary dictionary) .stream() .filter((clazz) -> dictionary.getAnnotation(clazz, FromTable.class) != null - || dictionary.getAnnotation(clazz, FromSubquery.class) != null + || dictionary.getAnnotation(clazz, FromSubquery.class) != null ) .collect(Collectors.toMap( Function.identity(), @@ -104,9 +102,10 @@ public Iterable executeQuery(Query query) { supplyFilterQueryParameters(query, pageTotalQuery); //Run the Pagination query and log the time spent. - long total = new TimedFunction<>(() -> { - return CoerceUtil.coerce(pageTotalQuery.getSingleResult(), Long.class); - }, "Running Query: " + paginationSQL).get(); + long total = new TimedFunction<>( + () -> CoerceUtil.coerce(pageTotalQuery.getSingleResult(), Long.class), + "Running Query: " + paginationSQL + ).get(); pagination.setPageTotals(total); } @@ -116,17 +115,9 @@ public Iterable executeQuery(Query query) { supplyFilterQueryParameters(query, jpaQuery); //Run the primary query and log the time spent. - List results = new TimedFunction<>(() -> { - return jpaQuery.getResultList(); - }, "Running Query: " + sql).get(); - + List results = new TimedFunction<>(() -> jpaQuery.getResultList(), "Running Query: " + sql).get(); - //Coerce the results into entity objects. - MutableInt counter = new MutableInt(0); - return results.stream() - .map((result) -> { return result instanceof Object[] ? (Object []) result : new Object[] { result }; }) - .map((result) -> coerceObjectToEntity(query, result, counter)) - .collect(Collectors.toList()); + return new SQLEntityHydrator(results, query, dictionary, entityManager).hydrate(); } /** @@ -149,16 +140,16 @@ protected SQLQuery toSQL(Query query) { if (query.getWhereFilter() != null) { joinPredicates.addAll(extractPathElements(query.getWhereFilter())); - builder.whereClause("WHERE " + translateFilterExpression(schema, query.getWhereFilter(), + builder.whereClause("WHERE " + translateFilterExpression(query.getWhereFilter(), this::generateWhereClauseColumnReference)); } if (query.getHavingFilter() != null) { - builder.havingClause("HAVING " + translateFilterExpression(schema, query.getHavingFilter(), + builder.havingClause("HAVING " + translateFilterExpression(query.getHavingFilter(), (predicate) -> { return generateHavingClauseColumnReference(predicate, query); })); } - if (!query.getDimensions().isEmpty()) { + if (! query.getDimensions().isEmpty()) { builder.groupByClause(extractGroupBy(query)); } @@ -181,67 +172,13 @@ protected SQLQuery toSQL(Query query) { return builder.build(); } - /** - * Coerces results from a JPA query into an Object. - * @param query The client query - * @param result A row from the results. - * @param counter Monotonically increasing number to generate IDs. - * @return A hydrated entity object. - */ - protected Object coerceObjectToEntity(Query query, Object[] result, MutableInt counter) { - Class entityClass = query.getSchema().getEntityClass(); - - //Get all the projections from the client query. - List projections = query.getMetrics().entrySet().stream() - .map(Map.Entry::getKey) - .map(Metric::getName) - .collect(Collectors.toList()); - - projections.addAll(query.getDimensions().stream() - .map(Dimension::getName) - .collect(Collectors.toList())); - - Preconditions.checkArgument(result.length == projections.size()); - - SQLSchema schema = (SQLSchema) query.getSchema(); - - //Construct the object. - Object entityInstance; - try { - entityInstance = entityClass.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { - throw new IllegalStateException(e); - } - - //Populate all of the fields. - for (int idx = 0; idx < result.length; idx++) { - Object value = result[idx]; - String fieldName = projections.get(idx); - - Dimension dim = schema.getDimension(fieldName); - if (dim != null && dim.getDimensionType() == DimensionType.ENTITY) { - //We don't hydrate relationships here. - continue; - } - - dictionary.setValue(entityInstance, fieldName, value); - } - - //Set the ID (it must be coerced from an integer) - dictionary.setValue(entityInstance, dictionary.getIdFieldName(entityClass), counter.getAndIncrement()); - - return entityInstance; - } - /** * Translates a filter expression into SQL. - * @param schema The schema being queried. * @param expression The filter expression * @param columnGenerator A function which generates a column reference in SQL from a FilterPredicate. * @return A SQL expression */ - private String translateFilterExpression(SQLSchema schema, - FilterExpression expression, + private String translateFilterExpression(FilterExpression expression, Function columnGenerator) { FilterTranslator filterVisitor = new FilterTranslator(); @@ -397,11 +334,11 @@ private SQLQuery toPageTotalSQL(SQLQuery sql) { Query clientQuery = sql.getClientQuery(); String groupByDimensions = clientQuery.getDimensions().stream() - .map(Dimension::getName) - .map((name) -> getColumnName(clientQuery.getSchema().getEntityClass(), name)) - .collect(Collectors.joining(",")); + .map(Dimension::getName) + .map((name) -> getColumnName(clientQuery.getSchema().getEntityClass(), name)) + .collect(Collectors.joining(",")); - String projectionClause = String.format("COUNT(DISTINCT(%s))", groupByDimensions); + String projectionClause = String.format("SELECT COUNT(DISTINCT(%s))", groupByDimensions); return SQLQuery.builder() .clientQuery(sql.getClientQuery()) @@ -422,7 +359,7 @@ private String extractProjection(Query query) { .map((entry) -> { Metric metric = entry.getKey(); Class agg = entry.getValue(); - return metric.getMetricExpression(Optional.of(agg)) + " AS " + metric.getName(); + return metric.getMetricExpression(agg) + " AS " + metric.getName(); }) .collect(Collectors.toList()); @@ -455,9 +392,8 @@ private String extractGroupBy(Query query) { .collect(Collectors.toList()); return "GROUP BY " + dimensionProjections.stream() - .map((name) -> query.getSchema().getAlias() + "." + name) - .collect(Collectors.joining(",")); - + .map((name) -> query.getSchema().getAlias() + "." + name) + .collect(Collectors.joining(",")); } /** @@ -481,7 +417,7 @@ private String generateHavingClauseColumnReference(FilterPredicate predicate, Qu Path.PathElement last = predicate.getPath().lastElement().get(); Class lastClass = last.getType(); - if (!lastClass.equals(query.getSchema().getEntityClass())) { + if (! lastClass.equals(query.getSchema().getEntityClass())) { throw new InvalidPredicateException("The having clause can only reference fact table aggregations."); } @@ -489,6 +425,6 @@ private String generateHavingClauseColumnReference(FilterPredicate predicate, Qu Metric metric = schema.getMetric(last.getFieldName()); Class agg = query.getMetrics().get(metric); - return metric.getMetricExpression(Optional.of(agg)); + return metric.getMetricExpression(agg); } } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/StitchList.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/StitchList.java new file mode 100644 index 0000000000..9d8c18dd2c --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/engine/StitchList.java @@ -0,0 +1,159 @@ +/* + * 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.engine; + +import com.yahoo.elide.core.EntityDictionary; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * An auxiliary class for {@link AbstractEntityHydrator} and is responsible for setting relationship values of an entity + * instance. + *

+ * {@link StitchList} should not be subclassed. + */ +public final class StitchList { + /** + * Maps an relationship entity class to a map of object ID to object instance. + *

+ * For example, [Country.class: [340: Country(id:340), 100: Country(id:100)]] + */ + @Getter(AccessLevel.PRIVATE) + private final Map, Map> objectLookups; + + /** + * List of relationships to hydrate + */ + @Getter(AccessLevel.PRIVATE) + private final List todoList; + + @Getter(AccessLevel.PRIVATE) + private final EntityDictionary entityDictionary; + + /** + * A representation of an TODO item in a {@link StitchList}. + */ + @Data + public static class Todo { + private final Object entityInstance; + private final String relationshipName; + private final Object foreignKey; + } + + /** + * Constructor. + * + * @param entityDictionary An object that sets entity instance values and provides entity metadata info + */ + public StitchList(EntityDictionary entityDictionary) { + this.objectLookups = new HashMap<>(); + this.todoList = new ArrayList<>(); + this.entityDictionary = entityDictionary; + } + + /** + * Returns whether or not the entity instances in this {@link StitchList} have relationships that are unset. + * + * @return {@code true} if the entity instances in this {@link StitchList} should be further hydrated because they + * have one or more relationship fields. + */ + public boolean shouldStitch() { + return !getTodoList().isEmpty(); + } + + /** + * Enqueues an entity instance which will be further hydrated on one of its relationship fields later + * + * @param entityInstance The entity instance to be hydrated + * @param fieldName The relationship field to hydrate in the entity instance + * @param value The foreign key between the entity instance and the field entity. + */ + public void todo(Object entityInstance, String fieldName, Object value) { + getTodoList().add(new Todo(entityInstance, fieldName, value)); + } + + /** + * Sets all the relationship values of an requested entity. + *

+ * Values associated with the existing key will be overwritten. + * + * @param relationshipType The type of the relationship to set + * @param idToInstance A map from relationship ID to the actual relationship instance with that ID + */ + public void populateLookup(Class relationshipType, Map idToInstance) { + if (getObjectLookups().containsKey(relationshipType)) { + getObjectLookups().get(relationshipType).putAll(idToInstance); + } else { + getObjectLookups().put(relationshipType, idToInstance); + } + } + + /** + * Stitch all entity instances currently in this {@link StitchList} by setting their relationship fields whose + * values are determined by relationship ID's. + */ + public void stitch() { + for (Todo todo : getTodoList()) { + Object entityInstance = todo.getEntityInstance(); + String relationshipName = todo.getRelationshipName(); + Object foreignKey = todo.getForeignKey(); + + Class relationshipType = getEntityDictionary().getParameterizedType(entityInstance, relationshipName); + Object relationshipValue = getObjectLookups().get(relationshipType).get(foreignKey); + + getEntityDictionary().setValue(entityInstance, relationshipName, relationshipValue); + } + } + + /** + * Returns a mapping from relationship name to an immutable list of foreign key ID objects. + *

+ * For example, given the following {@code todoList}: + *

+     * {@code
+     *     [PlayerStats, country, 344]
+     *     [PlayerStats, country, 840]
+     *     [PlayerStats, country, 344]
+     *     [PlayerStats, player, 1]
+     *     [PlayerStats, player, 1]
+     *     [PlayerStats, player, 1]
+     * }
+     * 
+ * this method returns a map of the following: + *
+     *     [
+     *         "country": [344, 840]
+     *         "player": [1]
+     *     ]
+     * 
+ * + * @return a mapping from relationship name to an ordered list of relationship join ID's + */ + public Map> getHydrationMapping() { + return getTodoList().stream() + .collect( + Collectors.groupingBy( + Todo::getRelationshipName, + Collectors.mapping( + Todo::getForeignKey, + Collectors.collectingAndThen( + Collectors.toCollection(LinkedList::new), + Collections::unmodifiableList + ) + ) + ) + ); + } +} diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metric/AggregatedMetric.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metric/AggregatedMetric.java index f8149c1208..20c3ebea3a 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metric/AggregatedMetric.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metric/AggregatedMetric.java @@ -62,20 +62,16 @@ public AggregatedMetric( } @Override - public String getMetricExpression(final Optional> aggregation) { - if (!aggregation.isPresent()) { - return ""; - } - + public String getMetricExpression(final Class aggregation) { try { - Class clazz = Class.forName(aggregation.get().getCanonicalName()); + Class clazz = Class.forName(aggregation.getCanonicalName()); Constructor ctor = clazz.getConstructor(); Aggregation instance = (Aggregation) ctor.newInstance(); return String.format(instance.getAggFunctionFormat(), schema.getAlias() + "." + name); } catch (Exception exception) { String message = String.format( "Cannot generate aggregation function for '%s'", - aggregation.get().getCanonicalName() + aggregation.getCanonicalName() ); log.error(message, exception); throw new IllegalStateException(message, exception); diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metric/Metric.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metric/Metric.java index 049df39e0f..d7152adefc 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metric/Metric.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metric/Metric.java @@ -11,7 +11,6 @@ import java.io.Serializable; import java.util.List; -import java.util.Optional; /** * Elide's definition of metric. @@ -52,9 +51,10 @@ public interface Metric extends Serializable { /** * Returns a metric expression that represents a specified aggregation. * + * @param aggregation aggregation type to be applied * @return a arithmetic formula for computing this {@link Metric} or default aggregation UDF on a base/simple metric */ - String getMetricExpression(Optional> aggregation); + String getMetricExpression(Class aggregation); /** * Returns a list of supported aggregations with the first as the default aggregation. diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/SchemaTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/SchemaTest.java index 1a3d989250..417f711354 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/SchemaTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/SchemaTest.java @@ -5,6 +5,10 @@ */ package com.yahoo.elide.datastores.aggregation; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.datastores.aggregation.annotation.CardinalitySize; import com.yahoo.elide.datastores.aggregation.example.Country; @@ -14,21 +18,18 @@ import com.yahoo.elide.datastores.aggregation.metric.Max; import com.yahoo.elide.datastores.aggregation.schema.Schema; - -import org.testng.Assert; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import java.util.Collections; -import java.util.Optional; public class SchemaTest { - private EntityDictionary entityDictionary; - private Schema playerStatsSchema; + private static EntityDictionary entityDictionary; + private static Schema playerStatsSchema; - @BeforeMethod - public void setupEntityDictionary() { + @BeforeAll + public static void setupEntityDictionary() { entityDictionary = new EntityDictionary(Collections.emptyMap()); entityDictionary.bindEntity(Country.class); entityDictionary.bindEntity(VideoGame.class); @@ -38,21 +39,22 @@ public void setupEntityDictionary() { playerStatsSchema = new Schema(PlayerStats.class, entityDictionary); } - @Test void testMetricCheck() { - Assert.assertTrue(playerStatsSchema.isMetricField("highScore")); - Assert.assertFalse(playerStatsSchema.isMetricField("country")); + @Test + public void testMetricCheck() { + assertTrue(playerStatsSchema.isMetricField("highScore")); + assertFalse(playerStatsSchema.isMetricField("country")); } @Test public void testGetDimension() { - Assert.assertEquals(playerStatsSchema.getDimension("country").getCardinality(), CardinalitySize.SMALL); + assertEquals(CardinalitySize.SMALL, playerStatsSchema.getDimension("country").getCardinality()); } @Test public void testGetMetric() { - Assert.assertEquals( - playerStatsSchema.getMetric("highScore").getMetricExpression(Optional.of(Max.class)), - "MAX(com_yahoo_elide_datastores_aggregation_example_PlayerStats.highScore)" + assertEquals( + "MAX(com_yahoo_elide_datastores_aggregation_example_PlayerStats.highScore)", + playerStatsSchema.getMetric("highScore").getMetricExpression(Max.class) ); } } diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/dimension/DimensionTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/dimension/DimensionTest.java index bbafa9db59..46e187c18f 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/dimension/DimensionTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/dimension/DimensionTest.java @@ -5,15 +5,15 @@ */ package com.yahoo.elide.datastores.aggregation.dimension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.Mockito.mock; import com.yahoo.elide.datastores.aggregation.annotation.CardinalitySize; import com.yahoo.elide.datastores.aggregation.example.Country; import com.yahoo.elide.datastores.aggregation.schema.Schema; import com.yahoo.elide.datastores.aggregation.time.TimeGrain; - -import org.testng.Assert; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; import java.util.HashSet; import java.util.Set; @@ -55,16 +55,16 @@ public class DimensionTest { @Test public void testDimensionAsCollectionElement() { - Assert.assertEquals(ENTITY_DIMENSION, ENTITY_DIMENSION); - Assert.assertEquals(DEGENERATE_DIMENSION, DEGENERATE_DIMENSION); - Assert.assertNotEquals(ENTITY_DIMENSION, DEGENERATE_DIMENSION); - Assert.assertNotEquals(ENTITY_DIMENSION.hashCode(), DEGENERATE_DIMENSION.hashCode()); + assertEquals(ENTITY_DIMENSION, ENTITY_DIMENSION); + assertEquals(DEGENERATE_DIMENSION, DEGENERATE_DIMENSION); + assertNotEquals(DEGENERATE_DIMENSION, ENTITY_DIMENSION); + assertNotEquals(DEGENERATE_DIMENSION.hashCode(), ENTITY_DIMENSION.hashCode()); // different dimensions should be separate elements in Set Set dimensions = new HashSet<>(); dimensions.add(ENTITY_DIMENSION); - Assert.assertEquals(dimensions.size(), 1); + assertEquals(1, dimensions.size()); // a separate same object doesn't increase collection size Dimension sameEntityDimension = new EntityDimension( @@ -75,35 +75,35 @@ public void testDimensionAsCollectionElement() { CardinalitySize.SMALL, "name" ); - Assert.assertEquals(sameEntityDimension, ENTITY_DIMENSION); + assertEquals(ENTITY_DIMENSION, sameEntityDimension); dimensions.add(sameEntityDimension); - Assert.assertEquals(dimensions.size(), 1); + assertEquals(1, dimensions.size()); dimensions.add(ENTITY_DIMENSION); - Assert.assertEquals(dimensions.size(), 1); + assertEquals(1, dimensions.size()); dimensions.add(DEGENERATE_DIMENSION); - Assert.assertEquals(dimensions.size(), 2); + assertEquals(2, dimensions.size()); dimensions.add(TIME_DIMENSION); - Assert.assertEquals(dimensions.size(), 3); + assertEquals(3, dimensions.size()); } @Test public void testToString() { // table dimension - Assert.assertEquals( + assertEquals( ENTITY_DIMENSION.toString(), "EntityDimension[name='country', longName='country', description='country', dimensionType=ENTITY, dataType=Country, cardinality=SMALL, friendlyName='name']" ); // degenerate dimension - Assert.assertEquals( + assertEquals( DEGENERATE_DIMENSION.toString(), "DegenerateDimension[columnType=FIELD, name='overallRating', longName='overallRating', description='overallRating', dimensionType=DEGENERATE, dataType=String, cardinality=SMALL, friendlyName='overallRating']" ); - Assert.assertEquals( + assertEquals( TIME_DIMENSION.toString(), "TimeDimension[timeZone=Pacific Standard Time, timeGrain=DAY, columnType=TEMPORAL, name='recordedTime', longName='recordedTime', description='recordedTime', dimensionType=DEGENERATE, dataType=class java.lang.Long, cardinality=LARGE, friendlyName='recordedTime']" ); diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/dimension/EntityDimensionTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/dimension/EntityDimensionTest.java index 64b9856862..e3e5c17f37 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/dimension/EntityDimensionTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/dimension/EntityDimensionTest.java @@ -5,6 +5,9 @@ */ package com.yahoo.elide.datastores.aggregation.dimension; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + import com.yahoo.elide.annotation.Include; import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.datastores.aggregation.annotation.CardinalitySize; @@ -12,16 +15,15 @@ import com.yahoo.elide.datastores.aggregation.example.Country; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; import com.yahoo.elide.datastores.aggregation.example.VideoGame; - -import org.testng.Assert; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import java.util.Collections; import javax.persistence.Entity; public class EntityDimensionTest { + private static EntityDictionary entityDictionary; /** * A class for testing un-happy path on finding friendly name. @@ -52,10 +54,8 @@ public String getSubTitle() { } } - private EntityDictionary entityDictionary; - - @BeforeMethod - public void setupEntityDictionary() { + @BeforeAll + public static void setupEntityDictionary() { entityDictionary = new EntityDictionary(Collections.emptyMap()); entityDictionary.bindEntity(PlayerStats.class); entityDictionary.bindEntity(Country.class); @@ -66,44 +66,46 @@ public void setupEntityDictionary() { @Test public void testHappyPathFriendlyNameScan() { // 1 field with @FriendlyName - Assert.assertEquals( - EntityDimension.getFriendlyNameField(PlayerStats.class, entityDictionary), - "overallRating" + assertEquals( + "overallRating", + EntityDimension.getFriendlyNameField(PlayerStats.class, entityDictionary) ); // no field with @FriendlyName - Assert.assertEquals( - EntityDimension.getFriendlyNameField(VideoGame.class, entityDictionary), - "id" + assertEquals( + "id", + EntityDimension.getFriendlyNameField(VideoGame.class, entityDictionary) ); } /** * Multiple {@link FriendlyName} annotations in entity is illegal. */ - @Test(expectedExceptions = IllegalStateException.class) + @Test public void testUnhappyPathFriendlyNameScan() { - EntityDimension.getFriendlyNameField(Book.class, entityDictionary); + assertThrows( + IllegalStateException.class, + () -> EntityDimension.getFriendlyNameField(Book.class, entityDictionary)); } @Test public void testCardinalityScan() { // annotation on entity - Assert.assertEquals( - EntityDimension.getEstimatedCardinality("country", PlayerStats.class, entityDictionary), - CardinalitySize.SMALL + assertEquals( + CardinalitySize.SMALL, + EntityDimension.getEstimatedCardinality("country", PlayerStats.class, entityDictionary) ); // annotation on field - Assert.assertEquals( - EntityDimension.getEstimatedCardinality("overallRating", PlayerStats.class, entityDictionary), - CardinalitySize.MEDIUM + assertEquals( + CardinalitySize.MEDIUM, + EntityDimension.getEstimatedCardinality("overallRating", PlayerStats.class, entityDictionary) ); // default is used - Assert.assertEquals( - EntityDimension.getEstimatedCardinality("recordedDate", PlayerStats.class, entityDictionary), - EntityDimension.getDefaultCardinality() + assertEquals( + EntityDimension.getDefaultCardinality(), + EntityDimension.getEstimatedCardinality("recordedDate", PlayerStats.class, entityDictionary) ); } } diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/engine/SQLQueryEngineTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/engine/SQLQueryEngineTest.java index bc69f7a953..bc930b3a75 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/engine/SQLQueryEngineTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/engine/SQLQueryEngineTest.java @@ -6,6 +6,9 @@ package com.yahoo.elide.datastores.aggregation.engine; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.filter.dialect.RSQLFilterDialect; import com.yahoo.elide.core.pagination.Pagination; @@ -20,8 +23,8 @@ import com.yahoo.elide.datastores.aggregation.example.PlayerStatsView; import com.yahoo.elide.datastores.aggregation.metric.Sum; import com.yahoo.elide.datastores.aggregation.schema.Schema; -import org.testng.Assert; -import org.testng.annotations.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import java.sql.Timestamp; import java.util.HashMap; @@ -35,15 +38,14 @@ import javax.persistence.Persistence; public class SQLQueryEngineTest { - - private EntityManagerFactory emf; - - private Schema playerStatsSchema; - private Schema playerStatsViewSchema; - private EntityDictionary dictionary; - private RSQLFilterDialect filterParser; - - public SQLQueryEngineTest() { + private static EntityManagerFactory emf; + private static Schema playerStatsSchema; + private static Schema playerStatsViewSchema; + private static EntityDictionary dictionary; + private static RSQLFilterDialect filterParser; + + @BeforeAll + public static void init() { emf = Persistence.createEntityManagerFactory("aggregationStore"); dictionary = new EntityDictionary(new HashMap<>()); dictionary.bindEntity(PlayerStats.class); @@ -56,8 +58,11 @@ public SQLQueryEngineTest() { playerStatsViewSchema = new SQLSchema(PlayerStatsView.class, dictionary); } + /** + * Test loading all three records from the table. + */ @Test - public void testFullTableLoad() throws Exception { + public void testFullTableLoad() { EntityManager em = emf.createEntityManager(); QueryEngine engine = new SQLQueryEngine(em, dictionary); @@ -65,33 +70,41 @@ public void testFullTableLoad() throws Exception { .schema(playerStatsSchema) .metric(playerStatsSchema.getMetric("lowScore"), Sum.class) .metric(playerStatsSchema.getMetric("highScore"), Sum.class) - .groupDimension(playerStatsSchema.getDimension("overallRating")) .timeDimension((TimeDimension) playerStatsSchema.getDimension("recordedDate")) .build(); List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) .collect(Collectors.toList()); - //Jon Doe,1234,72,Good,840,2019-07-12 00:00:00 + PlayerStats stats0 = new PlayerStats(); + stats0.setId("0"); + stats0.setLowScore(241); + stats0.setHighScore(2412); + stats0.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); + PlayerStats stats1 = new PlayerStats(); - stats1.setId("0"); + stats1.setId("1"); stats1.setLowScore(72); stats1.setHighScore(1234); - stats1.setOverallRating("Good"); stats1.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00")); PlayerStats stats2 = new PlayerStats(); - stats2.setId("1"); - stats2.setLowScore(241); - stats2.setHighScore(2412); - stats2.setOverallRating("Great"); - stats2.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); + stats2.setId("2"); + stats2.setLowScore(72); + stats2.setHighScore(1000); + stats2.setRecordedDate(Timestamp.valueOf("2019-07-13 00:00:00")); - Assert.assertEquals(results.size(), 2); - Assert.assertEquals(results.get(0), stats1); - Assert.assertEquals(results.get(1), stats2); + assertEquals(3, results.size()); + assertEquals(stats0, results.get(0)); + assertEquals(stats1, results.get(1)); + assertEquals(stats2, results.get(2)); } + /** + * Test group by a degenerate dimension with a filter applied. + * + * @throws Exception exception + */ @Test public void testDegenerateDimensionFilter() throws Exception { EntityManager em = emf.createEntityManager(); @@ -117,10 +130,15 @@ public void testDegenerateDimensionFilter() throws Exception { stats1.setOverallRating("Great"); stats1.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); - Assert.assertEquals(results.size(), 1); - Assert.assertEquals(results.get(0), stats1); + assertEquals(1, results.size()); + assertEquals(stats1, results.get(0)); } + /** + * Test filtering on a dimension attribute. + * + * @throws Exception exception + */ @Test public void testFilterJoin() throws Exception { EntityManager em = emf.createEntityManager(); @@ -131,6 +149,7 @@ public void testFilterJoin() throws Exception { .metric(playerStatsSchema.getMetric("lowScore"), Sum.class) .metric(playerStatsSchema.getMetric("highScore"), Sum.class) .groupDimension(playerStatsSchema.getDimension("overallRating")) + .groupDimension(playerStatsSchema.getDimension("country")) .timeDimension((TimeDimension) playerStatsSchema.getDimension("recordedDate")) .whereFilter(filterParser.parseFilterExpression("country.name=='United States'", PlayerStats.class, false)) @@ -139,25 +158,42 @@ public void testFilterJoin() throws Exception { List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) .collect(Collectors.toList()); - PlayerStats stats1 = new PlayerStats(); - stats1.setId("0"); - stats1.setLowScore(72); - stats1.setHighScore(1234); - stats1.setOverallRating("Good"); - stats1.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00")); - - PlayerStats stats2 = new PlayerStats(); - stats2.setId("1"); - stats2.setLowScore(241); - stats2.setHighScore(2412); - stats2.setOverallRating("Great"); - stats2.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); - - Assert.assertEquals(results.size(), 2); - Assert.assertEquals(results.get(0), stats1); - Assert.assertEquals(results.get(1), stats2); + Country expectedCountry = new Country(); + expectedCountry.setId("840"); + expectedCountry.setIsoCode("USA"); + expectedCountry.setName("United States"); + + + PlayerStats usa0 = new PlayerStats(); + usa0.setId("0"); + usa0.setLowScore(241); + usa0.setHighScore(2412); + usa0.setOverallRating("Great"); + usa0.setCountry(expectedCountry); + usa0.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); + + PlayerStats usa1 = new PlayerStats(); + usa1.setId("1"); + usa1.setLowScore(72); + usa1.setHighScore(1234); + usa1.setOverallRating("Good"); + usa1.setCountry(expectedCountry); + usa1.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00")); + + assertEquals(2, results.size()); + assertEquals(usa0, results.get(0)); + assertEquals(usa1, results.get(1)); + + // test join + PlayerStats actualStats1 = (PlayerStats) results.get(0); + assertNotNull(actualStats1.getCountry()); } + /** + * Test filtering on an attribute that's not present in the query. + * + * @throws Exception exception + */ @Test public void testSubqueryFilterJoin() throws Exception { EntityManager em = emf.createEntityManager(); @@ -177,10 +213,15 @@ public void testSubqueryFilterJoin() throws Exception { stats2.setId("0"); stats2.setHighScore(2412); - Assert.assertEquals(results.size(), 1); - Assert.assertEquals(results.get(0), stats2); + assertEquals(1, results.size()); + assertEquals(stats2, results.get(0)); } + /** + * Test a view which filters on "stats.overallRating = 'Great'". + * + * @throws Exception exception + */ @Test public void testSubqueryLoad() throws Exception { EntityManager em = emf.createEntityManager(); @@ -198,12 +239,15 @@ public void testSubqueryLoad() throws Exception { stats2.setId("0"); stats2.setHighScore(2412); - Assert.assertEquals(results.size(), 1); - Assert.assertEquals(results.get(0), stats2); + assertEquals(1, results.size()); + assertEquals(stats2, results.get(0)); } + /** + * Test sorting by dimension attribute which is not present in the query. + */ @Test - public void testSortJoin() throws Exception { + public void testSortJoin() { EntityManager em = emf.createEntityManager(); QueryEngine engine = new SQLQueryEngine(em, dictionary); @@ -221,25 +265,35 @@ public void testSortJoin() throws Exception { List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) .collect(Collectors.toList()); + PlayerStats stats0 = new PlayerStats(); + stats0.setId("0"); + stats0.setLowScore(72); + stats0.setOverallRating("Good"); + stats0.setRecordedDate(Timestamp.valueOf("2019-07-13 00:00:00")); + PlayerStats stats1 = new PlayerStats(); - stats1.setId("0"); + stats1.setId("1"); stats1.setLowScore(241); stats1.setOverallRating("Great"); stats1.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); PlayerStats stats2 = new PlayerStats(); - stats2.setId("1"); + stats2.setId("2"); stats2.setLowScore(72); stats2.setOverallRating("Good"); stats2.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00")); - Assert.assertEquals(results.size(), 2); - Assert.assertEquals(results.get(0), stats1); - Assert.assertEquals(results.get(1), stats2); + assertEquals(3, results.size()); + assertEquals(stats0, results.get(0)); + assertEquals(stats1, results.get(1)); + assertEquals(stats2, results.get(2)); } + /** + * Test pagination. + */ @Test - public void testPagination() throws Exception { + public void testPagination() { EntityManager em = emf.createEntityManager(); QueryEngine engine = new SQLQueryEngine(em, dictionary); @@ -265,11 +319,16 @@ public void testPagination() throws Exception { stats1.setOverallRating("Good"); stats1.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00")); - Assert.assertEquals(results.size(), 1, "Number of records returned does not match"); - Assert.assertEquals(results.get(0), stats1, "Returned record does not match"); - Assert.assertEquals(pagination.getPageTotals(), 2, "Page totals does not match"); + assertEquals(results.size(), 1, "Number of records returned does not match"); + assertEquals(results.get(0), stats1, "Returned record does not match"); + assertEquals(pagination.getPageTotals(), 3, "Page totals does not match"); } + /** + * Test having clause integrates with group by clause. + * + * @throws Exception exception + */ @Test public void testHavingClause() throws Exception { EntityManager em = emf.createEntityManager(); @@ -278,22 +337,29 @@ public void testHavingClause() throws Exception { Query query = Query.builder() .schema(playerStatsSchema) .metric(playerStatsSchema.getMetric("highScore"), Sum.class) - .havingFilter(filterParser.parseFilterExpression("highScore > 300", + .groupDimension(playerStatsSchema.getDimension("overallRating")) + .havingFilter(filterParser.parseFilterExpression("highScore < 2400", PlayerStats.class, false)) .build(); List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) .collect(Collectors.toList()); - //Jon Doe,1234,72,Good,840,2019-07-12 00:00:00 + // Only "Good" rating would have total high score less than 2400 PlayerStats stats1 = new PlayerStats(); stats1.setId("0"); - stats1.setHighScore(3646); + stats1.setOverallRating("Good"); + stats1.setHighScore(2234); - Assert.assertEquals(results.size(), 1); - Assert.assertEquals(results.get(0), stats1); + assertEquals(1, results.size()); + assertEquals(stats1, results.get(0)); } + /** + * Test group by, having, dimension, metric at the same time. + * + * @throws Exception exception + */ @Test public void testTheEverythingQuery() throws Exception { EntityManager em = emf.createEntityManager(); @@ -321,13 +387,15 @@ public void testTheEverythingQuery() throws Exception { stats2.setHighScore(2412); stats2.setCountryName("United States"); - - Assert.assertEquals(results.size(), 1); - Assert.assertEquals(results.get(0), stats2); + assertEquals(1, results.size()); + assertEquals(stats2, results.get(0)); } + /** + * Test sorting by two different columns-one metric and one dimension. + */ @Test - public void testSortByMultipleColumns() throws Exception { + public void testSortByMultipleColumns() { EntityManager em = emf.createEntityManager(); QueryEngine engine = new SQLQueryEngine(em, dictionary); @@ -346,20 +414,95 @@ public void testSortByMultipleColumns() throws Exception { 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"); + stats0.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); + PlayerStats stats1 = new PlayerStats(); - stats1.setId("0"); - stats1.setLowScore(241); - stats1.setOverallRating("Great"); - stats1.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); + stats1.setId("1"); + stats1.setLowScore(72); + stats1.setOverallRating("Good"); + stats1.setRecordedDate(Timestamp.valueOf("2019-07-13 00:00:00")); PlayerStats stats2 = new PlayerStats(); - stats2.setId("1"); + stats2.setId("2"); stats2.setLowScore(72); stats2.setOverallRating("Good"); stats2.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00")); - Assert.assertEquals(results.size(), 2); - Assert.assertEquals(results.get(0), stats1); - Assert.assertEquals(results.get(1), stats2); + assertEquals(3, results.size()); + assertEquals(stats0, results.get(0)); + assertEquals(stats1, results.get(1)); + assertEquals(stats2, results.get(2)); + } + + /** + * Test hydrating multiple relationship values. Make sure the objects are constructed correctly. + */ + @Test + public void testRelationshipHydration() { + EntityManager em = emf.createEntityManager(); + QueryEngine engine = new SQLQueryEngine(em, dictionary); + + Map sortMap = new TreeMap<>(); + sortMap.put("country.name", Sorting.SortOrder.desc); + + Query query = Query.builder() + .schema(playerStatsSchema) + .metric(playerStatsSchema.getMetric("lowScore"), Sum.class) + .metric(playerStatsSchema.getMetric("highScore"), Sum.class) + .groupDimension(playerStatsSchema.getDimension("overallRating")) + .groupDimension(playerStatsSchema.getDimension("country")) + .timeDimension((TimeDimension) playerStatsSchema.getDimension("recordedDate")) + .sorting(new Sorting(sortMap)) + .build(); + + List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) + .collect(Collectors.toList()); + + Country usa = new Country(); + usa.setId("840"); + usa.setIsoCode("USA"); + usa.setName("United States"); + + Country hk = new Country(); + hk.setId("344"); + hk.setIsoCode("HKG"); + hk.setName("Hong Kong"); + + 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(72); + 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(hk); + hk2.setRecordedDate(Timestamp.valueOf("2019-07-13 00:00:00")); + + assertEquals(3, results.size()); + assertEquals(usa0, results.get(0)); + assertEquals(usa1, results.get(1)); + assertEquals(hk2, results.get(2)); + + // test join + PlayerStats actualStats1 = (PlayerStats) results.get(0); + assertNotNull(actualStats1.getCountry()); } } 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 f9804b308b..6da056c26f 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 @@ -10,12 +10,15 @@ 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; /** * A root level entity for testing AggregationDataStore. */ +@Data @Entity @Include(rootLevel = true) @Cardinality(size = CardinalitySize.SMALL) diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/FilterConstraintsTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/FilterConstraintsTest.java index 89ef468efd..b0195b60ac 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/FilterConstraintsTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/FilterConstraintsTest.java @@ -5,13 +5,16 @@ */ package com.yahoo.elide.datastores.aggregation.filter.visitor; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.yahoo.elide.core.Path; import com.yahoo.elide.core.filter.FilterPredicate; import com.yahoo.elide.core.filter.Operator; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; - -import org.testng.Assert; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; import java.util.Collections; @@ -30,37 +33,37 @@ public class FilterConstraintsTest { @Test public void testPureHaving() { - Assert.assertTrue(FilterConstraints.pureHaving(HAVING_PREDICATE).isPureHaving()); - Assert.assertFalse(FilterConstraints.pureHaving(HAVING_PREDICATE).isPureWhere()); - Assert.assertEquals( - FilterConstraints.pureHaving(HAVING_PREDICATE).getHavingExpression().toString(), - "playerStats.highScore GT [99]" + assertTrue(FilterConstraints.pureHaving(HAVING_PREDICATE).isPureHaving()); + assertFalse(FilterConstraints.pureHaving(HAVING_PREDICATE).isPureWhere()); + assertEquals( + "playerStats.highScore GT [99]", + FilterConstraints.pureHaving(HAVING_PREDICATE).getHavingExpression().toString() ); - Assert.assertNull(FilterConstraints.pureHaving(HAVING_PREDICATE).getWhereExpression()); + assertNull(FilterConstraints.pureHaving(HAVING_PREDICATE).getWhereExpression()); } @Test public void testPureWhere() { - Assert.assertTrue(FilterConstraints.pureWhere(WHERE_PREDICATE).isPureWhere()); - Assert.assertFalse(FilterConstraints.pureWhere(WHERE_PREDICATE).isPureHaving()); - Assert.assertEquals( - FilterConstraints.pureWhere(WHERE_PREDICATE).getWhereExpression().toString(), - "playerStats.id IN [foo]" + assertTrue(FilterConstraints.pureWhere(WHERE_PREDICATE).isPureWhere()); + assertFalse(FilterConstraints.pureWhere(WHERE_PREDICATE).isPureHaving()); + assertEquals( + "playerStats.id IN [foo]", + FilterConstraints.pureWhere(WHERE_PREDICATE).getWhereExpression().toString() ); - Assert.assertNull(FilterConstraints.pureWhere(WHERE_PREDICATE).getHavingExpression()); + assertNull(FilterConstraints.pureWhere(WHERE_PREDICATE).getHavingExpression()); } @Test public void testWithWhereAndHaving() { - Assert.assertFalse(FilterConstraints.withWhereAndHaving(WHERE_PREDICATE, HAVING_PREDICATE).isPureWhere()); - Assert.assertFalse(FilterConstraints.withWhereAndHaving(WHERE_PREDICATE, HAVING_PREDICATE).isPureHaving()); - Assert.assertEquals( - FilterConstraints.withWhereAndHaving(WHERE_PREDICATE, HAVING_PREDICATE).getWhereExpression().toString(), - "playerStats.id IN [foo]" + assertFalse(FilterConstraints.withWhereAndHaving(WHERE_PREDICATE, HAVING_PREDICATE).isPureWhere()); + assertFalse(FilterConstraints.withWhereAndHaving(WHERE_PREDICATE, HAVING_PREDICATE).isPureHaving()); + assertEquals( + "playerStats.id IN [foo]", + FilterConstraints.withWhereAndHaving(WHERE_PREDICATE, HAVING_PREDICATE).getWhereExpression().toString() ); - Assert.assertEquals( - FilterConstraints.withWhereAndHaving(WHERE_PREDICATE, HAVING_PREDICATE).getHavingExpression().toString(), - "playerStats.highScore GT [99]" + assertEquals( + "playerStats.highScore GT [99]", + FilterConstraints.withWhereAndHaving(WHERE_PREDICATE, HAVING_PREDICATE).getHavingExpression().toString() ); } } diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/SplitFilterExpressionVisitorTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/SplitFilterExpressionVisitorTest.java index 441437b24b..834d1ec16e 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/SplitFilterExpressionVisitorTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/SplitFilterExpressionVisitorTest.java @@ -5,6 +5,11 @@ */ package com.yahoo.elide.datastores.aggregation.filter.visitor; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.Path; import com.yahoo.elide.core.filter.FilterPredicate; @@ -17,10 +22,8 @@ import com.yahoo.elide.datastores.aggregation.example.Player; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; import com.yahoo.elide.datastores.aggregation.schema.Schema; - -import org.testng.Assert; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import java.util.Collections; @@ -37,12 +40,12 @@ public class SplitFilterExpressionVisitorTest { Collections.singletonList(99) ); - private EntityDictionary entityDictionary; - private Schema schema; - private FilterExpressionVisitor splitFilterExpressionVisitor; + private static EntityDictionary entityDictionary; + private static Schema schema; + private static FilterExpressionVisitor splitFilterExpressionVisitor; - @BeforeMethod - public void setupEntityDictionary() { + @BeforeAll + public static void setupEntityDictionary() { entityDictionary = new EntityDictionary(Collections.emptyMap()); entityDictionary.bindEntity(PlayerStats.class); entityDictionary.bindEntity(Country.class); @@ -54,78 +57,78 @@ public void setupEntityDictionary() { @Test public void testVisitPredicate() { // predicate should be a WHERE - Assert.assertTrue(splitFilterExpressionVisitor.visitPredicate(WHERE_PREDICATE).isPureWhere()); - Assert.assertEquals( - splitFilterExpressionVisitor.visitPredicate(WHERE_PREDICATE).getWhereExpression().toString(), - "playerStats.id IN [foo]" + assertTrue(splitFilterExpressionVisitor.visitPredicate(WHERE_PREDICATE).isPureWhere()); + assertEquals( + "playerStats.id IN [foo]", + splitFilterExpressionVisitor.visitPredicate(WHERE_PREDICATE).getWhereExpression().toString() ); - Assert.assertFalse(splitFilterExpressionVisitor.visitPredicate(WHERE_PREDICATE).isPureHaving()); - Assert.assertNull(splitFilterExpressionVisitor.visitPredicate(WHERE_PREDICATE).getHavingExpression()); + assertFalse(splitFilterExpressionVisitor.visitPredicate(WHERE_PREDICATE).isPureHaving()); + assertNull(splitFilterExpressionVisitor.visitPredicate(WHERE_PREDICATE).getHavingExpression()); // predicate should be a HAVING - Assert.assertTrue(splitFilterExpressionVisitor.visitPredicate(HAVING_PREDICATE).isPureHaving()); - Assert.assertEquals( - splitFilterExpressionVisitor.visitPredicate(HAVING_PREDICATE).getHavingExpression().toString(), - "playerStats.highScore GT [99]" + assertTrue(splitFilterExpressionVisitor.visitPredicate(HAVING_PREDICATE).isPureHaving()); + assertEquals( + "playerStats.highScore GT [99]", + splitFilterExpressionVisitor.visitPredicate(HAVING_PREDICATE).getHavingExpression().toString() ); - Assert.assertFalse(splitFilterExpressionVisitor.visitPredicate(HAVING_PREDICATE).isPureWhere()); - Assert.assertNull(splitFilterExpressionVisitor.visitPredicate(HAVING_PREDICATE).getWhereExpression()); + assertFalse(splitFilterExpressionVisitor.visitPredicate(HAVING_PREDICATE).isPureWhere()); + assertNull(splitFilterExpressionVisitor.visitPredicate(HAVING_PREDICATE).getWhereExpression()); } @Test public void testVisitAndExpression() { // pure-W AND pure-W AndFilterExpression filterExpression = new AndFilterExpression(WHERE_PREDICATE, WHERE_PREDICATE); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(filterExpression).getWhereExpression().toString(), - "(playerStats.id IN [foo] AND playerStats.id IN [foo])" + assertEquals( + "(playerStats.id IN [foo] AND playerStats.id IN [foo])", + splitFilterExpressionVisitor.visitAndExpression(filterExpression).getWhereExpression().toString() ); - Assert.assertNull(splitFilterExpressionVisitor.visitAndExpression(filterExpression).getHavingExpression()); + assertNull(splitFilterExpressionVisitor.visitAndExpression(filterExpression).getHavingExpression()); // pure-H AND pure-W filterExpression = new AndFilterExpression(HAVING_PREDICATE, WHERE_PREDICATE); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(filterExpression).getWhereExpression().toString(), - "playerStats.id IN [foo]" + assertEquals( + "playerStats.id IN [foo]", + splitFilterExpressionVisitor.visitAndExpression(filterExpression).getWhereExpression().toString() ); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(filterExpression).getHavingExpression().toString(), - "playerStats.highScore GT [99]" + assertEquals( + "playerStats.highScore GT [99]", + splitFilterExpressionVisitor.visitAndExpression(filterExpression).getHavingExpression().toString() ); // pure-W AND pure-H filterExpression = new AndFilterExpression(WHERE_PREDICATE, HAVING_PREDICATE); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(filterExpression).getWhereExpression().toString(), - "playerStats.id IN [foo]" + assertEquals( + "playerStats.id IN [foo]", + splitFilterExpressionVisitor.visitAndExpression(filterExpression).getWhereExpression().toString() ); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(filterExpression).getHavingExpression().toString(), - "playerStats.highScore GT [99]" + assertEquals( + "playerStats.highScore GT [99]", + splitFilterExpressionVisitor.visitAndExpression(filterExpression).getHavingExpression().toString() ); // non-pure case - H1 AND W1 AND H2 AndFilterExpression and1 = new AndFilterExpression(HAVING_PREDICATE, WHERE_PREDICATE); AndFilterExpression and2 = new AndFilterExpression(and1, HAVING_PREDICATE); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(and2).getWhereExpression().toString(), - "playerStats.id IN [foo]" + assertEquals( + "playerStats.id IN [foo]", + splitFilterExpressionVisitor.visitAndExpression(and2).getWhereExpression().toString() ); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(and2).getHavingExpression().toString(), - "(playerStats.highScore GT [99] AND playerStats.highScore GT [99])" + assertEquals( + "(playerStats.highScore GT [99] AND playerStats.highScore GT [99])", + splitFilterExpressionVisitor.visitAndExpression(and2).getHavingExpression().toString() ); // non-pure case - (H1 OR H2) AND W1 OrFilterExpression or = new OrFilterExpression(HAVING_PREDICATE, HAVING_PREDICATE); AndFilterExpression and = new AndFilterExpression(or, WHERE_PREDICATE); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(and).getWhereExpression().toString(), - "playerStats.id IN [foo]" + assertEquals( + "playerStats.id IN [foo]", + splitFilterExpressionVisitor.visitAndExpression(and).getWhereExpression().toString() ); - Assert.assertEquals( - splitFilterExpressionVisitor.visitAndExpression(and).getHavingExpression().toString(), - "(playerStats.highScore GT [99] OR playerStats.highScore GT [99])" + assertEquals( + "(playerStats.highScore GT [99] OR playerStats.highScore GT [99])", + splitFilterExpressionVisitor.visitAndExpression(and).getHavingExpression().toString() ); } @@ -133,27 +136,27 @@ public void testVisitAndExpression() { public void testVisitOrExpression() { // pure-W OR pure-W OrFilterExpression filterExpression = new OrFilterExpression(WHERE_PREDICATE, WHERE_PREDICATE); - Assert.assertEquals( - splitFilterExpressionVisitor.visitOrExpression(filterExpression).getWhereExpression().toString(), - "(playerStats.id IN [foo] OR playerStats.id IN [foo])" + assertEquals( + "(playerStats.id IN [foo] OR playerStats.id IN [foo])", + splitFilterExpressionVisitor.visitOrExpression(filterExpression).getWhereExpression().toString() ); - Assert.assertNull(splitFilterExpressionVisitor.visitOrExpression(filterExpression).getHavingExpression()); + assertNull(splitFilterExpressionVisitor.visitOrExpression(filterExpression).getHavingExpression()); // H1 OR W1 OrFilterExpression or = new OrFilterExpression(HAVING_PREDICATE, WHERE_PREDICATE); - Assert.assertNull(splitFilterExpressionVisitor.visitOrExpression(or).getWhereExpression()); - Assert.assertEquals( - splitFilterExpressionVisitor.visitOrExpression(or).getHavingExpression().toString(), - "(playerStats.highScore GT [99] OR playerStats.id IN [foo])" + assertNull(splitFilterExpressionVisitor.visitOrExpression(or).getWhereExpression()); + assertEquals( + "(playerStats.highScore GT [99] OR playerStats.id IN [foo])", + splitFilterExpressionVisitor.visitOrExpression(or).getHavingExpression().toString() ); // (W1 AND H1) OR W2 AndFilterExpression and = new AndFilterExpression(WHERE_PREDICATE, HAVING_PREDICATE); or = new OrFilterExpression(and, WHERE_PREDICATE); - Assert.assertNull(splitFilterExpressionVisitor.visitOrExpression(or).getWhereExpression()); - Assert.assertEquals( - splitFilterExpressionVisitor.visitOrExpression(or).getHavingExpression().toString(), - "((playerStats.id IN [foo] AND playerStats.highScore GT [99]) OR playerStats.id IN [foo])" + assertNull(splitFilterExpressionVisitor.visitOrExpression(or).getWhereExpression()); + assertEquals( + "((playerStats.id IN [foo] AND playerStats.highScore GT [99]) OR playerStats.id IN [foo])", + splitFilterExpressionVisitor.visitOrExpression(or).getHavingExpression().toString() ); } @@ -162,10 +165,10 @@ public void testVisitNotExpression() { NotFilterExpression notExpression = new NotFilterExpression( new AndFilterExpression(WHERE_PREDICATE, HAVING_PREDICATE) ); - Assert.assertNull(splitFilterExpressionVisitor.visitNotExpression(notExpression).getWhereExpression()); - Assert.assertEquals( - splitFilterExpressionVisitor.visitNotExpression(notExpression).getHavingExpression().toString(), - "(playerStats.id NOT [foo] OR playerStats.highScore LE [99])" + assertNull(splitFilterExpressionVisitor.visitNotExpression(notExpression).getWhereExpression()); + assertEquals( + "(playerStats.id NOT [foo] OR playerStats.highScore LE [99])", + splitFilterExpressionVisitor.visitNotExpression(notExpression).getHavingExpression().toString() ); } diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/metric/AggregatedMetricTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/metric/AggregatedMetricTest.java index 24225a3557..51510f1f2a 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/metric/AggregatedMetricTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/metric/AggregatedMetricTest.java @@ -5,11 +5,12 @@ */ package com.yahoo.elide.datastores.aggregation.metric; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.Mockito.mock; import com.yahoo.elide.datastores.aggregation.schema.Schema; -import org.testng.Assert; -import org.testng.annotations.Test; +import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.HashSet; @@ -37,16 +38,16 @@ public class AggregatedMetricTest { @Test public void testMetricAsCollectionElement() { - Assert.assertEquals(SIMPLE_METRIC_1, SIMPLE_METRIC_1); - Assert.assertEquals(SIMPLE_METRIC_2, SIMPLE_METRIC_2); - Assert.assertNotEquals(SIMPLE_METRIC_1, SIMPLE_METRIC_2); - Assert.assertNotEquals(SIMPLE_METRIC_1.hashCode(), SIMPLE_METRIC_2.hashCode()); + assertEquals(SIMPLE_METRIC_1, SIMPLE_METRIC_1); + assertEquals(SIMPLE_METRIC_2, SIMPLE_METRIC_2); + assertNotEquals(SIMPLE_METRIC_1, SIMPLE_METRIC_2); + assertNotEquals(SIMPLE_METRIC_1.hashCode(), SIMPLE_METRIC_2.hashCode()); // different metrics should be separate elements in Set Set set = new HashSet<>(); set.add(SIMPLE_METRIC_1); - Assert.assertEquals(set.size(), 1); + assertEquals(1, set.size()); // a separate same object doesn't increase collection size Metric sameMetric = new AggregatedMetric( @@ -56,29 +57,29 @@ public void testMetricAsCollectionElement() { long.class, Collections.singletonList(Max.class) ); - Assert.assertEquals(sameMetric, SIMPLE_METRIC_1); + assertEquals(SIMPLE_METRIC_1, sameMetric); set.add(sameMetric); - Assert.assertEquals(set.size(), 1); + assertEquals(1, set.size()); set.add(SIMPLE_METRIC_1); - Assert.assertEquals(set.size(), 1); + assertEquals(1, set.size()); set.add(SIMPLE_METRIC_2); - Assert.assertEquals(set.size(), 2); + assertEquals(2, set.size()); } @Test public void testToString() { // simple metric - Assert.assertEquals( - SIMPLE_METRIC_1.toString(), - "AggregatedMetric[name='highScore', longName='highScore', description='highScore', dataType=long, aggregations=Max]" + assertEquals( + "AggregatedMetric[name='highScore', longName='highScore', description='highScore', dataType=long, aggregations=Max]", + SIMPLE_METRIC_1.toString() ); // computed metric - Assert.assertEquals( - SIMPLE_METRIC_2.toString(), - "AggregatedMetric[name='timeSpentPerGame', longName='timeSpentPerGame', description='timeSpentPerGame', dataType=class java.lang.Float, aggregations=Max]" + assertEquals( + "AggregatedMetric[name='timeSpentPerGame', longName='timeSpentPerGame', description='timeSpentPerGame', dataType=class java.lang.Float, aggregations=Max]", + SIMPLE_METRIC_2.toString() ); } } 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 207eb92c5e..e618f4e629 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 -840,USA,United States 344,HKG,Hong Kong +840,USA,United States 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 7a75dd4765..e0b18466d0 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,3 +1,4 @@ id,highScore,lowScore,overallRating,country_id,player_id,recordedDate Jon Doe,1234,72,Good,840,1,2019-07-12 00:00:00 Jane Doe,2412,241,Great,840,2,2019-07-11 00:00:00 +Han,1000,72,Good,344,3,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 d334acb2ee..a9604ecaad 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 @@ -202,8 +202,8 @@ public static void registerJPQLGenerator(Operator op, * @return Returns null if no generator is registered. */ public static JPQLPredicateGenerator lookupJPQLGenerator(Operator op, - Class entityClass, - String fieldName) { + Class entityClass, + String fieldName) { return predicateOverrides.get(Triple.of(op, entityClass, fieldName)); } diff --git a/pom.xml b/pom.xml index 91f19cb60c..9ef26451e9 100644 --- a/pom.xml +++ b/pom.xml @@ -79,7 +79,7 @@ 1.2.3 9.4.19.v20190610 2.9.0 - 2.9.9 + 2.9.10 2.28 3.6.10.Final 8.0.16 @@ -246,7 +246,7 @@ com.fasterxml.jackson.core jackson-databind - 2.9.9.3 + ${version.jackson} com.jayway.restassured @@ -400,11 +400,11 @@ 1.11.2 - + org.apache.maven.scm maven-scm-api 1.11.2 - + @{project.version}