Browse Source

Adds support for windowing functions.

This allows us to use analytic functions aka windowing functions in generated select statements.

Closes #1019
pull/1128/head
Jens Schauder 4 years ago
parent
commit
0388315ea5
No known key found for this signature in database
GPG Key ID: 45CC872F17423DBF
  1. 94
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java
  2. 29
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderBy.java
  3. 16
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java
  4. 29
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SegmentList.java
  5. 4
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java
  6. 102
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/AnalyticFunctionVisitor.java
  7. 7
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java
  8. 83
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SegmentListVisitor.java
  9. 3
      spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java
  10. 99
      spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java

94
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.sql;
import java.util.Arrays;
/**
* Represents an analytic function, also known as windowing function
*
* @author Jens Schauder
* @since 2.7
*/
public class AnalyticFunction extends AbstractSegment implements Expression {
private final SimpleFunction function;
private final Partition partition;
private final OrderBy orderBy;
public static AnalyticFunction create(String function, Expression... arguments) {
return new AnalyticFunction(SimpleFunction.create(function, Arrays.asList(arguments)), new Partition(),
new OrderBy());
}
private AnalyticFunction(SimpleFunction function, Partition partition, OrderBy orderBy) {
super(function, partition, orderBy);
this.function = function;
this.partition = partition;
this.orderBy = orderBy;
}
public AnalyticFunction partitionBy(Expression... partitionBy) {
return new AnalyticFunction(function, new Partition(partitionBy), orderBy);
}
public AnalyticFunction orderBy(OrderByField... orderBy) {
return new AnalyticFunction(function, partition, new OrderBy(orderBy));
}
public AnalyticFunction orderBy(Expression... orderByExpression) {
final OrderByField[] orderByFields = Arrays.stream(orderByExpression) //
.map(OrderByField::from) //
.toArray(OrderByField[]::new);
return new AnalyticFunction(function, partition, new OrderBy(orderByFields));
}
public AliasedAnalyticFunction as(String alias) {
return new AliasedAnalyticFunction(this, SqlIdentifier.unquoted(alias));
}
public AliasedAnalyticFunction as(SqlIdentifier alias) {
return new AliasedAnalyticFunction(this, alias);
}
public static class Partition extends SegmentList<Expression> {
Partition(Expression... expressions) {
super(expressions);
}
}
private static class AliasedAnalyticFunction extends AnalyticFunction implements Aliased {
private final SqlIdentifier alias;
AliasedAnalyticFunction(AnalyticFunction analyticFunction, SqlIdentifier alias) {
super(analyticFunction.function, analyticFunction.partition, analyticFunction.orderBy);
this.alias = alias;
}
@Override
public SqlIdentifier getAlias() {
return alias;
}
}
}

29
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderBy.java

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.sql;
/**
* Represents an `ORDER BY` clause. Currently, only used in {@link AnalyticFunction}.
*
* @author Jens Schauder
* @since 2.7
*/
public class OrderBy extends SegmentList<OrderByField> {
OrderBy(OrderByField... fields) {
super(fields);
}
}

16
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java

@ -46,24 +46,24 @@ public class OrderByField extends AbstractSegment { @@ -46,24 +46,24 @@ public class OrderByField extends AbstractSegment {
}
/**
* Creates a new {@link OrderByField} from a {@link Column} applying default ordering.
* Creates a new {@link OrderByField} from an {@link Expression} applying default ordering.
*
* @param column must not be {@literal null}.
* @param expression must not be {@literal null}.
* @return the {@link OrderByField}.
*/
public static OrderByField from(Column column) {
return new OrderByField(column, null, NullHandling.NATIVE);
public static OrderByField from(Expression expression) {
return new OrderByField(expression, null, NullHandling.NATIVE);
}
/**
* Creates a new {@link OrderByField} from a {@link Column} applying a given ordering.
* Creates a new {@link OrderByField} from an {@link Expression} applying a given ordering.
*
* @param column must not be {@literal null}.
* @param expression must not be {@literal null}.
* @param direction order direction
* @return the {@link OrderByField}.
*/
public static OrderByField from(Column column, Direction direction) {
return new OrderByField(column, direction, NullHandling.NATIVE);
public static OrderByField from(Expression expression, Direction direction) {
return new OrderByField(expression, direction, NullHandling.NATIVE);
}
/**

29
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SegmentList.java

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.sql;
/**
* A list of {@link Segment} instances. Normally used by inheritance to derive a more specific list.
*
* @see org.springframework.data.relational.core.sql.AnalyticFunction.Partition
* @see OrderBy
* @param <T> the type of the elements.
*/
public class SegmentList<T extends Segment> extends AbstractSegment {
SegmentList(T... segments) {
super(segments);
}
}

4
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java

@ -29,8 +29,8 @@ import org.springframework.util.StringUtils; @@ -29,8 +29,8 @@ import org.springframework.util.StringUtils;
*/
public class SimpleFunction extends AbstractSegment implements Expression {
private String functionName;
private List<Expression> expressions;
private final String functionName;
private final List<Expression> expressions;
private SimpleFunction(String functionName, List<Expression> expressions) {

102
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/AnalyticFunctionVisitor.java

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.AnalyticFunction;
import org.springframework.data.relational.core.sql.OrderBy;
import org.springframework.data.relational.core.sql.SimpleFunction;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.lang.Nullable;
/**
* Renderer for {@link AnalyticFunction}. Uses a {@link RenderTarget} to call back for render results.
*
* @author Jens Schauder
* @since 2.7
*/
class AnalyticFunctionVisitor extends TypedSingleConditionRenderSupport<AnalyticFunction> implements PartRenderer {
private final StringBuilder part = new StringBuilder();
private final RenderContext context;
@Nullable private PartRenderer delegate;
private boolean addSpace = false;
AnalyticFunctionVisitor(RenderContext context) {
super(context);
this.context = context;
}
@Override
Delegation enterNested(Visitable segment) {
if (segment instanceof SimpleFunction) {
delegate = new SimpleFunctionVisitor(context);
return Delegation.delegateTo((DelegatingVisitor) delegate);
}
if (segment instanceof AnalyticFunction.Partition) {
delegate = new SegmentListVisitor("PARTITION BY ", ", ", new ExpressionVisitor(context));
return Delegation.delegateTo((DelegatingVisitor) delegate);
}
if (segment instanceof OrderBy) {
delegate = new SegmentListVisitor("ORDER BY ", ", ", new OrderByClauseVisitor(context));
return Delegation.delegateTo((DelegatingVisitor) delegate);
}
return super.enterNested(segment);
}
@Override
Delegation leaveNested(Visitable segment) {
if (delegate instanceof SimpleFunctionVisitor) {
part.append(delegate.getRenderedPart());
part.append(" OVER(");
}
if (delegate instanceof SegmentListVisitor) {
final CharSequence renderedPart = delegate.getRenderedPart();
if (renderedPart.length() != 0) {
if (addSpace) {
part.append(' ');
}
part.append(renderedPart);
addSpace = true;
}
}
return super.leaveNested(segment);
}
@Override
Delegation leaveMatched(AnalyticFunction segment) {
part.append(")");
return super.leaveMatched(segment);
}
@Override
public CharSequence getRenderedPart() {
return part;
}
}

7
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java

@ -82,6 +82,13 @@ class ExpressionVisitor extends TypedSubtreeVisitor<Expression> implements PartR @@ -82,6 +82,13 @@ class ExpressionVisitor extends TypedSubtreeVisitor<Expression> implements PartR
return Delegation.delegateTo(visitor);
}
if (segment instanceof AnalyticFunction) {
AnalyticFunctionVisitor visitor = new AnalyticFunctionVisitor(context);
partRenderer = visitor;
return Delegation.delegateTo(visitor);
}
if (segment instanceof Column) {
Column column = (Column) segment;

83
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SegmentListVisitor.java

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.sql.render;
import org.springframework.data.relational.core.sql.SegmentList;
import org.springframework.data.relational.core.sql.Visitable;
import org.springframework.util.Assert;
/**
* A part rendering visitor for lists of segments. It can be set up depending on the elements in the list it should
* handle and the way elemnts should get separated when rendered.
*
* @author Jens Schauder
* @since 2.7
*/
class SegmentListVisitor extends TypedSubtreeVisitor<SegmentList<?>> implements PartRenderer {
private final StringBuilder part = new StringBuilder();
private final String start;
private final String separator;
private final DelegatingVisitor nestedVisitor;
private boolean first = true;
/**
* @param start a {@literal String} to be rendered before the first element if there is at least one element. Must not
* be {@literal null}.
* @param separator a {@literal String} to be rendered between elements. Must not be {@literal null}.
* @param nestedVisitor the {@link org.springframework.data.relational.core.sql.Visitor} responsible for rendering the
* elements of the list. Must not be {@literal null}.
*/
SegmentListVisitor(String start, String separator, DelegatingVisitor nestedVisitor) {
Assert.notNull(start, "Start must not be null.");
Assert.notNull(separator, "Separator must not be null.");
Assert.notNull(nestedVisitor, "Nested Visitor must not be null.");
Assert.isInstanceOf(PartRenderer.class, nestedVisitor, "Nested visitor must implement PartRenderer");
this.start = start;
this.separator = separator;
this.nestedVisitor = nestedVisitor;
}
@Override
Delegation enterNested(Visitable segment) {
if (first) {
part.append(start);
first = false;
} else {
part.append(separator);
}
return Delegation.delegateTo(nestedVisitor);
}
@Override
Delegation leaveNested(Visitable segment) {
part.append(((PartRenderer) nestedVisitor).getRenderedPart());
return super.leaveNested(segment);
}
@Override
public CharSequence getRenderedPart() {
return part;
}
}

3
spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java

@ -33,7 +33,6 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> implements PartR @@ -33,7 +33,6 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> implements PartR
private final StringBuilder builder = new StringBuilder();
private final RenderTarget target;
private boolean requiresComma = false;
private boolean insideFunction = false; // this is hackery and should be fix with a proper visitor for
private ExpressionVisitor expressionVisitor;
// subelements.
@ -86,7 +85,7 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> implements PartR @@ -86,7 +85,7 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> implements PartR
requiresComma = true;
}
if (segment instanceof Aliased && !insideFunction) {
if (segment instanceof Aliased) {
builder.append(" AS ").append(NameRenderer.render(context, (Aliased) segment));
}

99
spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java

@ -17,6 +17,7 @@ package org.springframework.data.relational.core.sql.render; @@ -17,6 +17,7 @@ package org.springframework.data.relational.core.sql.render;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.data.relational.core.dialect.PostgresDialect;
import org.springframework.data.relational.core.dialect.RenderContextFactory;
@ -265,8 +266,7 @@ class SelectRendererUnitTests { @@ -265,8 +266,7 @@ class SelectRendererUnitTests {
Select select = Select.builder().select(column).from(employee).orderBy(OrderByField.from(column).asc()).build();
assertThat(SqlRenderer.toString(select))
.isEqualTo("SELECT emp.name FROM employee emp ORDER BY emp.name ASC");
assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT emp.name FROM employee emp ORDER BY emp.name ASC");
}
@Test // GH-968
@ -544,9 +544,7 @@ class SelectRendererUnitTests { @@ -544,9 +544,7 @@ class SelectRendererUnitTests {
Table table = SQL.table("User");
Select select = StatementBuilder.select( //
Conditions.isGreater(table.column("age"), SQL.literalOf(
18))
) //
Conditions.isGreater(table.column("age"), SQL.literalOf(18))) //
.from(table) //
.build();
@ -575,4 +573,95 @@ class SelectRendererUnitTests { @@ -575,4 +573,95 @@ class SelectRendererUnitTests {
assertThat(rendered)
.isEqualTo("SELECT * FROM tableA JOIN tableB ON tableA.id = tableB.id ORDER BY tableA.name, tableB.name");
}
/**
* Tests the rendering of analytic functions.
*/
@Nested
class AnalyticFunctionsTests {
Table employee = SQL.table("employee");
Column department = employee.column("department");
Column age = employee.column("age");
Column salary = employee.column("salary");
@Test // GH-1019
void renderEmptyOver() {
Select select = StatementBuilder.select( //
AnalyticFunction.create("MAX", salary) //
) //
.from(employee) //
.build();
String rendered = SqlRenderer.toString(select);
assertThat(rendered).isEqualTo("SELECT MAX(employee.salary) OVER() FROM employee");
}
@Test // GH-1019
void renderPartition() {
Select select = StatementBuilder.select( //
AnalyticFunction.create("MAX", salary) //
.partitionBy(department) //
) //
.from(employee) //
.build();
String rendered = SqlRenderer.toString(select);
assertThat(rendered)
.isEqualTo("SELECT MAX(employee.salary) OVER(PARTITION BY employee.department) FROM employee");
}
@Test // GH-1019
void renderOrderBy() {
Select select = StatementBuilder.select( //
AnalyticFunction.create("MAX", salary) //
.orderBy(age) //
) //
.from(employee) //
.build();
String rendered = SqlRenderer.toString(select);
assertThat(rendered).isEqualTo("SELECT MAX(employee.salary) OVER(ORDER BY employee.age) FROM employee");
}
@Test // GH-1019
void renderFullAnalyticFunction() {
final Select select = StatementBuilder.select( //
AnalyticFunction.create("MAX", salary) //
.partitionBy(department) //
.orderBy(age) //
) //
.from(employee) //
.build();
String rendered = SqlRenderer.toString(select);
assertThat(rendered).isEqualTo(
"SELECT MAX(employee.salary) OVER(PARTITION BY employee.department ORDER BY employee.age) FROM employee");
}
@Test // GH-1019
void renderAnalyticFunctionWithAlias() {
final Select select = StatementBuilder.select( //
AnalyticFunction.create("MAX", salary) //
.partitionBy(department) //
.orderBy(age) //
.as("MAX_SELECT")) //
.from(employee) //
.build();
String rendered = SqlRenderer.toString(select);
assertThat(rendered).isEqualTo(
"SELECT MAX(employee.salary) OVER(PARTITION BY employee.department ORDER BY employee.age) AS MAX_SELECT FROM employee");
}
}
}

Loading…
Cancel
Save