Skip to content

Commit 51c3065

Browse files
authored
3683 - Add AggregateFormulaContext to support group_concat etc (#3691)
- Add AggregateFormulaContext with ability to override the default via DatabaseBuilder - Add group_concat, string_agg, listagg to set of known aggregation functions
1 parent e46dd66 commit 51c3065

File tree

15 files changed

+309
-18
lines changed

15 files changed

+309
-18
lines changed

ebean-api/src/main/java/io/ebean/DatabaseBuilder.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,14 @@ default DatabaseBuilder namingConvention(NamingConvention namingConvention) {
989989
@Deprecated
990990
DatabaseBuilder setNamingConvention(NamingConvention namingConvention);
991991

992+
/**
993+
* Set the AggregateFormulaContext which is used to determine if a database function
994+
* is an aggregate function (like sum, min, max, avg etc).
995+
* <p>
996+
* Use this to override the default known aggregation functions.
997+
*/
998+
DatabaseConfig aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
999+
9921000
/**
9931001
* Set to true if all DB column and table names should use quoted identifiers.
9941002
* <p>
@@ -2610,6 +2618,11 @@ interface Settings extends DatabaseBuilder {
26102618
*/
26112619
NamingConvention getNamingConvention();
26122620

2621+
/**
2622+
* Return the AggregateFormulaContext.
2623+
*/
2624+
AggregateFormulaContext aggregateFormulaContext();
2625+
26132626
/**
26142627
* Return true if all DB column and table names should use quoted identifiers.
26152628
*/
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.ebean.config;
2+
3+
import java.util.Set;
4+
5+
/**
6+
* Used when parsing formulas to determine if they are aggregation formulas like
7+
* sum, min, max, avg, count etc.
8+
* <p>
9+
* Ebean needs to determine if they are aggregation formulas to determine which
10+
* properties should be included in a GROUP BY clause etc.
11+
*/
12+
public interface AggregateFormulaContext {
13+
14+
/**
15+
* Return true if the outer function is an aggregate function (like sum, count, min, max, avg etc).
16+
*/
17+
boolean isAggregate(String outerFunction);
18+
19+
/**
20+
* Return true if the aggregate function returns a BIGINT type.
21+
* This is true for functions like count that return a numeric value regardless of the
22+
* type of the property or expression inside the outer function.
23+
*/
24+
boolean isCount(String outerFunction);
25+
26+
/**
27+
* Return true if the aggregate function returns a VARCHAR type.
28+
* This is true for functions that return a string concatenation like group_concat etc
29+
* regardless of the type of the property used inside the outer function.
30+
*/
31+
boolean isConcat(String outerFunction);
32+
33+
/**
34+
* Return a builder for the AggregateFormulaContext.
35+
*/
36+
static Builder builder() {
37+
return new AggregateFormulaContextBuilder();
38+
}
39+
40+
/**
41+
* A builder for the AggregateFormulaContext.
42+
*/
43+
interface Builder {
44+
45+
/**
46+
* Override the default set of aggregation functions.
47+
*/
48+
Builder aggregateFunctions(Set<String> count);
49+
50+
/**
51+
* Override the default set of concat functions.
52+
*/
53+
Builder concatFunctions(Set<String> concat);
54+
55+
/**
56+
* Override the default set of count functions.
57+
*/
58+
Builder countFunctions(Set<String> count);
59+
60+
/**
61+
* Build the AggregateFormulaContext.
62+
*/
63+
AggregateFormulaContext build();
64+
}
65+
66+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.ebean.config;
2+
3+
import java.util.Set;
4+
5+
final class AggregateFormulaContextBuilder implements AggregateFormulaContext.Builder {
6+
7+
private Set<String> aggFunctions = Set.of("count", "max", "min", "avg", "sum", "group_concat", "string_agg", "listagg");
8+
private Set<String> concat = Set.of("concat", "group_concat", "string_agg", "listagg");
9+
private Set<String> count = Set.of("count");
10+
11+
@Override
12+
public AggregateFormulaContext.Builder aggregateFunctions(Set<String> agg) {
13+
this.aggFunctions = agg;
14+
return this;
15+
}
16+
17+
@Override
18+
public AggregateFormulaContext.Builder concatFunctions(Set<String> concat) {
19+
this.concat = concat;
20+
return this;
21+
}
22+
23+
@Override
24+
public AggregateFormulaContext.Builder countFunctions(Set<String> count) {
25+
this.count = count;
26+
return this;
27+
}
28+
29+
@Override
30+
public AggregateFormulaContext build() {
31+
return new FormulaContext(aggFunctions, concat, count);
32+
}
33+
34+
private static final class FormulaContext implements AggregateFormulaContext {
35+
36+
private final Set<String> aggFunctions;
37+
private final Set<String> concat;
38+
private final Set<String> count;
39+
40+
private FormulaContext(Set<String> aggFunctions, Set<String> concat, Set<String> count) {
41+
this.aggFunctions = aggFunctions;
42+
this.concat = concat;
43+
this.count = count;
44+
}
45+
46+
@Override
47+
public boolean isAggregate(String outerFunction) {
48+
return aggFunctions.contains(outerFunction);
49+
}
50+
51+
@Override
52+
public boolean isCount(String outerFunction) {
53+
return count.contains(outerFunction);
54+
}
55+
56+
@Override
57+
public boolean isConcat(String outerFunction) {
58+
return concat.contains(outerFunction);
59+
}
60+
}
61+
}

ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
356356
*/
357357
private NamingConvention namingConvention = new UnderscoreNamingConvention();
358358

359+
private AggregateFormulaContext aggregateFormulaContext = AggregateFormulaContext.builder().build();
360+
359361
/**
360362
* Behaviour of updates in JDBC batch to by default include all properties.
361363
*/
@@ -1279,6 +1281,17 @@ public DatabaseConfig setNamingConvention(NamingConvention namingConvention) {
12791281
return this;
12801282
}
12811283

1284+
@Override
1285+
public AggregateFormulaContext aggregateFormulaContext() {
1286+
return aggregateFormulaContext;
1287+
}
1288+
1289+
@Override
1290+
public DatabaseConfig aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext) {
1291+
this.aggregateFormulaContext = aggregateFormulaContext;
1292+
return this;
1293+
}
1294+
12821295
@Override
12831296
public boolean isAllQuotedIdentifiers() {
12841297
return platformConfig.isAllQuotedIdentifiers();
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.ebean.config;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.List;
6+
import java.util.Set;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class AggregateFormulaContextTest {
11+
12+
@Test
13+
void defaultContext() {
14+
var defaultContext = AggregateFormulaContext.builder().build();
15+
for (String aggFunction : List.of("count", "max", "min", "avg", "sum", "group_concat", "string_agg", "listagg")) {
16+
assertThat(defaultContext.isAggregate(aggFunction)).isTrue();
17+
}
18+
for (String c : List.of("concat", "group_concat", "string_agg", "listagg")) {
19+
assertThat(defaultContext.isConcat(c)).isTrue();
20+
}
21+
for (String c : List.of("count")) {
22+
assertThat(defaultContext.isCount(c)).isTrue();
23+
}
24+
25+
assertThat(defaultContext.isConcat("junk")).isFalse();
26+
assertThat(defaultContext.isCount("junk")).isFalse();
27+
assertThat(defaultContext.isAggregate("junk")).isFalse();
28+
}
29+
30+
@Test
31+
void overrideAggregateFunctions() {
32+
AggregateFormulaContext mySum = AggregateFormulaContext.builder()
33+
.aggregateFunctions(Set.of("my_sum"))
34+
.build();
35+
36+
assertThat(mySum.isAggregate("my_sum")).isTrue();
37+
assertThat(mySum.isAggregate("avg")).isFalse();
38+
assertThat(mySum.isCount("count")).isTrue();
39+
assertThat(mySum.isConcat("group_concat")).isTrue();
40+
}
41+
42+
@Test
43+
void overrideConcatFunctions() {
44+
AggregateFormulaContext myConcat = AggregateFormulaContext.builder()
45+
.concatFunctions(Set.of("my_concat"))
46+
.build();
47+
48+
assertThat(myConcat.isAggregate("avg")).isTrue();
49+
assertThat(myConcat.isCount("count")).isTrue();
50+
assertThat(myConcat.isConcat("group_concat")).isFalse();
51+
assertThat(myConcat.isConcat("my_concat")).isTrue();
52+
}
53+
54+
@Test
55+
void overrideCountFunctions() {
56+
AggregateFormulaContext myCount = AggregateFormulaContext.builder()
57+
.countFunctions(Set.of("my_count"))
58+
.build();
59+
60+
assertThat(myCount.isAggregate("avg")).isTrue();
61+
assertThat(myCount.isCount("count")).isFalse();
62+
assertThat(myCount.isCount("my_count")).isTrue();
63+
assertThat(myCount.isConcat("group_concat")).isTrue();
64+
}
65+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.ebeaninternal.api;
2+
3+
import io.ebean.config.AggregateFormulaContext;
4+
import io.ebeaninternal.server.query.STreeProperty;
5+
6+
public interface FormulaBuilder {
7+
8+
STreeProperty create(AggregateFormulaContext context, String formula, String path);
9+
10+
}

ebean-core/src/main/java/io/ebeaninternal/api/SpiBeanType.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ public interface SpiBeanType {
1414
* or removals from the collection.
1515
*/
1616
boolean isToManyDirty(EntityBean bean);
17+
18+
/**
19+
* Return the FormulaBuilder for this type.
20+
*/
21+
FormulaBuilder formulaBuilder();
1722
}

ebean-core/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.ebeaninternal.api;
22

3+
import io.ebeaninternal.server.query.STreeProperty;
34
import org.jspecify.annotations.Nullable;
45
import io.ebean.*;
56
import io.ebean.bean.BeanCollectionLoader;
@@ -377,4 +378,6 @@ public interface SpiEbeanServer extends SpiServer, BeanCollectionLoader {
377378

378379
@Nullable
379380
SqlRow findOne(SpiSqlQuery query);
381+
382+
<T> STreeProperty createFormulaProperty(SpiBeanType desc, String formula, String path);
380383
}

ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
113113
private final long slowQueryMicros;
114114
private final SlowQueryListener slowQueryListener;
115115
private final boolean disableL2Cache;
116+
private final AggregateFormulaContext formulaContext;
116117
private boolean shutdown;
117118

118119
/**
@@ -128,6 +129,7 @@ public DefaultServer(InternalConfiguration config, ServerCacheManager cache) {
128129
this.backgroundExecutor = config.getBackgroundExecutor();
129130
this.extraMetrics = config.getExtraMetrics();
130131
this.serverName = this.config.getName();
132+
this.formulaContext = config.getConfig().aggregateFormulaContext();
131133
this.lazyLoadBatchSize = this.config.getLazyLoadBatchSize();
132134
this.cqueryEngine = config.getCQueryEngine();
133135
this.expressionFactory = config.getExpressionFactory();
@@ -928,6 +930,11 @@ public <T> T find(Class<T> beanType, Object id, @Nullable Transaction transactio
928930
return findId(query);
929931
}
930932

933+
@Override
934+
public <T> STreeProperty createFormulaProperty(SpiBeanType desc, String formula, String path) {
935+
return desc.formulaBuilder().create(formulaContext, formula, path);
936+
}
937+
931938
<T> SpiOrmQueryRequest<T> createQueryRequest(Type type, SpiQuery<T> query) {
932939
SpiOrmQueryRequest<T> request = buildQueryRequest(type, query);
933940
request.prepareQuery();

ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2410,7 +2410,12 @@ boolean matchBaseTable(String tableName) {
24102410
*/
24112411
private STreeProperty findSqlTreeFormula(String formula, String path) {
24122412
String key = formula + "-" + path;
2413-
return dynamicProperty.computeIfAbsent(key, (fullKey) -> FormulaPropertyPath.create(this, formula, path));
2413+
return dynamicProperty.computeIfAbsent(key, (fullKey) -> ebeanServer.createFormulaProperty(this, formula, path));
2414+
}
2415+
2416+
@Override
2417+
public FormulaBuilder formulaBuilder() {
2418+
return new DFormulaBuilder(this);
24142419
}
24152420

24162421
/**

0 commit comments

Comments
 (0)