DATAJDBC-357 - Dialect support pagination and array support.

We now support Dialect abstractions to consider vendor-specific deviations in SQL query rendering. Dialects support right now:

Feature flags for array support
Limit and Offset handling
The rendering is extended by considering rendering callback hook functions.

Current dialects:

Postgres SQL
SQL Server (2012)
MySQL

Original pull request: #125.
This commit is contained in:
Mark Paluch
2019-03-11 15:50:59 +01:00
committed by Jens Schauder
parent 141f2d77ac
commit 488ef53157
24 changed files with 1407 additions and 26 deletions

View File

@@ -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<Select, ? extends CharSequence> 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<Select, CharSequence> getAfterOrderBy() {
Function<Select, ? extends CharSequence> 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<Select, ? extends CharSequence> afterOrderBy;
DialectSelectRenderContext(Function<Select, ? extends CharSequence> afterOrderBy) {
this.afterOrderBy = afterOrderBy;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.render.SelectRenderContext#afterOrderBy(boolean)
*/
@Override
public Function<Select, ? extends CharSequence> afterOrderBy(boolean hasOrderBy) {
return afterOrderBy;
}
}
/**
* After {@code ORDER BY} function rendering the {@link LimitClause}.
*/
@RequiredArgsConstructor
static class AfterOrderByLimitRenderFunction implements Function<Select, CharSequence> {
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<CharSequence, CharSequence> {
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;
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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();
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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> 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();
}
}

View File

@@ -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:
* <ul>
* <li>Appends a synthetic ROW_NUMBER when using pagination and the query does not specify ordering</li>
* <li>Append synthetic ordering if query uses pagination and the query does not specify ordering</li>
* </ul>
*
* @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<Select, CharSequence> afterOrderBy;
/**
* Creates a new {@link SqlServerSelectRenderContext}.
*
* @param afterOrderBy the delegate {@code afterOrderBy} function.
*/
protected SqlServerSelectRenderContext(Function<Select, CharSequence> afterOrderBy) {
this.afterOrderBy = afterOrderBy;
}
@Override
public Function<Select, ? extends CharSequence> afterSelectList() {
return select -> {
if (usesPagination(select) && select.getOrderBy().isEmpty()) {
return SYNTHETIC_SELECT_LIST;
}
return "";
};
}
@Override
public Function<Select, ? extends CharSequence> 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();
}
}

View File

@@ -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;

View File

@@ -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<OrderByField> getOrderBy() {
return this.orderBy;
}
/*
* (non-Javadoc)
* @see org.springframework.data.relational.core.sql.Select#getLimit()

View File

@@ -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<OrderByField> getOrderBy();
/**
* Optional limit. Used for limit/offset paging.
*

View File

@@ -29,4 +29,9 @@ public interface RenderContext {
* @return the {@link RenderNamingStrategy}.
*/
RenderNamingStrategy getNamingStrategy();
/**
* @return the {@link SelectRenderContext}.
*/
SelectRenderContext getSelect();
}

View File

@@ -96,6 +96,9 @@ class SelectListVisitor extends TypedSubtreeVisitor<SelectList> 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));

View File

@@ -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<Select, ? extends CharSequence> 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<Select, ? extends CharSequence> afterOrderBy(boolean hasOrderBy) {
return select -> "";
}
}

View File

@@ -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();
}

View File

@@ -28,4 +28,13 @@ class SimpleRenderContext implements RenderContext {
private final RenderNamingStrategy namingStrategy;
@Override
public SelectRenderContext getSelect() {
return DefaultSelectRenderContext.INSTANCE;
}
enum DefaultSelectRenderContext implements SelectRenderContext {
INSTANCE;
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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() {