diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/AbstractDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/AbstractDialect.java new file mode 100644 index 00000000..6e00a106 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/AbstractDialect.java @@ -0,0 +1,147 @@ +/* + * Copyright 2019 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.dialect; + +import lombok.RequiredArgsConstructor; + +import java.util.OptionalLong; +import java.util.function.Function; + +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.render.SelectRenderContext; + +/** + * Base class for {@link Dialect} implementations. + * + * @author Mark Paluch + * @since 1.1 + */ +public abstract class AbstractDialect implements Dialect { + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#getSelectContext() + */ + @Override + public SelectRenderContext getSelectContext() { + + Function afterOrderBy = getAfterOrderBy(); + + return new DialectSelectRenderContext(afterOrderBy); + } + + /** + * Returns a {@link Function afterOrderBy Function}. Typically used for pagination. + * + * @return the {@link Function} called on {@code afterOrderBy}. + */ + protected Function getAfterOrderBy() { + + Function afterOrderBy; + + LimitClause limit = limit(); + + switch (limit.getClausePosition()) { + + case AFTER_ORDER_BY: + afterOrderBy = new AfterOrderByLimitRenderFunction(limit); + break; + + default: + throw new UnsupportedOperationException(String.format("Clause position %s not supported!", limit)); + } + + return afterOrderBy.andThen(PrependWithLeadingWhitespace.INSTANCE); + } + + /** + * {@link SelectRenderContext} derived from {@link Dialect} specifics. + */ + class DialectSelectRenderContext implements SelectRenderContext { + + private final Function afterOrderBy; + + DialectSelectRenderContext(Function afterOrderBy) { + this.afterOrderBy = afterOrderBy; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.SelectRenderContext#afterOrderBy(boolean) + */ + @Override + public Function afterOrderBy(boolean hasOrderBy) { + return afterOrderBy; + } + } + + /** + * After {@code ORDER BY} function rendering the {@link LimitClause}. + */ + @RequiredArgsConstructor + static class AfterOrderByLimitRenderFunction implements Function { + + private final LimitClause clause; + + /* + * (non-Javadoc) + * @see java.util.function.Function#apply(java.lang.Object) + */ + @Override + public CharSequence apply(Select select) { + + OptionalLong limit = select.getLimit(); + OptionalLong offset = select.getOffset(); + + if (limit.isPresent() && offset.isPresent()) { + return clause.getLimitOffset(limit.getAsLong(), offset.getAsLong()); + } + + if (limit.isPresent()) { + return clause.getLimit(limit.getAsLong()); + } + + if (offset.isPresent()) { + return clause.getOffset(offset.getAsLong()); + } + + return ""; + } + } + + /** + * Prepends a non-empty rendering result with a leading whitespace, + */ + @RequiredArgsConstructor + enum PrependWithLeadingWhitespace implements Function { + + INSTANCE; + + /* + * (non-Javadoc) + * @see java.util.function.Function#apply(java.lang.Object) + */ + @Override + public CharSequence apply(CharSequence charSequence) { + + if (charSequence.length() == 0) { + return charSequence; + } + + return " " + charSequence; + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/ArrayColumns.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/ArrayColumns.java new file mode 100644 index 00000000..734b1079 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/ArrayColumns.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 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.dialect; + +/** + * Interface declaring methods that express how a dialect supports array-typed columns. + * + * @author Mark Paluch + * @since 1.1 + */ +public interface ArrayColumns { + + /** + * Returns {@literal true} if the dialect supports array-typed columns. + * + * @return {@literal true} if the dialect supports array-typed columns. + */ + boolean isSupported(); + + /** + * Translate the {@link Class user type} of an array into the dialect-specific type. This method considers only the + * component type. + * + * @param userType component type of the array. + * @return the dialect-supported array type. + * @throws UnsupportedOperationException if array typed columns are not supported. + * @throws IllegalArgumentException if the {@code userType} is not a supported array type. + */ + Class getArrayType(Class userType); + + /** + * Default {@link ArrayColumns} implementation for dialects that do not support array-typed columns. + */ + enum Unsupported implements ArrayColumns { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.ArrayColumns#isSupported() + */ + @Override + public boolean isSupported() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.ArrayColumns#getArrayType(java.lang.Class) + */ + @Override + public Class getArrayType(Class userType) { + throw new UnsupportedOperationException("Array types not supported"); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java new file mode 100644 index 00000000..6c8008fe --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Dialect.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 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.dialect; + +import org.springframework.data.relational.core.sql.render.SelectRenderContext; + +/** + * Represents a dialect that is implemented by a particular database. Please note that not all features are supported by + * all vendors. Dialects typically express this with feature flags. Methods for unsupported functionality may throw + * {@link UnsupportedOperationException}. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 1.1 + */ +public interface Dialect { + + /** + * Return the {@link LimitClause} used by this dialect. + * + * @return the {@link LimitClause} used by this dialect. + */ + LimitClause limit(); + + /** + * Returns the array support object that describes how array-typed columns are supported by this dialect. + * + * @return the array support object that describes how array-typed columns are supported by this dialect. + */ + default ArrayColumns getArraySupport() { + return ArrayColumns.Unsupported.INSTANCE; + } + + /** + * Obtain the {@link SelectRenderContext}. + * + * @return the {@link SelectRenderContext}. + */ + SelectRenderContext getSelectContext(); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/LimitClause.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/LimitClause.java new file mode 100644 index 00000000..70842d94 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/LimitClause.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 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.dialect; + +/** + * A clause representing Dialect-specific {@code LIMIT}. + * + * @author Mark Paluch + * @since 1.1 + */ +public interface LimitClause { + + /** + * Returns the {@code LIMIT} clause to limit results. + * + * @param limit the actual limit to use. + * @return rendered limit clause. + * @see #getLimitOffset(long, long) + */ + String getLimit(long limit); + + /** + * Returns the {@code OFFSET} clause to consume rows at a given offset. + * + * @param limit the actual limit to use. + * @return rendered limit clause. + * @see #getLimitOffset(long, long) + */ + String getOffset(long limit); + + /** + * Returns a combined {@code LIMIT/OFFSET} clause that limits results and starts consumption at the given + * {@code offset}. + * + * @param limit the actual limit to use. + * @param offset the offset to start from. + * @return rendered limit clause. + */ + String getLimitOffset(long limit, long offset); + + /** + * Returns the {@link Position} where to apply the {@link #getOffset(long) clause}. + */ + Position getClausePosition(); + + /** + * Enumeration of where to render the clause within the SQL statement. + */ + enum Position { + + /** + * Append the clause at the end of the statement. + */ + AFTER_ORDER_BY + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java new file mode 100644 index 00000000..93efa5ee --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 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.dialect; + +/** + * An SQL dialect for MySQL. + * + * @author Mark Paluch + * @since 1.1 + */ +public class MySqlDialect extends AbstractDialect { + + /** + * Singleton instance. + */ + public static final MySqlDialect INSTANCE = new MySqlDialect(); + + private static final LimitClause LIMIT_CLAUSE = new LimitClause() { + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getLimit(long) + */ + @Override + public String getLimit(long limit) { + return "LIMIT " + limit; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getOffset(long) + */ + @Override + public String getOffset(long offset) { + throw new UnsupportedOperationException("MySQL does not support OFFSET without LIMIT"); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getClause(long, long) + */ + @Override + public String getLimitOffset(long limit, long offset) { + + // LIMIT {[offset,] row_count} + return String.format("LIMIT %d, %d", offset, limit); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getClausePosition() + */ + @Override + public Position getClausePosition() { + return Position.AFTER_ORDER_BY; + } + }; + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#limit() + */ + @Override + public LimitClause limit() { + return LIMIT_CLAUSE; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java new file mode 100644 index 00000000..6e47b13d --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java @@ -0,0 +1,119 @@ +/* + * Copyright 2019 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.dialect; + +import lombok.RequiredArgsConstructor; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * An SQL dialect for Postgres. + * + * @author Mark Paluch + * @since 1.1 + */ +public class PostgresDialect extends AbstractDialect { + + /** + * Singleton instance. + */ + public static final PostgresDialect INSTANCE = new PostgresDialect(); + + private static final LimitClause LIMIT_CLAUSE = new LimitClause() { + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getLimit(long) + */ + @Override + public String getLimit(long limit) { + return "LIMIT " + limit; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getOffset(long) + */ + @Override + public String getOffset(long offset) { + return "OFFSET " + offset; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getClause(long, long) + */ + @Override + public String getLimitOffset(long limit, long offset) { + return String.format("LIMIT %d OFFSET %d", limit, offset); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getClausePosition() + */ + @Override + public Position getClausePosition() { + return Position.AFTER_ORDER_BY; + } + }; + + private final PostgresArrayColumns ARRAY_COLUMNS = new PostgresArrayColumns(); + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#limit() + */ + @Override + public LimitClause limit() { + return LIMIT_CLAUSE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#getArraySupport() + */ + @Override + public ArrayColumns getArraySupport() { + return ARRAY_COLUMNS; + } + + @RequiredArgsConstructor + static class PostgresArrayColumns implements ArrayColumns { + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.ArrayColumns#isSupported() + */ + @Override + public boolean isSupported() { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.ArrayColumns#getArrayType(java.lang.Class) + */ + @Override + public Class getArrayType(Class userType) { + + Assert.notNull(userType, "Array component type must not be null"); + + return ClassUtils.resolvePrimitiveIfNecessary(userType); + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/RenderContextFactory.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/RenderContextFactory.java new file mode 100644 index 00000000..eeb249c6 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/RenderContextFactory.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019 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.dialect; + +import lombok.RequiredArgsConstructor; + +import org.springframework.data.relational.core.sql.render.NamingStrategies; +import org.springframework.data.relational.core.sql.render.RenderContext; +import org.springframework.data.relational.core.sql.render.RenderNamingStrategy; +import org.springframework.data.relational.core.sql.render.SelectRenderContext; +import org.springframework.util.Assert; + +/** + * Factory for {@link RenderContext} based on {@link Dialect}. + * + * @author Mark Paluch + * @since 1.1 + */ +public class RenderContextFactory { + + public final Dialect dialect; + + public RenderNamingStrategy namingStrategy = NamingStrategies.asIs(); + + /** + * Creates a new {@link RenderContextFactory} given {@link Dialect}. + * + * @param dialect must not be {@literal null}. + */ + public RenderContextFactory(Dialect dialect) { + + Assert.notNull(dialect, "Dialect must not be null!"); + + this.dialect = dialect; + } + + /** + * Set a {@link RenderNamingStrategy}. + * + * @param namingStrategy must not be {@literal null}. + */ + public void setNamingStrategy(RenderNamingStrategy namingStrategy) { + + Assert.notNull(namingStrategy, "RenderNamingStrategy must not be null"); + + this.namingStrategy = namingStrategy; + } + + /** + * Returns a {@link RenderContext} configured with {@link Dialect} specifics. + * + * @return the {@link RenderContext}. + */ + public RenderContext createRenderContext() { + + SelectRenderContext select = dialect.getSelectContext(); + + return new DialectRenderContext(namingStrategy, select); + } + + /** + * {@link RenderContext} derived from {@link Dialect} specifics. + */ + @RequiredArgsConstructor + static class DialectRenderContext implements RenderContext { + + private final RenderNamingStrategy renderNamingStrategy; + + private final SelectRenderContext selectRenderContext; + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.RenderContext#getNamingStrategy() + */ + @Override + public RenderNamingStrategy getNamingStrategy() { + return renderNamingStrategy; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.RenderContext#getSelect() + */ + @Override + public SelectRenderContext getSelect() { + return selectRenderContext; + } + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java new file mode 100644 index 00000000..73a0c2e0 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerDialect.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019 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.dialect; + +import org.springframework.data.relational.core.sql.render.SelectRenderContext; +import org.springframework.data.util.Lazy; + +/** + * An SQL dialect for Microsoft SQL Server. + * + * @author Mark Paluch + * @since 1.1 + */ +public class SqlServerDialect extends AbstractDialect { + + /** + * Singleton instance. + */ + public static final SqlServerDialect INSTANCE = new SqlServerDialect(); + + private static final LimitClause LIMIT_CLAUSE = new LimitClause() { + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getLimit(long) + */ + @Override + public String getLimit(long limit) { + return "OFFSET 0 ROWS FETCH NEXT " + limit + " ROWS ONLY"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getOffset(long) + */ + @Override + public String getOffset(long offset) { + return "OFFSET " + offset + " ROWS"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getClause(long, long) + */ + @Override + public String getLimitOffset(long limit, long offset) { + return String.format("OFFSET %d ROWS FETCH NEXT %d ROWS ONLY", offset, limit); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.LimitClause#getClausePosition() + */ + @Override + public Position getClausePosition() { + return Position.AFTER_ORDER_BY; + } + }; + + private final Lazy selectRenderContext = Lazy + .of(() -> new SqlServerSelectRenderContext(getAfterOrderBy())); + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#limit() + */ + @Override + public LimitClause limit() { + return LIMIT_CLAUSE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.AbstractDialect#getSelectContext() + */ + @Override + public SelectRenderContext getSelectContext() { + return selectRenderContext.get(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerSelectRenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerSelectRenderContext.java new file mode 100644 index 00000000..6f10669e --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/SqlServerSelectRenderContext.java @@ -0,0 +1,87 @@ +/* + * Copyright 2019 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.dialect; + +import java.util.function.Function; + +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.render.SelectRenderContext; + +/** + * SQL-Server specific {@link SelectRenderContext}. Summary of SQL-specifics: + *
    + *
  • Appends a synthetic ROW_NUMBER when using pagination and the query does not specify ordering
  • + *
  • Append synthetic ordering if query uses pagination and the query does not specify ordering
  • + *
+ * + * @author Mark Paluch + */ +public class SqlServerSelectRenderContext implements SelectRenderContext { + + private static final String SYNTHETIC_ORDER_BY_FIELD = "__relational_row_number__"; + + private static final String SYNTHETIC_SELECT_LIST = ", ROW_NUMBER() over (ORDER BY CURRENT_TIMESTAMP) AS " + + SYNTHETIC_ORDER_BY_FIELD; + + private final Function afterOrderBy; + + /** + * Creates a new {@link SqlServerSelectRenderContext}. + * + * @param afterOrderBy the delegate {@code afterOrderBy} function. + */ + protected SqlServerSelectRenderContext(Function afterOrderBy) { + this.afterOrderBy = afterOrderBy; + } + + @Override + public Function afterSelectList() { + + return select -> { + + if (usesPagination(select) && select.getOrderBy().isEmpty()) { + return SYNTHETIC_SELECT_LIST; + } + + return ""; + }; + } + + @Override + public Function afterOrderBy(boolean hasOrderBy) { + + if (hasOrderBy) { + return afterOrderBy; + } + + return select -> { + + StringBuilder builder = new StringBuilder(); + + if (usesPagination(select)) { + builder.append(" ORDER BY " + SYNTHETIC_ORDER_BY_FIELD); + } + + builder.append(afterOrderBy.apply(select)); + + return builder; + }; + } + + private static boolean usesPagination(Select select) { + return select.getOffset().isPresent() || select.getLimit().isPresent(); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/package-info.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/package-info.java new file mode 100644 index 00000000..00c26658 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/package-info.java @@ -0,0 +1,7 @@ +/** + * Dialects abstract the SQL dialect of the underlying database. + */ +@NonNullApi +package org.springframework.data.relational.core.dialect; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java index 4bf9b051..ed22a44d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSelect.java @@ -16,6 +16,7 @@ package org.springframework.data.relational.core.sql; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.OptionalLong; @@ -48,10 +49,19 @@ class DefaultSelect implements Select { this.limit = limit; this.offset = offset; this.joins = new ArrayList<>(joins); - this.orderBy = new ArrayList<>(orderBy); + this.orderBy = Collections.unmodifiableList(new ArrayList<>(orderBy)); this.where = where != null ? new Where(where) : null; } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.Select#getOrderBy() + */ + @Override + public List getOrderBy() { + return this.orderBy; + } + /* * (non-Javadoc) * @see org.springframework.data.relational.core.sql.Select#getLimit() diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java index 31c31583..2618f99c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Select.java @@ -15,6 +15,7 @@ */ package org.springframework.data.relational.core.sql; +import java.util.List; import java.util.OptionalLong; /** @@ -45,6 +46,11 @@ public interface Select extends Segment, Visitable { return new DefaultSelectBuilder(); } + /** + * @return the {@link List} of {@link OrderByField ORDER BY} fields. + */ + List getOrderBy(); + /** * Optional limit. Used for limit/offset paging. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java index 402dee99..a8843c9e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/RenderContext.java @@ -29,4 +29,9 @@ public interface RenderContext { * @return the {@link RenderNamingStrategy}. */ RenderNamingStrategy getNamingStrategy(); + + /** + * @return the {@link SelectRenderContext}. + */ + SelectRenderContext getSelect(); } 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 56a72788..cf5a7e13 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 @@ -96,6 +96,9 @@ class SelectListVisitor extends TypedSubtreeVisitor implements PartR insideFunction = false; requiresComma = true; + } else if (segment instanceof AsteriskFromTable) { + builder.append("*"); + requiresComma = true; } else if (segment instanceof Column) { builder.append(context.getNamingStrategy().getName((Column) segment)); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectRenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectRenderContext.java new file mode 100644 index 00000000..78ccce91 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectRenderContext.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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 java.util.function.Function; + +import org.springframework.data.relational.core.sql.Select; + +/** + * Render context specifically for {@code SELECT} statements. This interface declares rendering hooks that are called + * before/after a specific {@code SELECT} clause part. The rendering content is appended directly after/before an + * element without further whitespace processing. Hooks are responsible for adding required surrounding whitespaces. + * + * @author Mark Paluch + * @since 1.1 + */ +public interface SelectRenderContext { + + /** + * Customization hook: Rendition of a part after the {@code SELECT} list and before any {@code FROM} renderings. + * Renders an empty string by default. + * + * @return render {@link Function} invoked after rendering {@code SELECT} list. + */ + default Function afterSelectList() { + return select -> ""; + } + + /** + * Customization hook: Rendition of a part after {@code ORDER BY}. The rendering function is called always, regardless + * whether {@code ORDER BY} exists or not. Renders an empty string by default. + * + * @param hasOrderBy the actual value whether the {@link Select} statement has a {@code ORDER BY} clause. + * @return render {@link Function} invoked after rendering {@code ORDER BY}. + */ + default Function afterOrderBy(boolean hasOrderBy) { + return select -> ""; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java index 17cee1ee..5ee2d6eb 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SelectStatementVisitor.java @@ -15,8 +15,6 @@ */ package org.springframework.data.relational.core.sql.render; -import java.util.OptionalLong; - import org.springframework.data.relational.core.sql.From; import org.springframework.data.relational.core.sql.Join; import org.springframework.data.relational.core.sql.OrderByField; @@ -35,6 +33,7 @@ import org.springframework.data.relational.core.sql.Where; class SelectStatementVisitor extends DelegatingVisitor implements PartRenderer { private final RenderContext context; + private final SelectRenderContext selectRenderContext; private StringBuilder builder = new StringBuilder(); private StringBuilder selectList = new StringBuilder(); @@ -50,6 +49,7 @@ class SelectStatementVisitor extends DelegatingVisitor implements PartRenderer { SelectStatementVisitor(RenderContext context) { this.context = context; + this.selectRenderContext = context.getSelect(); this.selectListVisitor = new SelectListVisitor(context, selectList::append); this.orderByClauseVisitor = new OrderByClauseVisitor(context); this.fromClauseVisitor = new FromClauseVisitor(context, it -> { @@ -110,12 +110,16 @@ class SelectStatementVisitor extends DelegatingVisitor implements PartRenderer { if (segment instanceof Select) { + Select select = (Select) segment; + builder.append("SELECT "); - if (((Select) segment).isDistinct()) { + + if (select.isDistinct()) { builder.append("DISTINCT "); } builder.append(selectList); + builder.append(selectRenderContext.afterSelectList().apply(select)); if (from.length() != 0) { builder.append(" FROM ").append(from); @@ -130,18 +134,11 @@ class SelectStatementVisitor extends DelegatingVisitor implements PartRenderer { } CharSequence orderBy = orderByClauseVisitor.getRenderedPart(); - if (orderBy.length() != 0) + if (orderBy.length() != 0) { builder.append(" ORDER BY ").append(orderBy); - - OptionalLong limit = ((Select) segment).getLimit(); - if (limit.isPresent()) { - builder.append(" LIMIT ").append(limit.getAsLong()); } - OptionalLong offset = ((Select) segment).getOffset(); - if (offset.isPresent()) { - builder.append(" OFFSET ").append(offset.getAsLong()); - } + builder.append(selectRenderContext.afterOrderBy(orderBy.length() != 0).apply(select)); return Delegation.leave(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java index 373efd29..a569b07d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/SimpleRenderContext.java @@ -28,4 +28,13 @@ class SimpleRenderContext implements RenderContext { private final RenderNamingStrategy namingStrategy; + @Override + public SelectRenderContext getSelect() { + return DefaultSelectRenderContext.INSTANCE; + } + + enum DefaultSelectRenderContext implements SelectRenderContext { + INSTANCE; + } + } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectRenderingTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectRenderingTests.java new file mode 100644 index 00000000..49f0b62b --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectRenderingTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 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.dialect; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.StatementBuilder; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.render.NamingStrategies; +import org.springframework.data.relational.core.sql.render.SqlRenderer; + +/** + * Tests for {@link MySqlDialect}-specific rendering. + * + * @author Mark Paluch + */ +public class MySqlDialectRenderingTests { + + private final RenderContextFactory factory = new RenderContextFactory(MySqlDialect.INSTANCE); + + @Before + public void before() { + factory.setNamingStrategy(NamingStrategies.asIs()); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithLimit() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).limit(10).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT foo.* FROM foo LIMIT 10"); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithOffset() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).offset(10).build(); + + assertThatThrownBy(() -> SqlRenderer.create(factory.createRenderContext()).render(select)) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithLimitOffset() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).limit(10).offset(20).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT foo.* FROM foo LIMIT 20, 10"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectUnitTests.java new file mode 100644 index 00000000..392bda48 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MySqlDialectUnitTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 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.dialect; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * Unit tests for {@link MySqlDialect}. + * + * @author Mark Paluch + */ +public class MySqlDialectUnitTests { + + @Test // DATAJDBC-278 + public void shouldNotSupportArrays() { + + ArrayColumns arrayColumns = MySqlDialect.INSTANCE.getArraySupport(); + + assertThat(arrayColumns.isSupported()).isFalse(); + } + + @Test // DATAJDBC-278 + public void shouldRenderLimit() { + + LimitClause limit = MySqlDialect.INSTANCE.limit(); + + assertThat(limit.getClausePosition()).isEqualTo(LimitClause.Position.AFTER_ORDER_BY); + assertThat(limit.getLimit(10)).isEqualTo("LIMIT 10"); + } + + @Test // DATAJDBC-278 + public void shouldRenderOffset() { + + LimitClause limit = MySqlDialect.INSTANCE.limit(); + + assertThatThrownBy(() -> limit.getOffset(10)).isInstanceOf(UnsupportedOperationException.class); + } + + @Test // DATAJDBC-278 + public void shouldRenderLimitOffset() { + + LimitClause limit = MySqlDialect.INSTANCE.limit(); + + assertThat(limit.getLimitOffset(20, 10)).isEqualTo("LIMIT 10, 20"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/PostgresDialectRenderingTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/PostgresDialectRenderingTests.java new file mode 100644 index 00000000..23e37cdb --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/PostgresDialectRenderingTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 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.dialect; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.StatementBuilder; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.render.NamingStrategies; +import org.springframework.data.relational.core.sql.render.SqlRenderer; + +/** + * Tests for {@link PostgresDialect}-specific rendering. + * + * @author Mark Paluch + */ +public class PostgresDialectRenderingTests { + + private final RenderContextFactory factory = new RenderContextFactory(PostgresDialect.INSTANCE); + + @Before + public void before() throws Exception { + factory.setNamingStrategy(NamingStrategies.asIs()); + } + + @Test // DATAJDBC-278 + public void shouldRenderSimpleSelect() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT foo.* FROM foo"); + } + + @Test // DATAJDBC-278 + public void shouldApplyNamingStrategy() { + + factory.setNamingStrategy(NamingStrategies.toUpper()); + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT FOO.* FROM FOO"); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithLimit() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).limit(10).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT foo.* FROM foo LIMIT 10"); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithOffset() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).offset(10).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT foo.* FROM foo OFFSET 10"); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithLimitOffset() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).limit(10).offset(20).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT foo.* FROM foo LIMIT 10 OFFSET 20"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/PostgresDialectUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/PostgresDialectUnitTests.java new file mode 100644 index 00000000..f51f8495 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/PostgresDialectUnitTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019 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.dialect; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.SoftAssertions.*; + +import org.junit.Test; + +/** + * Unit tests for {@link PostgresDialect}. + * + * @author Mark Paluch + */ +public class PostgresDialectUnitTests { + + @Test // DATAJDBC-278 + public void shouldSupportArrays() { + + ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport(); + + assertThat(arrayColumns.isSupported()).isTrue(); + } + + @Test // DATAJDBC-278 + public void shouldUseBoxedArrayTypesForPrimitiveTypes() { + + ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport(); + + assertSoftly(it -> { + it.assertThat(arrayColumns.getArrayType(int.class)).isEqualTo(Integer.class); + it.assertThat(arrayColumns.getArrayType(double.class)).isEqualTo(Double.class); + it.assertThat(arrayColumns.getArrayType(String.class)).isEqualTo(String.class); + }); + } + + @Test // DATAJDBC-278 + public void shouldRenderLimit() { + + LimitClause limit = PostgresDialect.INSTANCE.limit(); + + assertThat(limit.getClausePosition()).isEqualTo(LimitClause.Position.AFTER_ORDER_BY); + assertThat(limit.getLimit(10)).isEqualTo("LIMIT 10"); + } + + @Test // DATAJDBC-278 + public void shouldRenderOffset() { + + LimitClause limit = PostgresDialect.INSTANCE.limit(); + + assertThat(limit.getOffset(10)).isEqualTo("OFFSET 10"); + } + + @Test // DATAJDBC-278 + public void shouldRenderLimitOffset() { + + LimitClause limit = PostgresDialect.INSTANCE.limit(); + + assertThat(limit.getLimitOffset(20, 10)).isEqualTo("LIMIT 20 OFFSET 10"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/SqlServerDialectRenderingTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/SqlServerDialectRenderingTests.java new file mode 100644 index 00000000..35f6ff42 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/SqlServerDialectRenderingTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2019 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.dialect; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.StatementBuilder; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.render.NamingStrategies; +import org.springframework.data.relational.core.sql.render.SqlRenderer; + +/** + * Tests for {@link SqlServerDialect}-specific rendering. + * + * @author Mark Paluch + */ +public class SqlServerDialectRenderingTests { + + private final RenderContextFactory factory = new RenderContextFactory(SqlServerDialect.INSTANCE); + + @Before + public void before() { + factory.setNamingStrategy(NamingStrategies.asIs()); + } + + @Test // DATAJDBC-278 + public void shouldRenderSimpleSelect() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT foo.* FROM foo"); + } + + @Test // DATAJDBC-278 + public void shouldApplyNamingStrategy() { + + factory.setNamingStrategy(NamingStrategies.toUpper()); + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT FOO.* FROM FOO"); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithLimit() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).limit(10).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo( + "SELECT foo.*, ROW_NUMBER() over (ORDER BY CURRENT_TIMESTAMP) AS __relational_row_number__ FROM foo ORDER BY __relational_row_number__ OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY"); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithOffset() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).offset(10).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo( + "SELECT foo.*, ROW_NUMBER() over (ORDER BY CURRENT_TIMESTAMP) AS __relational_row_number__ FROM foo ORDER BY __relational_row_number__ OFFSET 10 ROWS"); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithLimitOffset() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).limit(10).offset(20).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo( + "SELECT foo.*, ROW_NUMBER() over (ORDER BY CURRENT_TIMESTAMP) AS __relational_row_number__ FROM foo ORDER BY __relational_row_number__ OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY"); + } + + @Test // DATAJDBC-278 + public void shouldRenderSelectWithLimitOffsetAndOrderBy() { + + Table table = Table.create("foo"); + Select select = StatementBuilder.select(table.asterisk()).from(table).orderBy(table.column("column_1")).limit(10) + .offset(20).build(); + + String sql = SqlRenderer.create(factory.createRenderContext()).render(select); + + assertThat(sql).isEqualTo("SELECT foo.* FROM foo ORDER BY column_1 OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/SqlServerDialectUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/SqlServerDialectUnitTests.java new file mode 100644 index 00000000..b371282d --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/SqlServerDialectUnitTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 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.dialect; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * Unit tests for {@link SqlServerDialect}. + * + * @author Mark Paluch + */ +public class SqlServerDialectUnitTests { + + @Test // DATAJDBC-278 + public void shouldNotSupportArrays() { + + ArrayColumns arrayColumns = SqlServerDialect.INSTANCE.getArraySupport(); + + assertThat(arrayColumns.isSupported()).isFalse(); + assertThatThrownBy(() -> arrayColumns.getArrayType(String.class)).isInstanceOf(UnsupportedOperationException.class); + } + + @Test // DATAJDBC-278 + public void shouldRenderLimit() { + + LimitClause limit = SqlServerDialect.INSTANCE.limit(); + + assertThat(limit.getClausePosition()).isEqualTo(LimitClause.Position.AFTER_ORDER_BY); + assertThat(limit.getLimit(10)).isEqualTo("OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY"); + } + + @Test // DATAJDBC-278 + public void shouldRenderOffset() { + + LimitClause limit = SqlServerDialect.INSTANCE.limit(); + + assertThat(limit.getOffset(10)).isEqualTo("OFFSET 10 ROWS"); + } + + @Test // DATAJDBC-278 + public void shouldRenderLimitOffset() { + + LimitClause limit = SqlServerDialect.INSTANCE.limit(); + + assertThat(limit.getLimitOffset(20, 10)).isEqualTo("OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY"); + } +} 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 f61b17dc..d6b8b4e7 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 @@ -37,13 +37,13 @@ import org.springframework.util.StringUtils; */ public class SelectRendererUnitTests { - @Test // DATAJDBC-309 + @Test // DATAJDBC-309, DATAJDBC-278 public void shouldRenderSingleColumn() { Table bar = SQL.table("bar"); Column foo = bar.column("foo"); - Select select = Select.builder().select(foo).from(bar).build(); + Select select = Select.builder().select(foo).from(bar).limitOffset(1, 2).build(); assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT bar.foo FROM bar"); } @@ -180,17 +180,6 @@ public class SelectRendererUnitTests { .isEqualTo("SELECT emp.name AS emp_name FROM employee AS emp ORDER BY emp_name ASC"); } - @Test // DATAJDBC-309 - public void shouldRenderOrderLimitOffset() { - - Table table = SQL.table("foo"); - Column bar = table.column("bar"); - - Select select = Select.builder().select(bar).from("foo").limitOffset(10, 20).build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT foo.bar FROM foo LIMIT 10 OFFSET 20"); - } - @Test // DATAJDBC-309 public void shouldRenderIsNull() {