From 8c0cbc1ddabc420b59d133f177f04ef94b7e655d Mon Sep 17 00:00:00 2001 From: Han Chen Date: Mon, 21 Oct 2019 19:50:04 -0500 Subject: [PATCH] ISSUE-1026 Add support for @Subselect (#1038) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception * ISSUE-1026 Add support for @Subselect * Address comments --- .../queryengines/sql/SQLQueryEngine.java | 21 +- .../queryengines/sql/schema/SQLSchema.java | 41 ++- .../AggregationDataStoreTestHarness.java | 2 + .../aggregation/example/Country.java | 2 + .../aggregation/example/Player.java | 3 + .../aggregation/example/PlayerStats.java | 38 +- .../aggregation/example/PlayerStatsView.java | 2 +- .../aggregation/example/SubCountry.java | 61 ++++ .../aggregation/example/VideoGame.java | 4 +- .../SplitFilterExpressionVisitorTest.java | 2 + .../{ => sql}/SQLQueryEngineTest.java | 7 +- .../queryengines/sql/SubselectTest.java | 333 ++++++++++++++++++ .../aggregation/schema/SchemaTest.java | 5 +- .../test/resources/META-INF/persistence.xml | 1 + .../src/test/resources/create_tables.sql | 7 +- .../src/test/resources/player_stats.csv | 8 +- 16 files changed, 498 insertions(+), 39 deletions(-) create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/SubCountry.java rename elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/{ => sql}/SQLQueryEngineTest.java (99%) create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SubselectTest.java 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 7d254a4508..e2d03457a5 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 @@ -30,7 +30,6 @@ import com.yahoo.elide.utils.coerce.CoerceUtil; import com.google.common.base.Preconditions; - import org.hibernate.jpa.QueryHints; import lombok.Getter; @@ -48,7 +47,6 @@ import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; -import javax.persistence.Table; /** * QueryEngine for SQL backed stores. @@ -181,7 +179,7 @@ protected SQLQuery toSQL(Query query, SQLSchema schema) { (predicate) -> { return generateHavingClauseColumnReference(predicate, query); })); } - if (! query.getDimensions().isEmpty()) { + if (!query.getDimensions().isEmpty()) { builder.groupByClause(extractGroupBy(query)); joinPredicates.addAll(extractPathElements(query @@ -298,25 +296,20 @@ private String extractJoin(Path.PathElement pathElement) { //TODO - support joins where either side owns the relationship. //TODO - Support INNER and RIGHT joins. //TODO - Support toMany joins. - String relationshipName = pathElement.getFieldName(); - Class relationshipClass = pathElement.getFieldType(); - String relationshipAlias = FilterPredicate.getTypeAlias(relationshipClass); Class entityClass = pathElement.getType(); String entityAlias = FilterPredicate.getTypeAlias(entityClass); - Table tableAnnotation = dictionary.getAnnotation(relationshipClass, Table.class); - - String relationshipTableName = (tableAnnotation == null) - ? dictionary.getJsonAliasFor(relationshipClass) - : tableAnnotation.name(); - + Class relationshipClass = pathElement.getFieldType(); + String relationshipAlias = FilterPredicate.getTypeAlias(relationshipClass); + String relationshipName = pathElement.getFieldName(); String relationshipIdField = getColumnName(relationshipClass, dictionary.getIdFieldName(relationshipClass)); + String relationshipColumnName = getColumnName(entityClass, relationshipName); return String.format("LEFT JOIN %s AS %s ON %s.%s = %s.%s", - relationshipTableName, + SQLSchema.getTableOrSubselect(dictionary, relationshipClass), relationshipAlias, entityAlias, - getColumnName(entityClass, relationshipName), + relationshipColumnName, relationshipAlias, relationshipIdField); } diff --git a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/schema/SQLSchema.java b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/schema/SQLSchema.java index be3b926674..1d3acbfc05 100644 --- a/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/schema/SQLSchema.java +++ b/elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/schema/SQLSchema.java @@ -19,6 +19,8 @@ import com.yahoo.elide.datastores.aggregation.schema.metric.Aggregation; import com.yahoo.elide.datastores.aggregation.schema.metric.Metric; +import org.hibernate.annotations.Subselect; + import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -26,6 +28,7 @@ import java.util.List; import javax.persistence.Column; import javax.persistence.JoinColumn; +import javax.persistence.Table; /** * A subclass of Schema that supports additional metadata to construct the FROM clause of a SQL query. @@ -116,15 +119,15 @@ protected Metric constructMetric(String metricField, Class cls, EntityDiction /** * Maps a logical entity attribute into a physical SQL column name. * @param entityDictionary The dictionary for this elide instance. - * @param clazz The entity class. + * @param cls The entity class. * @param fieldName The entity attribute. * @return The physical SQL column name. */ - public static String getColumnName(EntityDictionary entityDictionary, Class clazz, String fieldName) { - Column[] column = entityDictionary.getAttributeOrRelationAnnotations(clazz, Column.class, fieldName); + public static String getColumnName(EntityDictionary entityDictionary, Class cls, String fieldName) { + Column[] column = entityDictionary.getAttributeOrRelationAnnotations(cls, Column.class, fieldName); // this would only be valid for dimension columns - JoinColumn[] joinColumn = entityDictionary.getAttributeOrRelationAnnotations(clazz, + JoinColumn[] joinColumn = entityDictionary.getAttributeOrRelationAnnotations(cls, JoinColumn.class, fieldName); if (column == null || column.length == 0) { @@ -138,14 +141,36 @@ public static String getColumnName(EntityDictionary entityDictionary, Class c } } + /** + * Maps an entity class to a physical table of subselect query, if neither {@link Table} nor {@link Subselect} + * annotation is present on this class, use the class alias as default. + * + * @param entityDictionary The dictionary for this elide instance. + * @param cls The entity class. + * @return The physical SQL table or subselect query. + */ + public static String getTableOrSubselect(EntityDictionary entityDictionary, Class cls) { + Subselect subselectAnnotation = entityDictionary.getAnnotation(cls, Subselect.class); + + if (subselectAnnotation == null) { + Table tableAnnotation = entityDictionary.getAnnotation(cls, Table.class); + + return (tableAnnotation == null) + ? entityDictionary.getJsonAliasFor(cls) + : tableAnnotation.name(); + } else { + return "(" + subselectAnnotation.value() + ")"; + } + } + /** * Returns the physical database column name of an entity field. - * @param clazz The entity which owns the field. + * @param cls The entity which owns the field. * @param fieldName The field name to lookup - * @return + * @return physical database column name of an entity field */ - private String getColumnName(Class clazz, String fieldName) { - return getColumnName(entityDictionary, clazz, fieldName); + private String getColumnName(Class cls, String fieldName) { + return getColumnName(entityDictionary, cls, fieldName); } private String getJoinColumn(Path path) { 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/AggregationDataStoreTestHarness.java index 6e53c9f299..ea42b6bd7f 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/AggregationDataStoreTestHarness.java @@ -12,6 +12,7 @@ import com.yahoo.elide.datastores.aggregation.example.Player; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; import com.yahoo.elide.datastores.aggregation.example.PlayerStatsView; +import com.yahoo.elide.datastores.aggregation.example.SubCountry; import com.yahoo.elide.datastores.aggregation.example.VideoGame; public class AggregationDataStoreTestHarness implements DataStoreTestHarness { @@ -28,6 +29,7 @@ public DataStore getDataStore() { public void populateEntityDictionary(EntityDictionary dictionary) { dictionary.bindEntity(PlayerStats.class); dictionary.bindEntity(Country.class); + dictionary.bindEntity(SubCountry.class); dictionary.bindEntity(PlayerStatsView.class); dictionary.bindEntity(Player.class); dictionary.bindEntity(VideoGame.class); 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 6da056c26f..5c4d55bb97 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 @@ -14,6 +14,7 @@ import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Table; /** * A root level entity for testing AggregationDataStore. @@ -21,6 +22,7 @@ @Data @Entity @Include(rootLevel = true) +@Table(name = "countries") @Cardinality(size = CardinalitySize.SMALL) public class Country { diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Player.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Player.java index fcc0b75bdb..c8b0c8c718 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Player.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Player.java @@ -9,16 +9,19 @@ 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.Table; /** * A root level entity for testing AggregationDataStore. */ @Entity @Include(rootLevel = true) +@Table(name = "players") @Cardinality(size = CardinalitySize.MEDIUM) @Data public class Player { 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 faec35f21a..189a0111a5 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 @@ -24,10 +24,11 @@ import lombok.ToString; import java.util.Date; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; -import javax.persistence.OneToOne; +import javax.persistence.ManyToOne; /** * A root level entity for testing AggregationDataStore. @@ -65,11 +66,21 @@ public class PlayerStats { */ private Country country; + /** + * A subselect dimension. + */ + private SubCountry subCountry; + /** * A dimension field joined to this table. */ private String countryIsoCode; + /** + * A dimension field joined to this table. + */ + private String subCountryIsoCode; + /** * A table dimension. */ @@ -115,7 +126,7 @@ public void setOverallRating(final String overallRating) { this.overallRating = overallRating; } - @OneToOne + @ManyToOne @JoinColumn(name = "country_id") public Country getCountry() { return country; @@ -125,7 +136,17 @@ public void setCountry(final Country country) { this.country = country; } - @OneToOne + @ManyToOne + @JoinColumn(name = "sub_country_id") + public SubCountry getSubCountry() { + return subCountry; + } + + public void setSubCountry(final SubCountry subCountry) { + this.subCountry = subCountry; + } + + @ManyToOne @JoinColumn(name = "player_id") public Player getPlayer() { return player; @@ -157,4 +178,15 @@ public String getCountryIsoCode() { public void setCountryIsoCode(String isoCode) { this.countryIsoCode = isoCode; } + + + @JoinTo(path = "subCountry.isoCode") + @Column(updatable = false, insertable = false) // subselect field should be read-only + public String getSubCountryIsoCode() { + return subCountryIsoCode; + } + + public void setSubCountryIsoCode(String isoCode) { + this.subCountryIsoCode = isoCode; + } } 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 6790073d4d..d3d3022fc2 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 @@ -23,7 +23,7 @@ @Entity @Include(rootLevel = true) @Data -@FromSubquery(sql = "SELECT stats.highScore, stats.player_id, c.name as countryName FROM playerStats AS stats LEFT JOIN country AS c ON stats.country_id = c.id WHERE stats.overallRating = 'Great'") +@FromSubquery(sql = "SELECT stats.highScore, stats.player_id, c.name as countryName FROM playerStats AS stats LEFT JOIN countries AS c ON stats.country_id = c.id WHERE stats.overallRating = 'Great'") public class PlayerStatsView { /** diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/SubCountry.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/SubCountry.java new file mode 100644 index 0000000000..a009fd3911 --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/SubCountry.java @@ -0,0 +1,61 @@ +/* + * 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 com.yahoo.elide.datastores.aggregation.annotation.FriendlyName; + +import org.hibernate.annotations.Subselect; + +import lombok.Data; + +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * A root level entity for testing AggregationDataStore with @Subselect annotation + */ +@Data +@Entity +@Include(rootLevel = true) +@Subselect(value = "select * from countries") +@Cardinality(size = CardinalitySize.SMALL) +public class SubCountry { + + private String id; + + private String isoCode; + + private String name; + + @Id + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getIsoCode() { + return isoCode; + } + + public void setIsoCode(final String isoCode) { + this.isoCode = isoCode; + } + + @FriendlyName + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } +} diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java index 61c40d0c3d..1519f09041 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java @@ -14,13 +14,15 @@ import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Table; /** * A root level entity for testing AggregationDataStore. */ @Entity @Include(rootLevel = true) -@FromTable(name = "videoGame") +@Table(name = "videoGames") +@FromTable(name = "videoGames") public class VideoGame { @Id 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 834d1ec16e..4bd07b3d39 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 @@ -21,6 +21,7 @@ import com.yahoo.elide.datastores.aggregation.example.Country; import com.yahoo.elide.datastores.aggregation.example.Player; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; +import com.yahoo.elide.datastores.aggregation.example.SubCountry; import com.yahoo.elide.datastores.aggregation.schema.Schema; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -49,6 +50,7 @@ public static void setupEntityDictionary() { entityDictionary = new EntityDictionary(Collections.emptyMap()); entityDictionary.bindEntity(PlayerStats.class); entityDictionary.bindEntity(Country.class); + entityDictionary.bindEntity(SubCountry.class); entityDictionary.bindEntity(Player.class); schema = new Schema(PlayerStats.class, entityDictionary); splitFilterExpressionVisitor = new SplitFilterExpressionVisitor(schema); diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/SQLQueryEngineTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngineTest.java similarity index 99% rename from elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/SQLQueryEngineTest.java rename to elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngineTest.java index 30c5f1858f..41218825d9 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/SQLQueryEngineTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngineTest.java @@ -4,7 +4,7 @@ * See LICENSE file in project root for terms. */ -package com.yahoo.elide.datastores.aggregation.queryengines; +package com.yahoo.elide.datastores.aggregation.queryengines.sql; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -18,15 +18,15 @@ import com.yahoo.elide.datastores.aggregation.example.Player; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; import com.yahoo.elide.datastores.aggregation.example.PlayerStatsView; +import com.yahoo.elide.datastores.aggregation.example.SubCountry; import com.yahoo.elide.datastores.aggregation.query.Query; 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.schema.SQLSchema; import com.yahoo.elide.datastores.aggregation.schema.Schema; import com.yahoo.elide.datastores.aggregation.schema.dimension.TimeDimensionColumn; import com.yahoo.elide.datastores.aggregation.schema.metric.Sum; - import com.yahoo.elide.datastores.aggregation.time.TimeGrain; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -57,6 +57,7 @@ public static void init() { dictionary.bindEntity(PlayerStats.class); dictionary.bindEntity(PlayerStatsView.class); dictionary.bindEntity(Country.class); + dictionary.bindEntity(SubCountry.class); dictionary.bindEntity(Player.class); filterParser = new RSQLFilterDialect(dictionary); 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 new file mode 100644 index 0000000000..fac18c495e --- /dev/null +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SubselectTest.java @@ -0,0 +1,333 @@ +/* + * 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.queryengines.sql; + +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.sort.Sorting; +import com.yahoo.elide.datastores.aggregation.QueryEngine; +import com.yahoo.elide.datastores.aggregation.example.Country; +import com.yahoo.elide.datastores.aggregation.example.Player; +import com.yahoo.elide.datastores.aggregation.example.PlayerStats; +import com.yahoo.elide.datastores.aggregation.example.PlayerStatsView; +import com.yahoo.elide.datastores.aggregation.example.SubCountry; +import com.yahoo.elide.datastores.aggregation.query.Query; +import com.yahoo.elide.datastores.aggregation.query.TimeDimensionProjection; +import com.yahoo.elide.datastores.aggregation.queryengines.sql.schema.SQLSchema; +import com.yahoo.elide.datastores.aggregation.schema.Schema; +import com.yahoo.elide.datastores.aggregation.schema.dimension.TimeDimensionColumn; +import com.yahoo.elide.datastores.aggregation.schema.metric.Sum; +import com.yahoo.elide.datastores.aggregation.time.TimeGrain; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; + +public class SubselectTest { + private static EntityManagerFactory emf; + private static Schema playerStatsSchema; + private static EntityDictionary dictionary; + private static RSQLFilterDialect filterParser; + + private static final Country HONG_KONG = new Country(); + private static final Country USA = new Country(); + private static final SubCountry SUB_HONG_KONG = new SubCountry(); + private static final SubCountry SUB_USA = new SubCountry(); + + @BeforeAll + public static void init() { + emf = Persistence.createEntityManagerFactory("aggregationStore"); + dictionary = new EntityDictionary(new HashMap<>()); + dictionary.bindEntity(PlayerStats.class); + dictionary.bindEntity(PlayerStatsView.class); + dictionary.bindEntity(Country.class); + dictionary.bindEntity(SubCountry.class); + dictionary.bindEntity(Player.class); + filterParser = new RSQLFilterDialect(dictionary); + + playerStatsSchema = new SQLSchema(PlayerStats.class, dictionary); + + HONG_KONG.setIsoCode("HKG"); + HONG_KONG.setName("Hong Kong"); + HONG_KONG.setId("344"); + + USA.setIsoCode("USA"); + USA.setName("United States"); + USA.setId("840"); + + SUB_HONG_KONG.setIsoCode("HKG"); + SUB_HONG_KONG.setName("Hong Kong"); + SUB_HONG_KONG.setId("344"); + + SUB_USA.setIsoCode("USA"); + SUB_USA.setName("United States"); + SUB_USA.setId("840"); + } + + /** + * Test filtering on a dimension attribute. + * + * @throws Exception exception + */ + @Test + public void testFilterJoin() throws Exception { + QueryEngine engine = new SQLQueryEngine(emf, dictionary); + + 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("subCountry")) + .groupDimension(playerStatsSchema.getDimension("country")) + .timeDimension(toTimeDimension(playerStatsSchema, TimeGrain.DAY, "recordedDate")) + .whereFilter(filterParser.parseFilterExpression("subCountry.name=='United States'", + PlayerStats.class, false)) + .build(); + + List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) + .collect(Collectors.toList()); + + PlayerStats usa0 = new PlayerStats(); + usa0.setId("0"); + usa0.setLowScore(35); + usa0.setHighScore(1234); + usa0.setOverallRating("Good"); + usa0.setCountry(USA); + usa0.setSubCountry(SUB_USA); + usa0.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00")); + + PlayerStats usa1 = new PlayerStats(); + usa1.setId("1"); + usa1.setLowScore(241); + usa1.setHighScore(2412); + usa1.setOverallRating("Great"); + usa1.setCountry(USA); + usa1.setSubCountry(SUB_USA); + usa1.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); + + assertEquals(2, results.size()); + assertEquals(usa0, results.get(0)); + assertEquals(usa1, results.get(1)); + + // test join + PlayerStats actualStats0 = (PlayerStats) results.get(0); + assertNotNull(actualStats0.getSubCountry()); + assertNotNull(actualStats0.getCountry()); + } + + /** + * Test hydrating multiple relationship values. Make sure the objects are constructed correctly. + */ + @Test + public void testRelationshipHydration() { + QueryEngine engine = new SQLQueryEngine(emf, dictionary); + + Map sortMap = new TreeMap<>(); + sortMap.put("subCountry.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")) + .groupDimension(playerStatsSchema.getDimension("subCountry")) + .timeDimension(toTimeDimension(playerStatsSchema, TimeGrain.DAY, "recordedDate")) + .sorting(new Sorting(sortMap)) + .build(); + + List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) + .collect(Collectors.toList()); + + PlayerStats usa0 = new PlayerStats(); + usa0.setId("0"); + usa0.setLowScore(35); + usa0.setHighScore(1234); + usa0.setOverallRating("Good"); + usa0.setCountry(USA); + usa0.setSubCountry(SUB_USA); + usa0.setRecordedDate(Timestamp.valueOf("2019-07-12 00:00:00")); + + PlayerStats usa1 = new PlayerStats(); + usa1.setId("1"); + usa1.setLowScore(241); + usa1.setHighScore(2412); + usa1.setOverallRating("Great"); + usa1.setCountry(USA); + usa1.setSubCountry(SUB_USA); + usa1.setRecordedDate(Timestamp.valueOf("2019-07-11 00:00:00")); + + PlayerStats hk2 = new PlayerStats(); + hk2.setId("2"); + hk2.setLowScore(72); + hk2.setHighScore(1000); + hk2.setOverallRating("Good"); + hk2.setCountry(HONG_KONG); + hk2.setSubCountry(SUB_HONG_KONG); + 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 actualStats0 = (PlayerStats) results.get(0); + assertNotNull(actualStats0.getSubCountry()); + assertNotNull(actualStats0.getCountry()); + } + + /** + * Test grouping by a dimension with a JoinTo annotation. + * + * @throws Exception exception + */ + @Test + public void testJoinToGroupBy() throws Exception { + QueryEngine engine = new SQLQueryEngine(emf, dictionary); + + Query query = Query.builder() + .schema(playerStatsSchema) + .metric(playerStatsSchema.getMetric("highScore"), Sum.class) + .groupDimension(playerStatsSchema.getDimension("subCountryIsoCode")) + .build(); + + List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) + .collect(Collectors.toList()); + + PlayerStats stats1 = new PlayerStats(); + stats1.setId("0"); + stats1.setHighScore(3646); + stats1.setSubCountryIsoCode("USA"); + + PlayerStats stats2 = new PlayerStats(); + stats2.setId("1"); + stats2.setHighScore(1000); + stats2.setSubCountryIsoCode("HKG"); + + assertEquals(2, results.size()); + assertEquals(stats1, results.get(0)); + assertEquals(stats2, results.get(1)); + } + + /** + * Test grouping by a dimension with a JoinTo annotation. + * + * @throws Exception exception + */ + @Test + public void testJoinToFilter() throws Exception { + QueryEngine engine = new SQLQueryEngine(emf, dictionary); + + Query query = Query.builder() + .schema(playerStatsSchema) + .metric(playerStatsSchema.getMetric("highScore"), Sum.class) + .groupDimension(playerStatsSchema.getDimension("overallRating")) + .whereFilter(filterParser.parseFilterExpression("subCountryIsoCode==USA", + PlayerStats.class, false)) + .build(); + + List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) + .collect(Collectors.toList()); + + PlayerStats stats1 = new PlayerStats(); + stats1.setId("0"); + stats1.setOverallRating("Good"); + stats1.setHighScore(1234); + + PlayerStats stats2 = new PlayerStats(); + stats2.setId("1"); + stats2.setOverallRating("Great"); + stats2.setHighScore(2412); + + assertEquals(2, results.size()); + assertEquals(stats1, results.get(0)); + assertEquals(stats2, results.get(1)); + } + + /** + * Test grouping by a dimension with a JoinTo annotation. + * + * @throws Exception exception + */ + @Test + public void testJoinToSort() throws Exception { + QueryEngine engine = new SQLQueryEngine(emf, dictionary); + + Map sortMap = new TreeMap<>(); + sortMap.put("subCountryIsoCode", Sorting.SortOrder.asc); + + Query query = Query.builder() + .schema(playerStatsSchema) + .metric(playerStatsSchema.getMetric("highScore"), Sum.class) + .groupDimension(playerStatsSchema.getDimension("overallRating")) + .groupDimension(playerStatsSchema.getDimension("subCountry")) + .sorting(new Sorting(sortMap)) + .build(); + + List results = StreamSupport.stream(engine.executeQuery(query).spliterator(), false) + .collect(Collectors.toList()); + + PlayerStats stats1 = new PlayerStats(); + stats1.setId("0"); + stats1.setOverallRating("Good"); + stats1.setSubCountry(SUB_HONG_KONG); + stats1.setHighScore(1000); + + PlayerStats stats2 = new PlayerStats(); + stats2.setId("1"); + stats2.setOverallRating("Good"); + stats2.setSubCountry(SUB_USA); + stats2.setHighScore(1234); + + PlayerStats stats3 = new PlayerStats(); + stats3.setId("2"); + stats3.setOverallRating("Great"); + stats3.setSubCountry(SUB_USA); + stats3.setHighScore(2412); + + assertEquals(3, results.size()); + assertEquals(stats1, results.get(0)); + assertEquals(stats2, results.get(1)); + assertEquals(stats3, results.get(2)); + } + + //TODO - Add Invalid Request Tests + + /** + * Searches the schema for a time dimension column that matches the requested column name and time grain. + * @param grain The column time grain requested. + * @param dimensionName The name of the column. + * @return A newly constructed requested time dimension with the matching grain. + */ + private static TimeDimensionProjection toTimeDimension(Schema schema, TimeGrain grain, String dimensionName) { + TimeDimensionColumn column = schema.getTimeDimension(dimensionName); + + if (column == null) { + return null; + } + + return column.getSupportedGrains().stream() + .filter(supportedGrain -> supportedGrain.grain().equals(grain)) + .findFirst() + .map(supportedGrain -> column.toProjectedDimension(supportedGrain)) + .orElse(null); + } +} diff --git a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/schema/SchemaTest.java b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/schema/SchemaTest.java index 05e8b8ea8f..b60346a111 100644 --- a/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/schema/SchemaTest.java +++ b/elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/schema/SchemaTest.java @@ -14,6 +14,7 @@ import com.yahoo.elide.datastores.aggregation.example.Country; import com.yahoo.elide.datastores.aggregation.example.Player; import com.yahoo.elide.datastores.aggregation.example.PlayerStats; +import com.yahoo.elide.datastores.aggregation.example.SubCountry; import com.yahoo.elide.datastores.aggregation.example.VideoGame; import com.yahoo.elide.datastores.aggregation.schema.metric.Max; @@ -24,13 +25,13 @@ public class SchemaTest { - private static EntityDictionary entityDictionary; private static Schema playerStatsSchema; @BeforeAll public static void setupEntityDictionary() { - entityDictionary = new EntityDictionary(Collections.emptyMap()); + EntityDictionary entityDictionary = new EntityDictionary(Collections.emptyMap()); entityDictionary.bindEntity(Country.class); + entityDictionary.bindEntity(SubCountry.class); entityDictionary.bindEntity(VideoGame.class); entityDictionary.bindEntity(Player.class); entityDictionary.bindEntity(PlayerStats.class); 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 e48bd146fc..02d537ae80 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 @@ -11,6 +11,7 @@ version="2.0"> com.yahoo.elide.datastores.aggregation.example.Country + com.yahoo.elide.datastores.aggregation.example.SubCountry com.yahoo.elide.datastores.aggregation.example.PlayerStats com.yahoo.elide.datastores.aggregation.example.Player com.yahoo.elide.datastores.aggregation.example.VideoGame 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 b0369898da..bc8b6e5444 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 @@ -5,24 +5,25 @@ CREATE TABLE IF NOT EXISTS playerStats lowScore BIGINT, overallRating VARCHAR(255), country_id VARCHAR(255), + sub_country_id VARCHAR(255), player_id BIGINT, recordedDate DATETIME ) AS SELECT * FROM CSVREAD('classpath:player_stats.csv'); -CREATE TABLE IF NOT EXISTS country +CREATE TABLE IF NOT EXISTS countries ( id VARCHAR(255), isoCode VARCHAR(255), name VARCHAR(255) ) AS SELECT * FROM CSVREAD('classpath:country.csv'); -CREATE TABLE IF NOT EXISTS player +CREATE TABLE IF NOT EXISTS players ( id BIGINT, name VARCHAR(255) ) AS SELECT * FROM CSVREAD('classpath:player.csv'); -CREATE TABLE IF NOT EXISTS videoGame +CREATE TABLE IF NOT EXISTS videoGames ( id BIGINT, game_rounds BIGINT, 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 1801ebe3d3..7a7c098abe 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 @@ -id,highScore,lowScore,overallRating,country_id,player_id,recordedDate -Jon Doe,1234,35,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 +id,highScore,lowScore,overallRating,country_id,sub_country_id,player_id,recordedDate +Jon Doe,1234,35,Good,840,840,1,2019-07-12 00:00:00 +Jane Doe,2412,241,Great,840,840,2,2019-07-11 00:00:00 +Han,1000,72,Good,344,344,3,2019-07-13 00:00:00