Skip to content

Commit

Permalink
Merge pull request #3397 from ebean-orm/feature/distinctOn
Browse files Browse the repository at this point in the history
Postgres: Add support for DISTINCT ON query clause
  • Loading branch information
rbygrave authored May 1, 2024
2 parents b99d4f5 + 2faf700 commit 753cf66
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 10 deletions.
7 changes: 7 additions & 0 deletions ebean-api/src/main/java/io/ebean/QueryBuilderProjection.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ public interface QueryBuilderProjection<SELF, T> {
*/
SELF select(String fetchProperties);

/**
* Set DISTINCT ON clause. This is a Postgres only SQL feature.
*
* @param distinctOn The properties to include in the DISTINCT ON clause.
*/
SELF distinctOn(String distinctOn);

/**
* Apply the fetchGroup which defines what part of the object graph to load.
*/
Expand Down
6 changes: 5 additions & 1 deletion ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,11 @@ public static TemporalMode of(SpiQuery<?> query) {
@Override
SpiQuery<T> copy();

/**
* Return the distinct on clause.
*/
String distinctOn();

/**
* Return a copy of the query attaching to a different EbeanServer.
*/
Expand Down Expand Up @@ -549,7 +554,6 @@ public static TemporalMode of(SpiQuery<?> query) {
* Return true if the query should include the Id property.
* <p>
* distinct and single attribute queries exclude the Id property.
* </p>
*/
boolean isWithId();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public void queryBindKey(BindValuesKey key) {

@Override
public void addSql(SpiExpressionRequest request) {
request.property(propName).append(op.expression).append('(').parse(sql).append(')');
request.property(propName).append(op.expression).append('(').append(sql).append(')');
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import io.ebean.*;
import io.ebean.annotation.Platform;
import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.config.dbplatform.SqlLimitRequest;
import io.ebean.config.dbplatform.SqlLimitResponse;
import io.ebean.config.dbplatform.SqlLimiter;
import io.ebean.event.readaudit.ReadAuditQueryPlan;
Expand Down Expand Up @@ -552,7 +551,7 @@ private BuildReq(String selectClause, OrmQueryRequest<?> request, CQueryPredicat
this.countSingleAttribute = query.isCountDistinct() && query.isSingleAttribute();
this.useSqlLimiter = selectClause == null
&& query.hasMaxRowsOrFirstRow()
&& (select.manyProperty() == null || query.isSingleAttribute());
&& (select.distinctOn() != null || select.manyProperty() == null || query.isSingleAttribute());
}

private void appendSelect() {
Expand Down Expand Up @@ -583,7 +582,7 @@ private void appendSelect() {
if (request.isInlineCountDistinct()) {
sb.append(')');
}
if (distinct && dbOrderBy != null) {
if (distinct && dbOrderBy != null && query.distinctOn() == null) {
// add the orderBy columns to the select clause (due to distinct)
String[] tokens = DbOrderByTrim.trim(dbOrderBy).split(",");
for (String token : tokens) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public final class CQueryPredicates {
*/
private String dbFilterMany;
private String dbOrderBy;
private String dbDistinctOn;
private String dbUpdateClause;
/**
* Includes from where and order by clauses.
Expand Down Expand Up @@ -209,6 +210,10 @@ public void prepare(boolean buildSql) {
}
}
if (buildSql) {
final String distinctOn = query.distinctOn();
if (distinctOn != null) {
dbDistinctOn = deployParser.parse(distinctOn);
}
predicateIncludes = deployParser.includes();
}
}
Expand All @@ -231,6 +236,9 @@ void parseTableAlias(SqlTreeAlias alias) {
if (dbOrderBy != null) {
dbOrderBy = alias.parse(dbOrderBy);
}
if (dbDistinctOn != null) {
dbDistinctOn = alias.parse(dbDistinctOn);
}
}

private String parseOrderBy() {
Expand Down Expand Up @@ -351,6 +359,13 @@ String dbOrderBy() {
return dbOrderBy;
}

/**
* Return the db distinct on clause.
*/
String dbDistinctOn() {
return dbDistinctOn;
}

/**
* Return the includes required for the where and order by clause.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public FetchGroup<T> buildFetchGroup() {
return new DFetchGroup<>(detail);
}

@Override
public Query<T> distinctOn(String distinctOn) {
throw new UnsupportedOperationException();
}

@Override
public Query<T> select(String columns) {
detail.select(columns);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ private String buildGroupByClause() {
}

private String buildDistinctOn() {
String distinctOn = predicates.dbDistinctOn();
if (distinctOn != null) {
return distinctOn;
}
if (rawSql || !distinctOnPlatform || !sqlDistinct || Type.COUNT == query.type()) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
* Lazy loading batch size (can override server wide default).
*/
private int lazyLoadBatchSize;
private String distinctOn;
private OrderBy<T> orderBy;
private String loadMode;
private String loadDescription;
Expand Down Expand Up @@ -515,7 +516,10 @@ public final SpiQuerySecondary convertJoins() {
* Limit the number of fetch joins to Many properties, mark as query joins as needed.
*/
private void markQueryJoins() {
detail.markQueryJoins(beanDescriptor, lazyLoadManyPath, isAllowOneManyFetch(), type.defaultSelect());
if (distinctOn == null) {
// no automatic join to query join conversion when distinctOn is used
detail.markQueryJoins(beanDescriptor, lazyLoadManyPath, isAllowOneManyFetch(), type.defaultSelect());
}
}

private boolean isAllowOneManyFetch() {
Expand Down Expand Up @@ -639,12 +643,11 @@ public final CountDistinctOrder countDistinctOrder() {
return countDistinctOrder;
}

/**
* Return true if the Id should be included in the query.
*/
@Override
public final boolean isWithId() {
return !manualId && !distinct && !singleAttribute;
// distinctOn orm query will auto include the id property
// distinctOn dto query does NOT (via setting manualId to true)
return !manualId && !singleAttribute && (!distinct || distinctOn != null);
}

@Override
Expand Down Expand Up @@ -740,6 +743,7 @@ public SpiQuery<T> copy(SpiEbeanServer server) {
copy.baseTable = baseTable;
copy.rootTableAlias = rootTableAlias;
copy.distinct = distinct;
copy.distinctOn = distinctOn;
copy.allowLoadErrors = allowLoadErrors;
copy.timeout = timeout;
copy.mapKey = mapKey;
Expand Down Expand Up @@ -799,6 +803,11 @@ public final void setType(Type type) {
this.type = type;
}

@Override
public String distinctOn() {
return distinctOn;
}

@Override
public final String loadDescription() {
return loadDescription;
Expand Down Expand Up @@ -1087,6 +1096,9 @@ private String planDescription() {
}
if (distinct) {
sb.append("/dt");
if (distinctOn != null) {
sb.append("/o:").append(distinctOn);
}
}
if (allowLoadErrors) {
sb.append("/ae");
Expand Down Expand Up @@ -1340,6 +1352,13 @@ public final void addNested(String name, OrmQueryDetail nestedDetail, FetchConfi
detail.addNested(name, nestedDetail, config);
}

@Override
public final Query<T> distinctOn(String distinctOn) {
this.distinctOn = distinctOn;
this.distinct = true;
return this;
}

@Override
public final Query<T> select(String columns) {
detail.select(columns);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import io.ebean.*;
import io.ebeaninternal.api.BindValuesKey;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.core.OrmQueryRequest;
import io.ebeaninternal.server.core.OrmQueryRequestTestHelper;
import io.ebeaninternal.server.expression.BaseExpressionTest;
Expand Down Expand Up @@ -54,6 +55,32 @@ public void checkForId_when_setId_ok() {
assertThat(q1.getId()).isEqualTo(42);
}

@Test
void when_distinctOn_then_planChanges() {
DefaultOrmQuery<Order> q1 = (DefaultOrmQuery<Order>) DB.find(Order.class).distinctOn("name");
DefaultOrmQuery<Order> q2 = (DefaultOrmQuery<Order>) DB.find(Order.class);

prepare(q1, q2);
assertThat(q1.createQueryPlanKey()).isNotEqualTo(q2.createQueryPlanKey());
}

@Test
void when_distinctOn_match() {
DefaultOrmQuery<Order> q1 = (DefaultOrmQuery<Order>) DB.find(Order.class).distinctOn("name");
DefaultOrmQuery<Order> q2 = (DefaultOrmQuery<Order>) DB.find(Order.class).distinctOn("name");

prepare(q1, q2);
assertThat(q1.createQueryPlanKey()).isEqualTo(q2.createQueryPlanKey());
assertThat(bindKey(q1)).isEqualTo(bindKey(q2));
}

@Test
void when_distinctOn_copy() {
DefaultOrmQuery<Order> q1 = (DefaultOrmQuery<Order>) DB.find(Order.class).distinctOn("name");
SpiQuery<Order> copy = q1.copy();
assertThat(copy.distinctOn()).isEqualTo("name");
}

@Test
public void when_addWhere_then_planChanges() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ public interface IQueryBean<T, R> extends QueryBuilder<R, T> {
*/
FetchGroup<T> buildFetchGroup();

/**
* Set DISTINCT ON properties. This is a Postgres only SQL feature.
*
* @param properties The properties to include in the DISTINCT ON clause.
*/
R distinctOn(TQProperty<R, ?>... properties);

/**
* Specify the properties to be loaded on the 'main' root level entity bean.
* <p>
Expand Down
17 changes: 17 additions & 0 deletions ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ public Query<T> query() {
return query;
}

@Override
public R distinctOn(String distinctOn) {
query.distinctOn(distinctOn);
return root;
}

@Override
public R select(String properties) {
query.select(properties);
Expand All @@ -171,6 +177,17 @@ public R select(FetchGroup<T> fetchGroup) {
return root;
}

@Override
@SafeVarargs
public final R distinctOn(TQProperty<R, ?>... properties) {
final var joiner = new StringJoiner(", ");
for (Query.Property<?> property : properties) {
joiner.add(property.toString());
}
distinctOn(joiner.toString());
return root;
}

@Override
@SafeVarargs
public final R select(TQProperty<R, ?>... properties) {
Expand Down
17 changes: 17 additions & 0 deletions ebean-querybean/src/test/java/org/querytest/QCustomerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.ebean.annotation.Transactional;
import io.ebean.test.LoggedSql;
import io.ebean.types.Inet;
import io.ebeaninternal.api.SpiQuery;
import org.example.domain.Address;
import org.example.domain.Country;
import org.example.domain.Customer;
Expand Down Expand Up @@ -226,6 +227,22 @@ public void isEmpty() {
.findList();
}

@Test
public void distinctOn() {
var c = QContact.alias();
var q = new QContact()
.distinctOn(c.customer)
.select(c.lastName, c.whenCreated)
.orderBy()
.customer.id.asc()
.whenCreated.desc()
.query();

SpiQuery<?> spiQuery = (SpiQuery<?>) q;
assertThat(spiQuery.distinctOn()).isEqualTo("customer");
assertThat(spiQuery.isDistinct()).isTrue();
}

@Transactional
@Test
public void forUpdate() {
Expand Down
Loading

0 comments on commit 753cf66

Please sign in to comment.