Adds support for windowing functions.
This allows us to use analytic functions aka windowing functions in generated select statements. Closes #1019
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
requiresComma = true;
|
||||
}
|
||||
|
||||
if (segment instanceof Aliased && !insideFunction) {
|
||||
if (segment instanceof Aliased) {
|
||||
builder.append(" AS ").append(NameRenderer.render(context, (Aliased) segment));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user