This is an automated email from the ASF dual-hosted git repository. ntimofeev pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit 1d142840ca19520d8eeda25d58e9552257aaebb1 Author: Nikita Timofeev <stari...@gmail.com> AuthorDate: Wed Sep 16 12:25:33 2020 +0300 CAY-2674 Support in-memory evaluation of aggregate functions --- .../exp/parser/ASTAggregateFunctionCall.java | 27 ++++ .../java/org/apache/cayenne/exp/parser/ASTAvg.java | 20 +++ .../org/apache/cayenne/exp/parser/ASTCount.java | 7 + .../java/org/apache/cayenne/exp/parser/ASTMax.java | 9 ++ .../java/org/apache/cayenne/exp/parser/ASTMin.java | 9 ++ .../exp/AggregateExpInMemoryEvaluationIT.java | 146 +++++++++++++++++++++ 6 files changed, 218 insertions(+) diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAggregateFunctionCall.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAggregateFunctionCall.java index f74e904..37209bc 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAggregateFunctionCall.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAggregateFunctionCall.java @@ -19,6 +19,9 @@ package org.apache.cayenne.exp.parser; +import java.util.Collection; +import java.util.Map; + /** * Base class for all aggregation functions expressions * It's more like marker interface for now. @@ -40,6 +43,30 @@ public abstract class ASTAggregateFunctionCall extends ASTFunctionCall { } @Override + protected Object evaluateNode(Object o) throws Exception { + int len = jjtGetNumChildren(); + if(len == 0) { + throw new UnsupportedOperationException("Aggregate functions can be calculated only for Collection or Map."); + } + + Object firstChild = evaluateChild(0, o); + Collection<?> values; + if(firstChild instanceof Map) { + values = ((Map<?, ?>) firstChild).values(); + } else if (firstChild instanceof Collection) { + values = (Collection<?>) firstChild; + } else { + throw new UnsupportedOperationException("Aggregate functions can be calculated only for Collection or Map."); + } + + return evaluateCollection(values); + } + + protected Object evaluateCollection(Collection<?> values) { + throw new UnsupportedOperationException("In-memory evaluation of aggregate functions not implemented yet."); + } + + @Override protected Object evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception { throw new UnsupportedOperationException("In-memory evaluation of aggregate functions not implemented yet."); } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAvg.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAvg.java index 9ad3029..b72f703 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAvg.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAvg.java @@ -19,6 +19,8 @@ package org.apache.cayenne.exp.parser; +import java.util.Collection; + import org.apache.cayenne.exp.Expression; /** @@ -38,4 +40,22 @@ public class ASTAvg extends ASTAggregateFunctionCall { public Expression shallowCopy() { return new ASTAvg(id); } + + @Override + protected Object evaluateCollection(Collection<?> values) { + if(values.isEmpty()) { + return 0.0; + } + + double sum = 0; + for(Object value : values) { + if(value instanceof Number) { + sum += ((Number) value).doubleValue(); + } else { + throw new UnsupportedOperationException("Can't calculate average for non-numeric type."); + } + } + + return sum / values.size(); + } } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCount.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCount.java index ebfe691..75fa77e 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCount.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCount.java @@ -19,6 +19,8 @@ package org.apache.cayenne.exp.parser; +import java.util.Collection; + import org.apache.cayenne.exp.Expression; /** @@ -42,4 +44,9 @@ public class ASTCount extends ASTAggregateFunctionCall { public Expression shallowCopy() { return new ASTCount(id); } + + @Override + protected Object evaluateCollection(Collection<?> values) { + return values.size(); + } } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMax.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMax.java index fdfe063..17b27b5 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMax.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMax.java @@ -19,6 +19,9 @@ package org.apache.cayenne.exp.parser; +import java.util.Collection; +import java.util.Collections; + import org.apache.cayenne.exp.Expression; /** @@ -38,4 +41,10 @@ public class ASTMax extends ASTAggregateFunctionCall { public Expression shallowCopy() { return new ASTMax(id); } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + protected Object evaluateCollection(Collection<?> values) { + return Collections.max((Collection)values); + } } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMin.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMin.java index 209dbb2..e11dd6d 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMin.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMin.java @@ -19,6 +19,9 @@ package org.apache.cayenne.exp.parser; +import java.util.Collection; +import java.util.Collections; + import org.apache.cayenne.exp.Expression; /** @@ -38,4 +41,10 @@ public class ASTMin extends ASTAggregateFunctionCall { public Expression shallowCopy() { return new ASTMin(id); } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + protected Object evaluateCollection(Collection<?> values) { + return Collections.min((Collection)values); + } } diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java new file mode 100644 index 0000000..2fe5d0a --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java @@ -0,0 +1,146 @@ +package org.apache.cayenne.exp; + +import java.math.BigDecimal; +import java.sql.Types; +import java.text.DateFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import org.apache.cayenne.access.DataContext; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.query.ObjectSelect; +import org.apache.cayenne.test.jdbc.DBHelper; +import org.apache.cayenne.test.jdbc.TableHelper; +import org.apache.cayenne.testdo.testmap.Artist; +import org.apache.cayenne.testdo.testmap.Painting; +import org.apache.cayenne.unit.di.server.CayenneProjects; +import org.apache.cayenne.unit.di.server.ServerCase; +import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) +public class AggregateExpInMemoryEvaluationIT extends ServerCase { + + // Format: d/m/YY + private static final DateFormat DATE_FORMAT = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US); + + @Inject + private DBHelper dbHelper; + + @Inject + private DataContext context; + + @Before + public void createArtistsDataSet() throws Exception { + TableHelper tArtist = new TableHelper(dbHelper, "ARTIST"); + tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH"); + tArtist.setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.DATE); + + java.sql.Date[] dates = new java.sql.Date[5]; + for(int i=1; i<=5; i++) { + dates[i-1] = new java.sql.Date(DATE_FORMAT.parse("1/" + i + "/17").getTime()); + } + for (int i = 1; i <= 20; i++) { + tArtist.insert(i, "artist" + i, dates[i % 5]); + } + + TableHelper tGallery = new TableHelper(dbHelper, "GALLERY"); + tGallery.setColumns("GALLERY_ID", "GALLERY_NAME"); + tGallery.insert(1, "tate modern"); + + TableHelper tPaintings = new TableHelper(dbHelper, "PAINTING"); + tPaintings.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "GALLERY_ID", "ESTIMATED_PRICE"); + for (int i = 1; i <= 20; i++) { + tPaintings.insert(i, "painting" + i, i % 5 + 1, 1, i * 10); + } + tPaintings.insert(21, "painting21", 2, 1, 1000); + } + + @After + public void clearArtistsDataSet() throws Exception { + for(String table : Arrays.asList("PAINTING", "ARTIST", "GALLERY")) { + TableHelper tHelper = new TableHelper(dbHelper, table); + tHelper.deleteAll(); + } + } + + @Test + public void testCount() { + List<Artist> artists = ObjectSelect.query(Artist.class) + .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc()) + .prefetch(Artist.PAINTING_ARRAY.disjoint()) + .select(context); + + Expression countExp = Artist.PAINTING_ARRAY.count().getExpression(); + + for (Artist artist : artists) { + assertEquals(artist.getPaintingArray().size(), countExp.evaluate(artist)); + } + } + + @Test + public void testMax() { + List<Artist> artists = ObjectSelect.query(Artist.class) + .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc()) + .prefetch(Artist.PAINTING_ARRAY.disjoint()) + .select(context); + + Expression maxExp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).max().getExpression(); + + Object max0 = maxExp.evaluate(artists.get(0)); + BigDecimal expected0 = BigDecimal.valueOf(20000, 2); + assertEquals(expected0, max0); + + Object max1 = maxExp.evaluate(artists.get(1)); + BigDecimal expected1 = BigDecimal.valueOf(100000, 2); + assertEquals(expected1, max1); + + Object max4 = maxExp.evaluate(artists.get(4)); + BigDecimal expected4 = BigDecimal.valueOf(19000, 2); + assertEquals(expected4, max4); + } + + @Test + public void testMin() { + List<Artist> artists = ObjectSelect.query(Artist.class) + .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc()) + .prefetch(Artist.PAINTING_ARRAY.disjoint()) + .select(context); + + Expression minExp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).min().getExpression(); + + Object min0 = minExp.evaluate(artists.get(0)); + BigDecimal expected0 = BigDecimal.valueOf(5000, 2); + assertEquals(expected0, min0); + + Object min3 = minExp.evaluate(artists.get(3)); + BigDecimal expected1 = BigDecimal.valueOf(3000, 2); + assertEquals(expected1, min3); + + Object min4 = minExp.evaluate(artists.get(4)); + BigDecimal expected4 = BigDecimal.valueOf(4000, 2); + assertEquals(expected4, min4); + } + + @Test + public void testAvg() { + List<Artist> artists = ObjectSelect.query(Artist.class) + .prefetch(Artist.PAINTING_ARRAY.disjoint()) + .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc()) + .select(context); + + Expression avgExp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).avg().getExpression(); + + Object avg0 = avgExp.evaluate(artists.get(0)); + assertEquals(125.0, avg0); + + Object avg2 = avgExp.evaluate(artists.get(2)); + assertEquals(95.0, avg2); + } + +}