diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java new file mode 100644 index 00000000..fde47bb7 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java @@ -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 { + 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; + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderBy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderBy.java new file mode 100644 index 00000000..e66418ad --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderBy.java @@ -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 { + + OrderBy(OrderByField... fields) { + super(fields); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java index 5d24a616..a98e1595 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/OrderByField.java @@ -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); } /** diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SegmentList.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SegmentList.java new file mode 100644 index 00000000..fb382c5d --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SegmentList.java @@ -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 the type of the elements. + */ +public class SegmentList extends AbstractSegment { + SegmentList(T... segments) { + super(segments); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java index 0126a343..9c24c518 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SimpleFunction.java @@ -29,8 +29,8 @@ import org.springframework.util.StringUtils; */ public class SimpleFunction extends AbstractSegment implements Expression { - private String functionName; - private List expressions; + private final String functionName; + private final List expressions; private SimpleFunction(String functionName, List expressions) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/AnalyticFunctionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/AnalyticFunctionVisitor.java new file mode 100644 index 00000000..33526019 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/AnalyticFunctionVisitor.java @@ -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 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; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index a63ec8fb..097bf7b1 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -82,6 +82,13 @@ class ExpressionVisitor extends TypedSubtreeVisitor 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; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SegmentListVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SegmentListVisitor.java new file mode 100644 index 00000000..8f455e2f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SegmentListVisitor.java @@ -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> 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; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java index bea45006..18351597 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectListVisitor.java @@ -33,7 +33,6 @@ class SelectListVisitor extends TypedSubtreeVisitor 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 implements PartR requiresComma = true; } - if (segment instanceof Aliased && !insideFunction) { + if (segment instanceof Aliased) { builder.append(" AS ").append(NameRenderer.render(context, (Aliased) segment)); } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java index b8f649ab..5904f300 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java +++ b/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; 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"); + } + } }