This is an automated email from the ASF dual-hosted git repository.
xiedeyantu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new c0295dc6b4 [CALCITE-7592] Add expression support for FETCH
c0295dc6b4 is described below
commit c0295dc6b4380ecee0a3935a6f971ff4deab4c8d
Author: Kirill Tkalenko <[email protected]>
AuthorDate: Fri Jul 17 14:33:04 2026 +0300
[CALCITE-7592] Add expression support for FETCH
---
core/src/main/codegen/templates/Parser.jj | 20 +-
.../calcite/adapter/enumerable/EnumUtils.java | 7 +-
.../adapter/enumerable/EnumerableLimit.java | 20 +-
.../adapter/enumerable/EnumerableLimitSort.java | 6 +-
.../enumerable/EnumerableMergeUnionRule.java | 9 +-
.../org/apache/calcite/interpreter/SortNode.java | 102 +++++--
.../calcite/rel/metadata/RelMdMaxRowCount.java | 15 +-
.../calcite/rel/metadata/RelMdMinRowCount.java | 17 +-
.../apache/calcite/rel/metadata/RelMdRowCount.java | 12 +-
.../org/apache/calcite/rel/metadata/RelMdUtil.java | 15 +-
.../calcite/rel/rel2sql/RelToSqlConverter.java | 12 +-
.../org/apache/calcite/rel/rules/MeasureRules.java | 4 +-
.../apache/calcite/rel/rules/PruneEmptyRules.java | 6 +-
.../calcite/rel/rules/SortJoinTransposeRule.java | 4 +-
.../calcite/rel/rules/SortRemoveRedundantRule.java | 3 +
.../calcite/rel/rules/SortUnionTransposeRule.java | 9 +-
.../main/java/org/apache/calcite/rex/RexUtil.java | 80 +++++
.../apache/calcite/runtime/CalciteResource.java | 9 +
.../java/org/apache/calcite/sql/SqlDialect.java | 34 ++-
.../calcite/sql/dialect/SqliteSqlDialect.java | 2 +-
.../calcite/sql/validate/SqlValidatorImpl.java | 29 ++
.../apache/calcite/sql2rel/RelDecorrelator.java | 29 +-
.../sql2rel/TopDownGeneralDecorrelator.java | 35 ++-
.../java/org/apache/calcite/tools/RelBuilder.java | 52 +++-
.../calcite/runtime/CalciteResource.properties | 3 +
.../calcite/adapter/enumerable/EnumUtilsTest.java | 11 +
.../calcite/rel/rel2sql/RelToSqlConverterTest.java | 68 +++++
.../org/apache/calcite/rex/RexProgramTest.java | 30 ++
.../java/org/apache/calcite/test/JdbcTest.java | 338 +++++++++++++++++++++
.../org/apache/calcite/test/RelBuilderTest.java | 105 +++++++
.../org/apache/calcite/test/RelMetadataTest.java | 33 +-
.../org/apache/calcite/test/RelOptRulesTest.java | 81 ++++-
.../apache/calcite/test/SqlToRelConverterTest.java | 9 +
.../org/apache/calcite/test/SqlValidatorTest.java | 16 +
.../test/enumerable/EnumerableMergeUnionTest.java | 30 ++
.../org/apache/calcite/test/RelOptRulesTest.xml | 197 +++++++++++-
.../apache/calcite/test/SqlToRelConverterTest.xml | 12 +
core/src/test/resources/sql/fetch.iq | 183 +++++++++++
.../java/org/apache/calcite/test/ServerTest.java | 37 +++
site/_docs/reference.md | 9 +-
.../apache/calcite/sql/parser/SqlParserTest.java | 19 ++
41 files changed, 1619 insertions(+), 93 deletions(-)
diff --git a/core/src/main/codegen/templates/Parser.jj
b/core/src/main/codegen/templates/Parser.jj
index 7246e60842..ce69124c4b 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -709,7 +709,7 @@ SqlNode ExprOrJoinOrOrderedQuery(ExprContext exprContext) :
*
* <blockquote><pre>
* [ OFFSET start { ROW | ROWS } ]
- * [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ]</pre>
+ * [ FETCH { FIRST | NEXT } [ count | (expression) ] { ROW | ROWS } ONLY
]</pre>
* </blockquote>
*/
SqlNode OrderedQueryOrExpr(ExprContext exprContext) :
@@ -796,10 +796,26 @@ void FetchClause(SqlNode[] offsetFetch) :
{
// SQL:2008-style syntax. "OFFSET ... FETCH ...".
// If you specify both LIMIT and FETCH, FETCH wins.
- <FETCH> ( <FIRST> | <NEXT> ) offsetFetch[1] =
UnsignedNumericLiteralOrParam()
+ <FETCH> ( <FIRST> | <NEXT> ) offsetFetch[1] = FetchCount()
( <ROW> | <ROWS> ) <ONLY>
}
+/**
+ * Parses the row count of a FETCH clause. Expressions must be parenthesized.
+ */
+SqlNode FetchCount() :
+{
+ final SqlNode e;
+}
+{
+ (
+ e = UnsignedNumericLiteralOrParam()
+ |
+ <LPAREN> e = Expression(ExprContext.ACCEPT_NON_QUERY) <RPAREN>
+ )
+ { return e; }
+}
+
/**
* Parses a LIMIT clause in an ORDER BY expression.
*/
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java
index d3da43466d..40a824536b 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java
@@ -116,7 +116,7 @@ private EnumUtils() {}
/** Converts a FETCH or OFFSET runtime value to {@link BigDecimal}.
*
* <p>The value must be numeric and non-negative. */
- public static BigDecimal numberToBigDecimal(Object value, String kind) {
+ public static BigDecimal numberToBigDecimal(@Nullable Object value, String
kind) {
return numberToBigDecimal(value, kind, FetchOffsetRoundingPolicy.NONE);
}
@@ -124,8 +124,11 @@ public static BigDecimal numberToBigDecimal(Object value,
String kind) {
*
* <p>The value must be numeric and non-negative. The result is adjusted by
* the configured rounding policy. */
- public static BigDecimal numberToBigDecimal(Object value, String kind,
+ public static BigDecimal numberToBigDecimal(@Nullable Object value, String
kind,
FetchOffsetRoundingPolicy roundingPolicy) {
+ if (value == null) {
+ throw new IllegalArgumentException(kind + " expression evaluated to
NULL");
+ }
if (!(value instanceof Number)) {
throw new IllegalArgumentException(kind + " must be a number");
}
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimit.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimit.java
index 02fd54bdad..de1f94d562 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimit.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimit.java
@@ -106,13 +106,15 @@ public static EnumerableLimit create(final RelNode input,
@Nullable RexNode offs
v =
builder.append("offset",
Expressions.call(BuiltInMethod.SKIP_BIG_DECIMAL.method, v,
- getExpression(offset, "OFFSET", roundingPolicyExp)));
+ getExpression(offset, "OFFSET", implementor, builder,
+ roundingPolicyExp, false)));
}
if (fetch != null) {
v =
builder.append("fetch",
Expressions.call(BuiltInMethod.TAKE_BIG_DECIMAL.method, v,
- getExpression(fetch, "FETCH", roundingPolicyExp)));
+ getExpression(fetch, "FETCH", implementor, builder,
+ roundingPolicyExp, true)));
}
builder.add(Expressions.return_(null, v));
@@ -120,7 +122,8 @@ public static EnumerableLimit create(final RelNode input,
@Nullable RexNode offs
}
static Expression getExpression(RexNode rexNode, String kind,
- Expression roundingPolicy) {
+ EnumerableRelImplementor implementor, BlockBuilder builder,
+ Expression roundingPolicy, boolean translateExpression) {
final Expression value;
if (rexNode instanceof RexDynamicParam) {
final RexDynamicParam param = (RexDynamicParam) rexNode;
@@ -128,8 +131,17 @@ static Expression getExpression(RexNode rexNode, String
kind,
Expressions.call(DataContext.ROOT,
BuiltInMethod.DATA_CONTEXT_GET.method,
Expressions.constant("?" + param.getIndex()));
- } else {
+ } else if (rexNode instanceof RexLiteral) {
value = Expressions.constant(RexLiteral.bigDecimalValue(rexNode));
+ } else {
+ if (!translateExpression) {
+ throw new IllegalArgumentException(kind + " must be a literal or
dynamic parameter");
+ }
+
+ value =
+ RexToLixTranslator.forAggregation(implementor.getTypeFactory(),
+ builder, null, implementor.getConformance())
+ .translate(rexNode);
}
return Expressions.call(
BuiltInMethod.NUMBER_TO_BIG_DECIMAL_LIMIT.method,
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitSort.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitSort.java
index 325fe687ba..97d9fd8169 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitSort.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitSort.java
@@ -104,14 +104,16 @@ public static EnumerableLimitSort create(
if (this.fetch == null) {
fetchVal = Expressions.constant(BigDecimal.valueOf(Integer.MAX_VALUE));
} else {
- fetchVal = getExpression(this.fetch, "FETCH", roundingPolicyExp);
+ fetchVal =
+ getExpression(this.fetch, "FETCH", implementor, builder,
roundingPolicyExp, true);
}
final Expression offsetVal;
if (this.offset == null) {
offsetVal = Expressions.constant(BigDecimal.ZERO);
} else {
- offsetVal = getExpression(this.offset, "OFFSET", roundingPolicyExp);
+ offsetVal =
+ getExpression(this.offset, "OFFSET", implementor, builder,
roundingPolicyExp, false);
}
builder.add(
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeUnionRule.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeUnionRule.java
index 7d47e639b7..57f864794a 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeUnionRule.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeUnionRule.java
@@ -29,6 +29,7 @@
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.ImmutableBitSet;
@@ -88,9 +89,13 @@ public EnumerableMergeUnionRule(Config config) {
// Push down sort limit, if possible.
RexNode inputFetch = null;
if (sort.fetch != null) {
- if (sort.offset == null) {
+ final boolean safeToReevaluate =
+ RexUtil.isDeterministic(sort.fetch);
+ if (sort.offset == null && safeToReevaluate) {
inputFetch = sort.fetch;
- } else if (sort.fetch instanceof RexLiteral && sort.offset instanceof
RexLiteral) {
+ } else if (safeToReevaluate
+ && sort.fetch instanceof RexLiteral
+ && sort.offset instanceof RexLiteral) {
inputFetch =
call.builder().literal(RexLiteral.bigDecimalValue(sort.fetch)
.add(RexLiteral.bigDecimalValue(sort.offset)));
diff --git a/core/src/main/java/org/apache/calcite/interpreter/SortNode.java
b/core/src/main/java/org/apache/calcite/interpreter/SortNode.java
index 71d9f2b22e..0f393a3e68 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/SortNode.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/SortNode.java
@@ -16,14 +16,22 @@
*/
package org.apache.calcite.interpreter;
+import org.apache.calcite.adapter.enumerable.EnumUtils;
+import org.apache.calcite.adapter.enumerable.EnumerableRelImplementor;
+import org.apache.calcite.adapter.enumerable.FetchOffsetRoundingPolicy;
import org.apache.calcite.rel.RelFieldCollation;
import org.apache.calcite.rel.core.Sort;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.util.Util;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@@ -35,37 +43,57 @@
* {@link org.apache.calcite.rel.core.Sort}.
*/
public class SortNode extends AbstractSingleNode<Sort> {
+ private final @Nullable Scalar offsetScalar;
+ private final @Nullable Context offsetContext;
+ private final @Nullable Scalar fetchScalar;
+ private final @Nullable Context fetchContext;
+ private final FetchOffsetRoundingPolicy fetchOffsetRoundingPolicy;
+
public SortNode(Compiler compiler, Sort rel) {
super(compiler, rel);
- }
-
- private static int getValueAsInt(RexNode node) {
- return requireNonNull(((RexLiteral) node).getValueAs(Integer.class),
- () -> "getValueAs(Integer.class) for " + node);
+ if (rel.offset != null && !(rel.offset instanceof RexLiteral)) {
+ this.offsetScalar = compiler.compile(ImmutableList.of(rel.offset), null);
+ this.offsetContext = compiler.createContext();
+ } else {
+ this.offsetScalar = null;
+ this.offsetContext = null;
+ }
+ if (rel.fetch != null && !(rel.fetch instanceof RexLiteral)) {
+ this.fetchScalar = compiler.compile(ImmutableList.of(rel.fetch), null);
+ this.fetchContext = compiler.createContext();
+ } else {
+ this.fetchScalar = null;
+ this.fetchContext = null;
+ }
+ final Object roundingPolicy = compiler.getDataContext()
+ .get(EnumerableRelImplementor.FETCH_OFFSET_ROUNDING_POLICY);
+ this.fetchOffsetRoundingPolicy =
+ roundingPolicy instanceof FetchOffsetRoundingPolicy
+ ? (FetchOffsetRoundingPolicy) roundingPolicy
+ : FetchOffsetRoundingPolicy.NONE;
}
@Override public void run() throws InterruptedException {
- final int offset =
- rel.offset == null
- ? 0
- : getValueAsInt(rel.offset);
- final int fetch =
- rel.fetch == null
- ? -1
- : getValueAsInt(rel.fetch);
+ final BigDecimal offset = getOffset();
+ final @Nullable BigDecimal fetch = getFetch();
// In pure limit mode. No sort required.
Row row;
loop:
if (rel.getCollation().getFieldCollations().isEmpty()) {
- for (int i = 0; i < offset; i++) {
+ BigDecimal skipped = BigDecimal.ZERO;
+ while (skipped.compareTo(offset) < 0) {
row = source.receive();
if (row == null) {
break loop;
}
+ skipped = skipped.add(BigDecimal.ONE);
}
- if (fetch >= 0) {
- for (int i = 0; i < fetch && (row = source.receive()) != null; i++) {
+ if (fetch != null) {
+ BigDecimal fetched = BigDecimal.ZERO;
+ while (fetched.compareTo(fetch) < 0
+ && (row = source.receive()) != null) {
sink.send(row);
+ fetched = fetched.add(BigDecimal.ONE);
}
} else {
while ((row = source.receive()) != null) {
@@ -79,10 +107,15 @@ private static int getValueAsInt(RexNode node) {
list.add(row);
}
list.sort(comparator());
- final int end = fetch < 0 || offset + fetch > list.size()
+ final int start = offset.compareTo(BigDecimal.valueOf(list.size())) >= 0
+ ? list.size()
+ : rowCount(offset);
+ final int available = list.size() - start;
+ final int end = fetch == null
+ || fetch.compareTo(BigDecimal.valueOf(available)) >= 0
? list.size()
- : offset + fetch;
- for (int i = offset; i < end; i++) {
+ : start + rowCount(fetch);
+ for (int i = start; i < end; i++) {
sink.send(list.get(i));
}
}
@@ -116,4 +149,35 @@ private static Comparator<Row>
comparator(RelFieldCollation fieldCollation) {
};
}
}
+
+ private @Nullable BigDecimal getFetch() {
+ if (rel.fetch == null) {
+ return null;
+ }
+ return getValue(rel.fetch, fetchScalar, fetchContext, "FETCH");
+ }
+
+ private BigDecimal getOffset() {
+ if (rel.offset == null) {
+ return BigDecimal.ZERO;
+ }
+ return getValue(rel.offset, offsetScalar, offsetContext, "OFFSET");
+ }
+
+ private BigDecimal getValue(RexNode node, @Nullable Scalar scalar,
+ @Nullable Context context, String kind) {
+ final @Nullable Object value;
+ if (node instanceof RexLiteral) {
+ value = RexLiteral.bigDecimalValue(node);
+ } else {
+ value =
+ requireNonNull(scalar, () -> kind + " scalar")
+ .execute(requireNonNull(context, () -> kind + " context"));
+ }
+ return EnumUtils.numberToBigDecimal(value, kind,
fetchOffsetRoundingPolicy);
+ }
+
+ private static int rowCount(BigDecimal value) {
+ return value.setScale(0, RoundingMode.CEILING).intValueExact();
+ }
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdMaxRowCount.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdMaxRowCount.java
index e728c22e1e..869f1ad50e 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdMaxRowCount.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdMaxRowCount.java
@@ -117,10 +117,12 @@ public Double getMaxRowCount(Sort rel, RelMetadataQuery
mq) {
rowCount = Double.POSITIVE_INFINITY;
}
- final double offset = literalValueApproximatedByDouble(rel.offset, 0D);
+ final double offset =
+ literalValueApproximatedByDouble(rel.offset, 0D);
rowCount = Math.max(rowCount - offset, 0D);
- final double limit = literalValueApproximatedByDouble(rel.fetch, rowCount);
+ final double limit =
+ literalValueApproximatedByDouble(rel.fetch, rowCount);
return limit < rowCount ? limit : rowCount;
}
@@ -130,10 +132,12 @@ public Double getMaxRowCount(EnumerableLimit rel,
RelMetadataQuery mq) {
rowCount = Double.POSITIVE_INFINITY;
}
- final double offset = literalValueApproximatedByDouble(rel.offset, 0D);
+ final double offset =
+ literalValueApproximatedByDouble(rel.offset, 0D);
rowCount = Math.max(rowCount - offset, 0D);
- final double limit = literalValueApproximatedByDouble(rel.fetch, rowCount);
+ final double limit =
+ literalValueApproximatedByDouble(rel.fetch, rowCount);
return limit < rowCount ? limit : rowCount;
}
@@ -214,7 +218,8 @@ public Double getMaxRowCount(RelSubset rel,
RelMetadataQuery mq) {
if (node instanceof Sort) {
Sort sort = (Sort) node;
if (sort.fetch instanceof RexLiteral) {
- return literalValueApproximatedByDouble(sort.fetch,
Double.POSITIVE_INFINITY);
+ return literalValueApproximatedByDouble(sort.fetch,
+ Double.POSITIVE_INFINITY);
}
}
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdMinRowCount.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdMinRowCount.java
index 869d343335..2cb710f398 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdMinRowCount.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdMinRowCount.java
@@ -116,10 +116,13 @@ public Double getMinRowCount(Sort rel, RelMetadataQuery
mq) {
rowCount = 0D;
}
- final double offset = literalValueApproximatedByDouble(rel.offset, 0D);
+ final double offset =
+ literalValueApproximatedByDouble(rel.offset, 0D);
rowCount = Math.max(rowCount - offset, 0D);
- final double limit = literalValueApproximatedByDouble(rel.fetch, rowCount);
+ final double limit =
+ literalValueApproximatedByDouble(rel.fetch,
+ rel.fetch == null ? rowCount : 0D);
return limit < rowCount ? limit : rowCount;
}
@@ -129,10 +132,13 @@ public Double getMinRowCount(EnumerableLimit rel,
RelMetadataQuery mq) {
rowCount = 0D;
}
- final double offset = literalValueApproximatedByDouble(rel.offset, 0D);
+ final double offset =
+ literalValueApproximatedByDouble(rel.offset, 0D);
rowCount = Math.max(rowCount - offset, 0D);
- final double limit = literalValueApproximatedByDouble(rel.fetch, rowCount);
+ final double limit =
+ literalValueApproximatedByDouble(rel.fetch,
+ rel.fetch == null ? rowCount : 0D);
return limit < rowCount ? limit : rowCount;
}
@@ -174,7 +180,8 @@ public Double getMinRowCount(RelSubset rel,
RelMetadataQuery mq) {
if (node instanceof Sort) {
Sort sort = (Sort) node;
if (sort.fetch instanceof RexLiteral) {
- return literalValueApproximatedByDouble(sort.fetch,
Double.POSITIVE_INFINITY);
+ return literalValueApproximatedByDouble(sort.fetch,
+ Double.POSITIVE_INFINITY);
}
}
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
index e83f4c1da9..3e7824e1aa 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdRowCount.java
@@ -165,10 +165,12 @@ public Double getRowCount(Calc rel, RelMetadataQuery mq) {
return null;
}
- final double offset = literalValueApproximatedByDouble(rel.offset, 0D);
+ final double offset =
+ literalValueApproximatedByDouble(rel.offset, 0D);
rowCount = Math.max(rowCount - offset, 0D);
- final double limit = literalValueApproximatedByDouble(rel.fetch, rowCount);
+ final double limit =
+ literalValueApproximatedByDouble(rel.fetch, rowCount);
return limit < rowCount ? limit : rowCount;
}
@@ -178,10 +180,12 @@ public Double getRowCount(Calc rel, RelMetadataQuery mq) {
return null;
}
- final double offset = literalValueApproximatedByDouble(rel.offset, 0D);
+ final double offset =
+ literalValueApproximatedByDouble(rel.offset, 0D);
rowCount = Math.max(rowCount - offset, 0D);
- final double limit = literalValueApproximatedByDouble(rel.fetch, rowCount);
+ final double limit =
+ literalValueApproximatedByDouble(rel.fetch, rowCount);
return limit < rowCount ? limit : rowCount;
}
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java
b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java
index 1f65022436..5b09628938 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java
@@ -25,6 +25,7 @@
import org.apache.calcite.rel.core.JoinRelType;
import org.apache.calcite.rel.core.Minus;
import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.Sort;
import org.apache.calcite.rel.core.Union;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
@@ -56,6 +57,7 @@
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import static com.google.common.base.Preconditions.checkArgument;
@@ -483,6 +485,9 @@ public static double
literalValueApproximatedByDouble(@Nullable RexNode node,
throw new IllegalArgumentException(
"literal value " + number + " cannot be converted to BigDecimal");
}
+ if (decimal.signum() < 0) {
+ return defaultValue;
+ }
if (decimal.abs().compareTo(BigDecimal.valueOf(Double.MAX_VALUE)) > 0) {
throw new IllegalArgumentException(
"literal value " + decimal + " exceeds double range");
@@ -1043,8 +1048,16 @@ private static boolean alreadySmaller(RelMetadataQuery
mq, RelNode input,
if (fetch == null) {
return true;
}
+ final RelNode strippedInput = input.stripped();
+ if (strippedInput instanceof Sort) {
+ final Sort sort = (Sort) strippedInput;
+ if (Objects.equals(offset, sort.offset)
+ && Objects.equals(fetch, sort.fetch)) {
+ return true;
+ }
+ }
final Double rowCount = mq.getMaxRowCount(input);
- if (rowCount == null || offset instanceof RexDynamicParam || fetch
instanceof RexDynamicParam) {
+ if (rowCount == null || offset instanceof RexDynamicParam || !(fetch
instanceof RexLiteral)) {
// Cannot be determined
return false;
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
index 8871f24d3e..718e6b1cb9 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
@@ -59,6 +59,7 @@
import org.apache.calcite.rex.RexLocalRef;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexProgram;
+import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.sql.JoinConditionType;
import org.apache.calcite.sql.JoinType;
import org.apache.calcite.sql.SqlAsofJoin;
@@ -1227,7 +1228,7 @@ public Result visit(Sort e) {
sqlSelect.setOffset(offset);
}
if (e.fetch != null) {
- SqlNode fetch = builder.context.toSql(null, e.fetch);
+ SqlNode fetch = toSqlFetch(e, builder.context);
sqlSelect.setFetch(fetch);
}
return result(sqlSelect, ImmutableList.of(Clause.ORDER_BY), e, null);
@@ -1285,13 +1286,20 @@ public Result visit(Sort e) {
* The builder must have been created with OFFSET and FETCH clauses. */
void offsetFetch(Sort e, Builder builder) {
if (e.fetch != null) {
- builder.setFetch(builder.context.toSql(null, e.fetch));
+ builder.setFetch(toSqlFetch(e, builder.context));
}
if (e.offset != null) {
builder.setOffset(builder.context.toSql(null, e.offset));
}
}
+ private static SqlNode toSqlFetch(Sort sort, Context context) {
+ final RexNode fetch = requireNonNull(sort.fetch, "fetch");
+ final @Nullable RexLiteral reduced =
+ RexUtil.reduceFetchToLiteral(sort.getCluster(), fetch);
+ return context.toSql(null, reduced == null ? fetch : reduced);
+ }
+
public boolean hasTrickyRollup(Sort e, Aggregate aggregate) {
return !dialect.supportsAggregateFunction(SqlKind.ROLLUP)
&& dialect.supportsGroupByWithRollup()
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/MeasureRules.java
b/core/src/main/java/org/apache/calcite/rel/rules/MeasureRules.java
index 037a4d6054..f69810a14d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/MeasureRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/MeasureRules.java
@@ -30,7 +30,6 @@
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexCorrelVariable;
import org.apache.calcite.rex.RexInputRef;
-import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexShuttle;
import org.apache.calcite.rex.RexUtil;
@@ -508,8 +507,7 @@ protected
ProjectSortMeasureRule(ProjectSortMeasureRuleConfig config) {
relBuilder.push(sort.getInput())
.projectPlus(map.keySet())
- .sortLimit(sort.offset == null ? 0 :
RexLiteral.numberValue(sort.offset),
- sort.fetch == null ? -1 : RexLiteral.numberValue(sort.fetch),
+ .sortLimit(sort.offset, sort.fetch,
sort.getSortExps())
.project(newProjects);
call.transformTo(relBuilder.build());
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java
b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java
index 02b0bd8af1..95331c69a4 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/PruneEmptyRules.java
@@ -42,7 +42,6 @@
import org.apache.calcite.rel.logical.LogicalValues;
import org.apache.calcite.rel.metadata.RelMdUtil;
import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rex.RexDynamicParam;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.tools.RelBuilder;
@@ -536,9 +535,8 @@ public interface SortFetchZeroRuleConfig extends
PruneEmptyRule.Config {
return new RemoveEmptySingleRule(this) {
@Override public boolean matches(final RelOptRuleCall call) {
Sort sort = call.rel(0);
- return sort.fetch != null
- && !(sort.fetch instanceof RexDynamicParam)
- &&
RexLiteral.bigDecimalValue(sort.fetch).equals(BigDecimal.ZERO);
+ return sort.fetch instanceof RexLiteral
+ &&
BigDecimal.ZERO.equals(RexLiteral.bigDecimalValue(sort.fetch));
}
};
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
b/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
index 4310d6d655..df967e56aa 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/SortJoinTransposeRule.java
@@ -105,9 +105,9 @@ public SortJoinTransposeRule(Class<? extends Sort>
sortClass,
final Sort sort = call.rel(0);
final Join join = call.rel(1);
- // Do nothing if SORT contains dynamic parameters in offset or fetch
+ // The pushed fetch is calculated from literal offset and fetch values.
if (sort.offset instanceof RexDynamicParam
- || sort.fetch instanceof RexDynamicParam) {
+ || sort.fetch != null && !(sort.fetch instanceof RexLiteral)) {
return false;
}
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/SortRemoveRedundantRule.java
b/core/src/main/java/org/apache/calcite/rel/rules/SortRemoveRedundantRule.java
index 9bcf026fc6..08563cdcdb 100644
---
a/core/src/main/java/org/apache/calcite/rel/rules/SortRemoveRedundantRule.java
+++
b/core/src/main/java/org/apache/calcite/rel/rules/SortRemoveRedundantRule.java
@@ -133,6 +133,9 @@ protected SortRemoveRedundantRule(final
SortRemoveRedundantRule.Config config) {
private static Optional<BigDecimal> getRowCountThreshold(Sort sort) {
if (RelOptUtil.isLimit(sort)) {
assert sort.fetch != null;
+ if (!(sort.fetch instanceof RexLiteral)) {
+ return Optional.empty();
+ }
final BigDecimal fetch = RexLiteral.bigDecimalValue(sort.fetch);
// We don't need to deal with fetch is 0.
diff --git
a/core/src/main/java/org/apache/calcite/rel/rules/SortUnionTransposeRule.java
b/core/src/main/java/org/apache/calcite/rel/rules/SortUnionTransposeRule.java
index 416825ee92..93b6af657c 100644
---
a/core/src/main/java/org/apache/calcite/rel/rules/SortUnionTransposeRule.java
+++
b/core/src/main/java/org/apache/calcite/rel/rules/SortUnionTransposeRule.java
@@ -23,7 +23,7 @@
import org.apache.calcite.rel.core.Union;
import org.apache.calcite.rel.metadata.RelMdUtil;
import org.apache.calcite.rel.metadata.RelMetadataQuery;
-import org.apache.calcite.rex.RexDynamicParam;
+import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.tools.RelBuilderFactory;
import org.immutables.value.Value;
@@ -67,13 +67,14 @@ public SortUnionTransposeRule(
@Override public boolean matches(RelOptRuleCall call) {
final Sort sort = call.rel(0);
final Union union = call.rel(1);
- // We only apply this rule if Union.all is true, Sort.offset is null and
Sort.fetch is not
- // a dynamic param.
+ // Re-evaluating a non-deterministic FETCH in every branch can produce a
+ // different limit from the top Sort.
// There is a flag indicating if this rule should be applied when
// Sort.fetch is null.
return union.all
&& sort.offset == null
- && !(sort.fetch instanceof RexDynamicParam)
+ && (sort.fetch == null
+ || RexUtil.isDeterministic(sort.fetch))
&& (config.matchNullFetch() || sort.fetch != null);
}
diff --git a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
index 3604e98dfd..b592093a5a 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
@@ -19,6 +19,7 @@
import org.apache.calcite.DataContexts;
import org.apache.calcite.linq4j.function.Predicate1;
import org.apache.calcite.plan.PlanTooComplexError;
+import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptPredicateList;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.rel.RelCollation;
@@ -48,6 +49,7 @@
import org.apache.calcite.util.ControlFlowException;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.Litmus;
+import org.apache.calcite.util.NumberUtil;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.RangeSets;
import org.apache.calcite.util.Sarg;
@@ -63,9 +65,11 @@
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.Nullable;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -840,6 +844,82 @@ public static boolean isDeterministic(RexNode e) {
}
}
+ /** Returns whether an expression contains a dynamic function. */
+ public static boolean containsDynamicFunction(RexNode e) {
+ try {
+ e.accept(
+ new RexVisitorImpl<Void>(true) {
+ @Override public Void visitCall(RexCall call) {
+ if (call.getOperator().isDynamicFunction()) {
+ throw Util.FoundOne.NULL;
+ }
+ return super.visitCall(call);
+ }
+ });
+ return false;
+ } catch (Util.FoundOne ex) {
+ Util.swallow(ex, null);
+ return true;
+ }
+ }
+
+ /** Returns whether an expression contains a dynamic parameter. */
+ public static boolean containsDynamicParam(RexNode e) {
+ try {
+ e.accept(
+ new RexVisitorImpl<Void>(true) {
+ @Override public Void visitDynamicParam(RexDynamicParam
dynamicParam) {
+ throw Util.FoundOne.NULL;
+ }
+ });
+ return false;
+ } catch (Util.FoundOne ex) {
+ Util.swallow(ex, null);
+ return true;
+ }
+ }
+
+ /** Converts a FETCH expression result to its validated canonical
representation. */
+ public static BigDecimal validateFetchValue(@Nullable Number value) {
+ if (value == null) {
+ throw new IllegalArgumentException("FETCH expression evaluated to NULL");
+ }
+ final BigDecimal decimal = NumberUtil.toBigDecimal(value);
+ if (decimal.signum() < 0) {
+ throw new IllegalArgumentException("FETCH value " + value
+ + " is out of range; expected a non-negative value");
+ }
+ return decimal;
+ }
+
+ /** Reduces a constant FETCH expression to a validated literal. */
+ public static @Nullable RexLiteral reduceFetchToLiteral(
+ RelOptCluster cluster, RexNode fetch) {
+ final RexLiteral literal;
+ if (fetch instanceof RexLiteral) {
+ literal = (RexLiteral) fetch;
+ } else {
+ if (!isConstant(fetch)
+ || !isDeterministic(fetch)
+ || containsDynamicFunction(fetch)
+ || containsDynamicParam(fetch)) {
+ return null;
+ }
+ final RexExecutor executor =
+ Util.first(cluster.getPlanner().getExecutor(), EXECUTOR);
+ final List<RexNode> reducedValues = new ArrayList<>(1);
+ executor.reduce(cluster.getRexBuilder(),
+ Collections.singletonList(fetch), reducedValues);
+ final RexNode reduced = reducedValues.get(0);
+ if (!(reduced instanceof RexLiteral)) {
+ return null;
+ }
+ literal = (RexLiteral) reduced;
+ }
+ validateFetchValue(literal.getValueAs(Number.class));
+ return literal;
+ }
+
public static List<RexNode> retainDeterministic(List<RexNode> list) {
List<RexNode> conjunctions = new ArrayList<>();
for (RexNode x : list) {
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index c5047574a3..452c2bf84a 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -164,6 +164,15 @@ ExInstWithCause<CalciteContextException>
validatorContext(int a0, int a1,
@BaseMessage("Values passed to {0} operator must have compatible types")
ExInst<SqlValidatorException> incompatibleValueType(String a0);
+ @BaseMessage("FETCH expression must have a numeric type; actual type is
''{0}''")
+ ExInst<SqlValidatorException> fetchExpressionMustBeNumeric(String type);
+
+ @BaseMessage("FETCH expression cannot reference table column ''{0}''")
+ ExInst<SqlValidatorException> fetchExpressionCannotReferenceColumn(String
column);
+
+ @BaseMessage("FETCH expression evaluated to NULL")
+ ExInst<SqlValidatorException> fetchExpressionEvaluatedToNull();
+
@BaseMessage("Values in expression list must have compatible types")
ExInst<SqlValidatorException> incompatibleTypesInList();
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
index e659d1d17e..164f212c6c 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
@@ -1088,7 +1088,18 @@ protected static void unparseFetchUsingAnsi(SqlWriter
writer, @Nullable SqlNode
writer.startList(SqlWriter.FrameTypeEnum.FETCH);
writer.keyword("FETCH");
writer.keyword("NEXT");
- fetch.unparse(writer, -1, -1);
+ if (fetch instanceof SqlLiteral
+ || fetch instanceof SqlDynamicParam) {
+ fetch.unparse(writer, -1, -1);
+ } else {
+ final SqlWriter.Frame expressionFrame = writer.startList("(", ")");
+ if (fetch instanceof SqlCall) {
+ writer.getDialect().unparseCall(writer, (SqlCall) fetch, 0, 0);
+ } else {
+ fetch.unparse(writer, 0, 0);
+ }
+ writer.endList(expressionFrame);
+ }
writer.keyword("ROWS");
writer.keyword("ONLY");
writer.endList(fetchFrame);
@@ -1098,13 +1109,32 @@ protected static void unparseFetchUsingAnsi(SqlWriter
writer, @Nullable SqlNode
/** Unparses offset/fetch using "LIMIT fetch OFFSET offset" syntax. */
protected static void unparseFetchUsingLimit(SqlWriter writer, @Nullable
SqlNode offset,
@Nullable SqlNode fetch) {
+ unparseFetchUsingLimit(writer, offset, fetch, false);
+ }
+
+ /** Unparses offset/fetch using "LIMIT fetch OFFSET offset" syntax,
+ * optionally allowing a scalar expression as fetch. */
+ protected static void unparseFetchUsingLimit(SqlWriter writer, @Nullable
SqlNode offset,
+ @Nullable SqlNode fetch, boolean allowExpression) {
checkArgument(fetch != null || offset != null);
- unparseLimit(writer, fetch);
+ unparseLimit(writer, fetch, allowExpression);
unparseOffset(writer, offset);
}
protected static void unparseLimit(SqlWriter writer, @Nullable SqlNode
fetch) {
+ unparseLimit(writer, fetch, false);
+ }
+
+ private static void unparseLimit(SqlWriter writer, @Nullable SqlNode fetch,
+ boolean allowExpression) {
if (fetch != null) {
+ if (!allowExpression
+ && !(fetch instanceof SqlLiteral)
+ && !(fetch instanceof SqlDynamicParam)) {
+ throw new IllegalArgumentException(
+ "LIMIT dialect does not support FETCH expressions that cannot "
+ + "be reduced to a literal");
+ }
writer.newlineAndIndent();
final SqlWriter.Frame fetchFrame =
writer.startList(SqlWriter.FrameTypeEnum.FETCH);
diff --git
a/core/src/main/java/org/apache/calcite/sql/dialect/SqliteSqlDialect.java
b/core/src/main/java/org/apache/calcite/sql/dialect/SqliteSqlDialect.java
index 82376ae576..f312764136 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/SqliteSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/SqliteSqlDialect.java
@@ -90,7 +90,7 @@ public SqliteSqlDialect(SqlDialect.Context context) {
@Override public void unparseOffsetFetch(SqlWriter writer, @Nullable SqlNode
offset,
@Nullable SqlNode fetch) {
- unparseFetchUsingLimit(writer, offset, fetch);
+ unparseFetchUsingLimit(writer, offset, fetch, true);
}
@Override public void unparseCall(SqlWriter writer, SqlCall call,
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 7021696b67..0ea01a3514 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -1771,6 +1771,34 @@ private void handleOffsetFetch(@Nullable SqlNode offset,
@Nullable SqlNode fetch
}
}
+ private void validateFetchExpression(@Nullable SqlNode fetch) {
+ if (fetch == null || fetch instanceof SqlDynamicParam) {
+ return;
+ }
+ if (SqlUtil.isNullLiteral(fetch, true)) {
+ throw newValidationError(fetch,
+ RESOURCE.fetchExpressionEvaluatedToNull());
+ }
+ validateNoAggs(aggOrOverFinder, fetch, "FETCH");
+ fetch.accept(new SqlBasicVisitor<Void>() {
+ @Override public Void visit(SqlIdentifier id) {
+ if (makeNullaryCall(id) != null) {
+ return null;
+ }
+ throw newValidationError(id,
+ RESOURCE.fetchExpressionCannotReferenceColumn(id.toString()));
+ }
+ });
+ final SqlValidatorScope scope = getEmptyScope();
+ inferUnknownTypes(typeFactory.createSqlType(SqlTypeName.DECIMAL), scope,
fetch);
+ validateExpr(fetch, scope);
+ final RelDataType type = getValidatedNodeType(fetch);
+ if (!SqlTypeUtil.isNumeric(type)) {
+ throw newValidationError(fetch,
+ RESOURCE.fetchExpressionMustBeNumeric(type.getFullTypeString()));
+ }
+ }
+
/**
* Performs expression rewrites which are always used unconditionally. These
* rewrites massage the expression tree into a standard form so that the
@@ -4499,6 +4527,7 @@ protected void validateSelect(
validateWindowClause(select);
validateQualifyClause(select);
handleOffsetFetch(select.getOffset(), select.getFetch());
+ validateFetchExpression(select.getFetch());
// Validate the SELECT clause late, because a select item might
// depend on the GROUP BY list, or the window function might reference
diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
index 300ff959bc..4e4104ad48 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/RelDecorrelator.java
@@ -575,6 +575,10 @@ protected RexNode removeCorrelationExpr(
// Its output does not change the input ordering, so there's no
// need to call propagateExpr.
+ if (isCorVarDefined && !canDecorrelateOffsetFetch(rel)) {
+ return null;
+ }
+
final RelNode oldInput = rel.getInput();
final Frame frame = getInvoke(oldInput, isCorVarDefined, rel, true);
if (frame == null) {
@@ -1137,8 +1141,31 @@ private static void shiftMapping(Map<Integer, Integer>
mapping, int startIndex,
return register(sort, result, mapOldToNewOutputs, corDefOutputs);
}
+ static boolean canDecorrelateOffsetFetch(Sort sort) {
+ final @Nullable RexLiteral fetch = sort.fetch == null
+ ? null
+ : RexUtil.reduceFetchToLiteral(sort.getCluster(), sort.fetch);
+ return isNonNegativeIntegralLiteral(sort.offset)
+ && (sort.fetch == null
+ || fetch != null && isNonNegativeIntegralLiteral(fetch));
+ }
+
+ private static boolean isNonNegativeIntegralLiteral(@Nullable RexNode node) {
+ if (node == null) {
+ return true;
+ }
+ if (!(node instanceof RexLiteral)) {
+ return false;
+ }
+ final @Nullable BigDecimal value =
+ ((RexLiteral) node).getValueAs(BigDecimal.class);
+ return value != null
+ && value.signum() >= 0
+ && value.stripTrailingZeros().scale() <= 0;
+ }
+
protected @Nullable Frame decorrelateSortAsAggregate(Sort sort, final Frame
frame) {
- if (sort.offset != null || sort.fetch == null) {
+ if (sort.offset != null || !(sort.fetch instanceof RexLiteral)) {
return null;
}
diff --git
a/core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java
b/core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java
index 291eb619d0..4139cf4b2b 100644
---
a/core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java
+++
b/core/src/main/java/org/apache/calcite/sql2rel/TopDownGeneralDecorrelator.java
@@ -233,12 +233,14 @@ public static RelNode decorrelateQuery(RelNode rel,
RelBuilder builder) {
RelNode preparedRel = prePlanner.findBestExp();
// start decorrelating
- TopDownGeneralDecorrelator decorrelator = createEmptyDecorrelator(builder);
RelNode decorrelateNode = rel;
- try {
- decorrelateNode = decorrelator.correlateElimination(preparedRel, true);
- } catch (UnsupportedOperationException e) {
- // if the correlation exists in an unsupported operator, retain the
original plan.
+ if (canDecorrelateOffsetFetch(preparedRel, false)) {
+ TopDownGeneralDecorrelator decorrelator =
createEmptyDecorrelator(builder);
+ try {
+ decorrelateNode = decorrelator.correlateElimination(preparedRel, true);
+ } catch (UnsupportedOperationException e) {
+ // if the correlation exists in an unsupported operator, retain the
original plan.
+ }
}
HepProgram postProgram = HepProgram.builder()
@@ -255,6 +257,29 @@ public static RelNode decorrelateQuery(RelNode rel,
RelBuilder builder) {
return postPlanner.findBestExp();
}
+ /** Returns whether correlated Sorts in a tree have OFFSET and FETCH values
+ * that can be decorrelated without changing their row-count semantics. */
+ private static boolean canDecorrelateOffsetFetch(RelNode rel,
+ boolean isCorVarDefined) {
+ if (isCorVarDefined && rel instanceof Sort
+ && !RelDecorrelator.canDecorrelateOffsetFetch((Sort) rel)) {
+ return false;
+ }
+ if (rel instanceof Correlate) {
+ final Correlate correlate = (Correlate) rel;
+ if (!canDecorrelateOffsetFetch(correlate.getLeft(), isCorVarDefined)) {
+ return false;
+ }
+ return canDecorrelateOffsetFetch(correlate.getRight(), true);
+ }
+ for (RelNode input : rel.getInputs()) {
+ if (!canDecorrelateOffsetFetch(input, isCorVarDefined)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Eliminates Correlate.
*
diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index 36d56a9f04..2309102ff8 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -86,6 +86,7 @@
import org.apache.calcite.rex.RexSubQuery;
import org.apache.calcite.rex.RexUnknownAs;
import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.rex.RexVisitorImpl;
import org.apache.calcite.rex.RexWindowBound;
import org.apache.calcite.rex.RexWindowBounds;
import org.apache.calcite.rex.RexWindowExclusion;
@@ -108,6 +109,7 @@
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.SqlReturnTypeInference;
import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.sql.type.TableFunctionReturnTypeInference;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.sql2rel.SqlToRelConverter;
@@ -3801,8 +3803,7 @@ public RelBuilder sortLimit(Number offset, Number fetch,
*
* @param offsetNode RexLiteral means number of rows to skip is
deterministic,
* RexDynamicParam means number of rows to skip is dynamic.
- * @param fetchNode RexLiteral means maximum number of rows to fetch is
deterministic,
- * RexDynamicParam mean maximum number is dynamic.
+ * @param fetchNode Maximum number of rows to fetch
* @param nodes Sort expressions
*/
public RelBuilder sortLimit(@Nullable RexNode offsetNode, @Nullable RexNode
fetchNode,
@@ -3812,12 +3813,17 @@ public RelBuilder sortLimit(@Nullable RexNode
offsetNode, @Nullable RexNode fetc
throw new IllegalArgumentException("OFFSET node must be RexLiteral or
RexDynamicParam");
}
}
- if (fetchNode != null) {
- if (!(fetchNode instanceof RexLiteral || fetchNode instanceof
RexDynamicParam)) {
- throw new IllegalArgumentException("FETCH node must be RexLiteral or
RexDynamicParam");
- }
+ if (fetchNode != null && !isValidFetchExpression(fetchNode)) {
+ throw new IllegalArgumentException(
+ "FETCH node must not reference input fields or contain aggregate
functions, "
+ + "window functions, or subqueries");
+ }
+ if (fetchNode != null
+ && !SqlTypeUtil.isNumeric(fetchNode.getType())) {
+ throw new IllegalArgumentException(
+ "FETCH node must have a numeric type; actual type is "
+ + fetchNode.getType().getFullTypeString());
}
-
final Registrar registrar = new Registrar(fields(), ImmutableList.of());
final List<RelFieldCollation> fieldCollations =
registrar.registerFieldCollations(nodes);
@@ -3884,6 +3890,38 @@ public RelBuilder sortLimit(@Nullable RexNode
offsetNode, @Nullable RexNode fetc
return this;
}
+ private static boolean isValidFetchExpression(RexNode node) {
+ return Boolean.TRUE.equals(node.accept(new FetchExpressionVisitor()));
+ }
+
+ /** Visitor that validates FETCH expressions. */
+ private static class FetchExpressionVisitor
+ extends RexVisitorImpl<@Nullable Boolean> {
+ FetchExpressionVisitor() {
+ super(false);
+ }
+
+ @Override public Boolean visitLiteral(RexLiteral literal) {
+ return true;
+ }
+
+ @Override public Boolean visitDynamicParam(RexDynamicParam dynamicParam) {
+ return true;
+ }
+
+ @Override public Boolean visitCall(RexCall call) {
+ if (call.getOperator().isAggregator()) {
+ return false;
+ }
+ for (RexNode operand : call.getOperands()) {
+ if (!Boolean.TRUE.equals(operand.accept(this))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
private static RelFieldCollation collation(RexNode node,
RelFieldCollation.Direction direction,
RelFieldCollation.@Nullable NullDirection nullDirection,
diff --git
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index a90099d7cb..703536de88 100644
---
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -61,6 +61,9 @@ ValidatorContext=From line {0,number,#}, column {1,number,#}
to line {2,number,#
CannotCastValue=Cast function cannot convert value of type {0} to type {1}
UnknownDatatypeName=Unknown datatype name ''{0}''
IncompatibleValueType=Values passed to {0} operator must have compatible types
+FetchExpressionMustBeNumeric=FETCH expression must have a numeric type; actual
type is ''{0}''
+FetchExpressionCannotReferenceColumn=FETCH expression cannot reference table
column ''{0}''
+FetchExpressionEvaluatedToNull=FETCH expression evaluated to NULL
IncompatibleTypesInList=Values in expression list must have compatible types
IncompatibleCharset=Cannot apply operation ''{0}'' to strings with different
charsets ''{1}'' and ''{2}''
InvalidOrderByPos=ORDER BY is only allowed on top-level SELECT
diff --git
a/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java
b/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java
index 40b825939d..70370d7bb1 100644
---
a/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java
+++
b/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java
@@ -34,6 +34,7 @@
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Tests for {@link EnumUtils}.
@@ -186,6 +187,16 @@ public final class EnumUtilsTest {
is(BigDecimal.valueOf(2)));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testNumberToBigDecimalRejectsNull() {
+ final IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class,
+ () -> EnumUtils.numberToBigDecimal(null, "FETCH"));
+ assertThat(e.getMessage(), is("FETCH expression evaluated to NULL"));
+ }
+
@Test void testMethodCallExpression() {
// test for Object.class method parameter type
final ConstantExpression arg0 = Expressions.constant(1, int.class);
diff --git
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 772c409692..01f1578936 100644
---
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -4950,6 +4950,74 @@ private SqlDialect nonOrdinalDialect() {
.withSybase().ok(expectedSybase);
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionWithLimitDialect() {
+ final String query = "select \"product_id\"\n"
+ + "from \"product\"\n"
+ + "fetch next (1 + 2) rows only";
+ final String expected = "SELECT `product_id`\n"
+ + "FROM `foodmart`.`product`\n"
+ + "LIMIT 3";
+ sql(query).withMysql().ok(expected);
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testNegativeFetchExpressionIsRejectedBeforeSqlGeneration() {
+ final String query = "select \"product_id\"\n"
+ + "from \"product\"\n"
+ + "fetch next (0 - 1) rows only";
+ final String error =
+ "FETCH value -1 is out of range; expected a non-negative value";
+ sql(query).throws_(error);
+ sql(query).withMysql().throws_(error);
+ sql(query).withSQLite().throws_(error);
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testParameterizedFetchExpressionWithLimitDialect() {
+ final String query = "select \"product_id\"\n"
+ + "from \"product\"\n"
+ + "fetch next (? + 1) rows only";
+ sql(query).withMysql().throws_(
+ "LIMIT dialect does not support FETCH expressions that cannot "
+ + "be reduced to a literal");
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testParameterizedFetchExpressionWithSQLite() {
+ final String query = "select \"product_id\"\n"
+ + "from \"product\"\n"
+ + "fetch next (? + 1) rows only";
+ final String expected = "SELECT \"product_id\"\n"
+ + "FROM \"foodmart\".\"product\"\n"
+ + "LIMIT ? + 1";
+ sql(query).withSQLite().ok(expected);
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testDynamicFetchExpressionIsNotReduced() {
+ final String query = "select \"product_id\"\n"
+ + "from \"product\"\n"
+ + "fetch next (extract(day from current_date)) rows only";
+ final String expected = "SELECT \"product_id\"\n"
+ + "FROM \"foodmart\".\"product\"\n"
+ + "FETCH NEXT (EXTRACT(DAY FROM CURRENT_DATE)) ROWS ONLY";
+ sql(query).ok(expected);
+ sql(query).withMysql().throws_(
+ "LIMIT dialect does not support FETCH expressions that cannot "
+ + "be reduced to a literal");
+ }
+
@Test void testSelectQueryComplex() {
String query =
"select count(*), \"units_per_case\" from \"product\" where
\"cases_per_pallet\" > 100 "
diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
index b22218b955..5f8b8edfb1 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -83,6 +83,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static java.util.Objects.requireNonNull;
@@ -3631,6 +3632,35 @@ private void assertTypeAndToString(
hasSize(0));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testContainsDynamicParam() {
+ final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
+ final RexNode literal = rexBuilder.makeExactLiteral(BigDecimal.ONE,
intType);
+ final RexNode dynamicParam = rexBuilder.makeDynamicParam(intType, 0);
+ final RexNode expression =
+ rexBuilder.makeCall(SqlStdOperatorTable.PLUS, literal, dynamicParam);
+
+ assertThat(RexUtil.containsDynamicParam(literal), is(false));
+ assertThat(RexUtil.containsDynamicParam(dynamicParam), is(true));
+ assertThat(RexUtil.containsDynamicParam(expression), is(true));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testValidateFetchValueAllowsFractionalBigDecimal() {
+ assertThat(RexUtil.validateFetchValue(new BigDecimal("1.5")),
+ is(new BigDecimal("1.5")));
+
+ final IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class,
+ () -> RexUtil.validateFetchValue(new BigDecimal("-1.5")));
+ assertThat(e.getMessage(),
+ containsString("FETCH value -1.5 is out of range"));
+ }
+
@Test void testConstantMap() {
final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
final RelDataType bigintType =
typeFactory.createSqlType(SqlTypeName.BIGINT);
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 83ce5b1d78..f213b12dcb 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -3581,6 +3581,181 @@ public void checkOrderBy(final boolean desc,
+ "store_id=4; grocery_sqft=16844\n");
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpression() {
+ CalciteAssert.that()
+ .query("select * from (values (1), (2), (3), (4)) as t(x)\n"
+ + "fetch next (1 + abs(-2)) rows only")
+ .returns("X=1\n"
+ + "X=2\n"
+ + "X=3\n");
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testBindableFetchExpression() {
+ try (Hook.Closeable ignored =
Hook.ENABLE_BINDABLE.addThread(Hook.propertyJ(true))) {
+ final CalciteAssert.AssertThat with = CalciteAssert.that();
+ with
+ .query("select * from (values (1), (2), (3), (4)) as t(x)\n"
+ + "fetch next (rand_integer(1) + 2) rows only")
+ .explainContains("BindableSort(fetch=[+(RAND_INTEGER(1), 2)])")
+ .returns("X=1\n"
+ + "X=2\n");
+ with.query("select * from (values (1), (2), (3), (4)) as t(x)\n"
+ + "fetch next (cast(9223372036854775808 as decimal(20, 0))) rows
only")
+ .returns("X=1\nX=2\nX=3\nX=4\n");
+ with.query("select * from (values (1), (2), (3), (4)) as t(x)\n"
+ + "order by x fetch next ? rows only")
+ .explainContains("BindableSort(sort0=[$0], dir0=[ASC], fetch=[?0])")
+ .consumesPreparedStatement(p ->
+ p.setBigDecimal(1, new BigDecimal("1.5")))
+ .returns("X=1\n"
+ + "X=2\n");
+ }
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionFunctionArguments() {
+ final CalciteAssert.AssertThat with = CalciteAssert.that();
+ final String values = "select * from (values (1), (2), (3)) as t(x)\n";
+ with.query(values + "fetch next (abs(2)) rows only")
+ .returns("X=1\n"
+ + "X=2\n");
+ with.query(values + "fetch next (abs(-2)) rows only")
+ .returns("X=1\n"
+ + "X=2\n");
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionInvalidValue() {
+ final CalciteAssert.AssertThat with = CalciteAssert.that();
+ final String values = "select * from (values (1), (2), (3)) as t(x)\n";
+ with.query(values + "fetch next (0 - 1) rows only")
+ .throws_("FETCH must not be negative");
+ with.query(values + "fetch next (-1) rows only")
+ .throws_("FETCH must not be negative");
+ with.query(values
+ + "fetch next (cast(null as integer)) rows only")
+ .throws_("FETCH expression evaluated to NULL");
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testCorrelatedFetchExpressionInvalidValue() {
+ final String sqlPrefix = "select d.\"name\", e.\"name\"\n"
+ + "from \"hr\".\"depts\" d,\n"
+ + "lateral (select \"name\" from \"hr\".\"emps\"\n"
+ + " where \"deptno\" = d.\"deptno\"\n";
+ for (String fetch : new String[] {"(0 - 1)", "(-1)"}) {
+ for (boolean topDown : new boolean[] {false, true}) {
+ CalciteAssert.hr()
+
.with(CalciteConnectionProperty.TOPDOWN_GENERAL_DECORRELATION_ENABLED, topDown)
+ .query(sqlPrefix + " fetch next " + fetch + " rows only) e")
+ .throws_("FETCH value -1 is out of range");
+ }
+ }
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testCorrelatedFractionalOffsetFetch() {
+ final String sqlPrefix = "select d.\"name\" as dname, e.\"name\" as
ename\n"
+ + "from \"hr\".\"depts\" d,\n"
+ + "lateral (select \"empid\", \"name\" from \"hr\".\"emps\"\n"
+ + " where \"deptno\" = d.\"deptno\"\n"
+ + " order by \"empid\" ";
+ final String sqlSuffix = ") e\norder by e.\"empid\"";
+ for (boolean topDown : new boolean[] {false, true}) {
+ final CalciteAssert.AssertThat with = CalciteAssert.hr()
+
.with(CalciteConnectionProperty.TOPDOWN_GENERAL_DECORRELATION_ENABLED, topDown);
+ with.query(sqlPrefix + "fetch next (0.5 + 1) rows only" + sqlSuffix)
+ .returns("DNAME=Sales; ENAME=Bill\n"
+ + "DNAME=Sales; ENAME=Theodore\n");
+ with.query(sqlPrefix + "offset 1.5 rows fetch next 1 row only" +
sqlSuffix)
+ .returns("DNAME=Sales; ENAME=Sebastian\n");
+ }
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testCorrelatedPreparedFractionalOffset() throws Exception {
+ final String sql = "select d.\"name\" as dname, e.\"name\" as ename\n"
+ + "from \"hr\".\"depts\" d,\n"
+ + "lateral (select \"empid\", \"name\" from \"hr\".\"emps\"\n"
+ + " where \"deptno\" = d.\"deptno\"\n"
+ + " order by \"empid\" offset ? rows fetch next 1 row only) e\n"
+ + "order by e.\"empid\"";
+ for (boolean topDown : new boolean[] {false, true}) {
+ CalciteAssert.hr()
+
.with(CalciteConnectionProperty.TOPDOWN_GENERAL_DECORRELATION_ENABLED, topDown)
+ .doWithConnection(connection -> {
+ checkPreparedBigDecimalParameter(connection, sql,
+ new BigDecimal("1.5"),
+ "DNAME=Sales; ENAME=Sebastian\n");
+ });
+ }
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testCorrelatedPreparedFetchExpression() throws Exception {
+ for (String fetch : new String[] {"?", "(? + 0)"}) {
+ final String sql = "select d.\"name\" as dname, e.\"name\" as ename\n"
+ + "from \"hr\".\"depts\" d,\n"
+ + "lateral (select \"empid\", \"name\" from \"hr\".\"emps\"\n"
+ + " where \"deptno\" = d.\"deptno\"\n"
+ + " order by \"empid\" fetch next " + fetch + " rows only) e\n"
+ + "order by e.\"empid\"";
+ for (boolean topDown : new boolean[] {false, true}) {
+ CalciteAssert.hr()
+
.with(CalciteConnectionProperty.TOPDOWN_GENERAL_DECORRELATION_ENABLED, topDown)
+ .doWithConnection(connection -> {
+ checkPreparedFetchRepeated(connection, sql,
+ new int[] {1, 3},
+ new String[] {
+ "DNAME=Sales; ENAME=Bill\n",
+ "DNAME=Sales; ENAME=Bill\n"
+ + "DNAME=Sales; ENAME=Theodore\n"
+ + "DNAME=Sales; ENAME=Sebastian\n"
+ });
+ checkPreparedParameterFails(connection, sql, -1,
+ "FETCH must not be negative");
+ checkPreparedParameterNullFails(connection, sql,
+ "FETCH expression evaluated to NULL");
+ });
+ }
+ }
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionBeyondLong() {
+ final CalciteAssert.AssertThat with = CalciteAssert.that();
+ final String values = "select * from (values (1), (2), (3), (4)) as
t(x)\n";
+ final String expected = "X=1\nX=2\nX=3\nX=4\n";
+ with.query(values + "fetch next 9223372036854775808 rows only")
+ .returns(expected);
+ with.query(values + "fetch next "
+ + "(cast(9223372036854775808 as decimal(20, 0)) + 1) rows only")
+ .returns(expected);
+ with.query(values + "order by x fetch next "
+ + "(cast(9223372036854775808 as decimal(20, 0)) + 1) rows only")
+ .returns(expected);
+ }
+
/** Tests ORDER BY ... OFFSET ... FETCH. */
@Test void testOrderByOffsetFetch() {
CalciteAssert.that()
@@ -6058,6 +6233,169 @@ private CalciteAssert.AssertQuery withEmpDept(String
sql) {
"name=Theodore");
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testPreparedFetchExpression() throws Exception {
+ CalciteAssert.that()
+ .doWithConnection(connection -> {
+ final String values =
+ "select * from (values (1), (2), (3), (4)) as t(x)\n";
+ checkPreparedFetch(connection, values + "fetch next (?) rows only",
+ 2, "X=1\nX=2\n");
+ checkPreparedFetch(connection, values + "fetch next (? + 1) rows
only",
+ 2, "X=1\nX=2\nX=3\n");
+ checkPreparedFetch(connection,
+ values + "fetch next (abs(cast(? as integer))) rows only",
+ 2, "X=1\nX=2\n");
+ checkPreparedFetch(connection,
+ values + "fetch next (abs(cast(? as integer))) rows only",
+ -2, "X=1\nX=2\n");
+ checkPreparedFetchRepeated(connection,
+ values + "fetch next (?) rows only",
+ new int[] {1, 3},
+ new String[] {"X=1\n", "X=1\nX=2\nX=3\n"});
+ checkPreparedFetchRepeated(connection,
+ values + "fetch next (? + 1) rows only",
+ new int[] {0, 2, 3},
+ new String[] {"X=1\n", "X=1\nX=2\nX=3\n",
+ "X=1\nX=2\nX=3\nX=4\n"});
+ checkPreparedFetch(connection,
+ values + "fetch next (? + abs(2)) rows only",
+ 1, "X=1\nX=2\nX=3\n");
+ checkPreparedBigDecimalParameter(connection,
+ values + "fetch next (cast(? as decimal(20, 0))) rows only",
+ new BigDecimal("9223372036854775808"),
+ "X=1\nX=2\nX=3\nX=4\n");
+
+ checkPreparedParameterFails(connection,
+ values + "fetch next (?) rows only", -1,
+ "FETCH must not be negative");
+ checkPreparedParameterFails(connection,
+ values + "fetch next (? + 1) rows only", -2,
+ "FETCH must not be negative");
+ });
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testBindablePreparedFetchExpression() throws Exception {
+ try (Hook.Closeable ignored =
Hook.ENABLE_BINDABLE.addThread(Hook.propertyJ(true))) {
+ CalciteAssert.that()
+ .doWithConnection(connection -> {
+ final String values =
+ "select * from (values (1), (2), (3), (4)) as t(x)\n";
+ checkPreparedFetch(connection,
+ values + "fetch next (? + 1) rows only",
+ 2, "X=1\nX=2\nX=3\n");
+ checkPreparedFetchRepeated(connection,
+ values + "fetch next (? + 1) rows only",
+ new int[] {0, 2, 3},
+ new String[] {"X=1\n", "X=1\nX=2\nX=3\n",
+ "X=1\nX=2\nX=3\nX=4\n"});
+ checkPreparedBigDecimalParameter(connection,
+ values + "fetch next (cast(? as decimal(20, 0))) rows only",
+ new BigDecimal("9223372036854775808"),
+ "X=1\nX=2\nX=3\nX=4\n");
+ });
+ }
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testBindablePreparedOffset() throws Exception {
+ try (Hook.Closeable ignored =
Hook.ENABLE_BINDABLE.addThread(Hook.propertyJ(true))) {
+ CalciteAssert.that()
+ .doWithConnection(connection -> {
+ final String values =
+ "select * from (values (1), (2), (3), (4)) as t(x)\n";
+ final String offset = values + "offset ? rows";
+ checkPreparedBigDecimalParameter(connection, offset,
+ new BigDecimal("1.5"),
+ "X=3\nX=4\n");
+ checkPreparedBigDecimalParameter(connection, offset,
+ BigDecimal.valueOf(Integer.MAX_VALUE).add(BigDecimal.ONE), "");
+
+ final String sortedOffset = values + "order by x desc offset ?
rows";
+ checkPreparedBigDecimalParameter(connection, sortedOffset,
+ new BigDecimal("1.5"),
+ "X=2\nX=1\n");
+ checkPreparedParameterFails(connection, offset, -1,
+ "OFFSET must not be negative");
+ checkPreparedParameterNullFails(connection, offset,
+ "OFFSET expression evaluated to NULL");
+ });
+ }
+ }
+
+ private static void checkPreparedFetch(Connection connection, String sql,
+ int value, String expected) {
+ try (PreparedStatement p = connection.prepareStatement(sql)) {
+ p.setInt(1, value);
+ try (ResultSet r = p.executeQuery()) {
+ assertThat(CalciteAssert.toString(r), is(expected));
+ }
+ } catch (SQLException e) {
+ throw TestUtil.rethrow(e);
+ }
+ }
+
+ private static void checkPreparedBigDecimalParameter(Connection connection,
String sql,
+ BigDecimal value, String expected) {
+ try (PreparedStatement p = connection.prepareStatement(sql)) {
+ p.setBigDecimal(1, value);
+ try (ResultSet r = p.executeQuery()) {
+ assertThat(CalciteAssert.toString(r), is(expected));
+ }
+ } catch (SQLException e) {
+ throw TestUtil.rethrow(e);
+ }
+ }
+
+ private static void checkPreparedFetchRepeated(Connection connection, String
sql,
+ int[] values, String[] expected) {
+ try (PreparedStatement p = connection.prepareStatement(sql)) {
+ for (int i = 0; i < values.length; i++) {
+ p.setInt(1, values[i]);
+ try (ResultSet r = p.executeQuery()) {
+ assertThat(CalciteAssert.toString(r), is(expected[i]));
+ }
+ }
+ } catch (SQLException e) {
+ throw TestUtil.rethrow(e);
+ }
+ }
+
+ private static void checkPreparedParameterFails(Connection connection,
String sql,
+ long value, String expectedMessage) {
+ try (PreparedStatement p = connection.prepareStatement(sql)) {
+ if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
+ p.setInt(1, (int) value);
+ } else {
+ p.setLong(1, value);
+ }
+ final SQLException e =
+ assertThrows(SQLException.class, p::executeQuery);
+ assertThat(e.getMessage(), containsString(expectedMessage));
+ } catch (SQLException e) {
+ throw TestUtil.rethrow(e);
+ }
+ }
+
+ private static void checkPreparedParameterNullFails(Connection connection,
String sql,
+ String expectedMessage) {
+ try (PreparedStatement p = connection.prepareStatement(sql)) {
+ p.setNull(1, Types.INTEGER);
+ final SQLException e =
+ assertThrows(SQLException.class, p::executeQuery);
+ assertThat(e.getMessage(), containsString(expectedMessage));
+ } catch (SQLException e) {
+ throw TestUtil.rethrow(e);
+ }
+ }
+
private void checkPreparedOffsetFetch(final int offset, final int fetch,
final Matcher<? super ResultSet> matcher) throws Exception {
CalciteAssert.hr()
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index 493df7c30b..7ad9a4d733 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -52,7 +52,10 @@
import org.apache.calcite.rex.RexCorrelVariable;
import org.apache.calcite.rex.RexFieldCollation;
import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLambdaRef;
import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexNodeAndFieldIndex;
+import org.apache.calcite.rex.RexSubQuery;
import org.apache.calcite.rex.RexWindowBounds;
import org.apache.calcite.runtime.CalciteException;
import org.apache.calcite.schema.SchemaPlus;
@@ -5647,6 +5650,108 @@ private static RelNode
buildCorrelateWithJoin(JoinRelType type, RelBuilder build
assertThat(mq.getMaxRowCount(planAfter), is(Double.POSITIVE_INFINITY));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionCannotReferenceInputField() {
+ final RelBuilder builder = RelBuilder.create(config().build());
+ builder.scan("DEPT");
+ final RexNode field = builder.field("DEPTNO");
+
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null, field, ImmutableList.of()));
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null,
+ builder.call(SqlStdOperatorTable.PLUS, builder.literal(1), field),
+ ImmutableList.of()));
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null,
+ new RexNodeAndFieldIndex(0, 0, "DEPTNO", field.getType()),
+ ImmutableList.of()));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionMustHaveNumericType() {
+ final RelBuilder builder = RelBuilder.create(config().build());
+ builder.scan("DEPT");
+
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null, builder.literal("x"),
ImmutableList.of()));
+ builder.sortLimit(null, builder.literal(new BigDecimal("1.5")),
+ ImmutableList.of());
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionAllowsScalarCallAndDynamicParameter() {
+ final RelBuilder builder = RelBuilder.create(config().build());
+ final RelDataType intType =
+ builder.getTypeFactory().createSqlType(SqlTypeName.INTEGER);
+ builder.scan("DEPT")
+ .sortLimit(null,
+ builder.call(SqlStdOperatorTable.PLUS,
+ builder.getRexBuilder().makeDynamicParam(intType, 0),
+ builder.literal(1)),
+ ImmutableList.of());
+
+ assertThat(
+ builder.build(), hasTree("LogicalSort(fetch=[+(?0, 1)])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n"));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionCannotContainAggregateWindowOrSubQuery() {
+ final RelBuilder builder = RelBuilder.create(config().build());
+ final RelDataType intType =
+ builder.getTypeFactory().createSqlType(SqlTypeName.INTEGER);
+ builder.scan("DEPT");
+ final RexNode aggregate =
+ builder.call(SqlStdOperatorTable.SUM, builder.literal(1));
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null, aggregate, ImmutableList.of()));
+
+ final RexNode over =
+ builder.getRexBuilder().makeOver(intType,
+ SqlStdOperatorTable.ROW_NUMBER, ImmutableList.of(),
+ ImmutableList.of(), ImmutableList.of(),
+ RexWindowBounds.UNBOUNDED_PRECEDING,
+ RexWindowBounds.UNBOUNDED_FOLLOWING,
+ true, true, false, false, false);
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null, over, ImmutableList.of()));
+
+ final RelBuilder subQueryBuilder = RelBuilder.create(config().build());
+ final RexNode subQuery =
+ RexSubQuery.scalar(subQueryBuilder.values(new String[] {"N"},
1).build());
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null, subQuery, ImmutableList.of()));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionCannotContainLambda() {
+ final RelBuilder builder = RelBuilder.create(config().build());
+ final RelDataType intType =
+ builder.getTypeFactory().createSqlType(SqlTypeName.INTEGER);
+ builder.scan("DEPT");
+ final RexLambdaRef lambdaRef = new RexLambdaRef(0, "x", intType);
+ final RexNode lambda =
+ builder.getRexBuilder().makeLambdaCall(
+ builder.call(SqlStdOperatorTable.PLUS, lambdaRef,
builder.literal(1)),
+ ImmutableList.of(lambdaRef));
+
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null, lambda, ImmutableList.of()));
+ assertThrows(IllegalArgumentException.class,
+ () -> builder.sortLimit(null, lambdaRef, ImmutableList.of()));
+ }
+
@Test void testAdoptConventionEnumerable() {
final RelBuilder builder = RelBuilder.create(config().build());
RelNode root = builder
diff --git a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
index 9c3c30e344..460e066051 100644
--- a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
@@ -1467,7 +1467,7 @@ void testColumnOriginsUnion() {
@Test void testRowCountSortLimitBeyondLong() {
final BigDecimal fetch =
BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE);
final double fetchDouble = fetch.doubleValue();
- final String sql = "select * from emp order by ename limit " + fetchDouble;
+ final String sql = "select * from emp order by ename limit " +
fetch.toPlainString();
final RelMetadataFixture fixture = sql(sql);
fixture.assertThatRowCount(is(EMP_SIZE), is(0D), is(fetchDouble));
}
@@ -1496,6 +1496,37 @@ void testColumnOriginsUnion() {
fixture.assertThatRowCount(is(1d), is(0D), is(0d));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testMinRowCountFetchExpression() {
+ final String sql = "select * from (values (1), (2)) as t(x)\n"
+ + "fetch next (2 - 2) rows only";
+ final RelMetadataFixture fixture = sql(sql);
+ fixture.assertThatRowCount(is(2D), is(0D), is(2D));
+
+ fixture
+ .withCluster(cluster -> {
+ final RelOptPlanner planner = new VolcanoPlanner();
+ planner.addRule(EnumerableRules.ENUMERABLE_VALUES_RULE);
+ planner.addRule(EnumerableRules.ENUMERABLE_PROJECT_RULE);
+ planner.addRule(EnumerableRules.ENUMERABLE_LIMIT_RULE);
+ planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+ return RelOptCluster.create(planner, cluster.getRexBuilder());
+ })
+ .withRelTransform(rel -> {
+ final RelOptPlanner planner = rel.getCluster().getPlanner();
+ planner.setRoot(rel);
+ final RelTraitSet requiredOutputTraits =
+
rel.getCluster().traitSet().replace(EnumerableConvention.INSTANCE);
+ final RelNode root = planner.changeTraits(rel, requiredOutputTraits);
+ planner.setRoot(root);
+ return planner.findBestExp();
+ })
+ .assertThatRel(is(instanceOf(EnumerableLimit.class)))
+ .assertThatRowCount(is(2D), is(0D), is(2D));
+ }
+
@Test void testRowCountSortLimitOffset() {
final String sql = "select * from emp order by ename limit 10 offset 5";
/* 14 - 5 */
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 4ba2f4aa96..5c687519da 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -1738,6 +1738,34 @@ private void
checkJoinProjectTransposeDoesNotMatch(JoinRelType type) {
.check();
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testSortUnionTransposeWithNonDeterministicFetch() {
+ final String sql = "select a.name from dept a\n"
+ + "union all\n"
+ + "select b.name from dept b\n"
+ + "order by name fetch next (rand_integer(10)) rows only";
+ sql(sql)
+ .withPreRule(CoreRules.PROJECT_SET_OP_TRANSPOSE)
+ .withRule(CoreRules.SORT_UNION_TRANSPOSE)
+ .checkUnchanged();
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testSortUnionTransposePushesParameterizedFetchExpression() {
+ final String sql = "select a.name from dept a\n"
+ + "union all\n"
+ + "select b.name from dept b\n"
+ + "order by name fetch next (? + 1) rows only";
+ sql(sql)
+ .withPreRule(CoreRules.PROJECT_SET_OP_TRANSPOSE)
+ .withRule(CoreRules.SORT_UNION_TRANSPOSE)
+ .check();
+ }
+
@Test void testSortRemovalAllKeysConstant() {
final String sql = "select count(*) as c\n"
+ "from sales.emp\n"
@@ -5997,10 +6025,9 @@ private void checkEmptyJoin(RelOptFixture f) {
}
/** Test case for
- * <a
href="https://issues.apache.org/jira/browse/CALCITE-6647">[CALCITE-6647]
- * SortUnionTransposeRule should not push SORT past a UNION when SORT's
fetch is DynamicParam
- </a>. */
- @Test void testSortWithDynamicParam() {
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testSortWithDynamicParamPushesOnce() {
HepProgramBuilder builder = new HepProgramBuilder();
builder.addRuleClass(SortProjectTransposeRule.class);
builder.addRuleClass(SortUnionTransposeRule.class);
@@ -9730,6 +9757,19 @@ private void checkSemiJoinRuleOnAntiJoin(RelOptRule
rule) {
.check();
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testDecorrelateProjectWithFetchExpression() {
+ final String query = "SELECT name, "
+ + "(SELECT sal FROM emp where dept.deptno = emp.deptno order by sal "
+ + "fetch next (1 + 0) rows only) "
+ + "FROM dept";
+ sql(query).withRule(CoreRules.PROJECT_SUB_QUERY_TO_CORRELATE)
+ .withLateDecorrelate(true)
+ .check();
+ }
+
/** Test case for <a
href="https://issues.apache.org/jira/browse/CALCITE-7289">[CALCITE-7289]
* Select NULL subquery throwing exception</a>. */
@Test void testNullSelect() {
@@ -12218,6 +12258,39 @@ private static RelNode
applyAggregateRemoveLiteralAggRule(RelNode rel) {
.check();
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testNondeterministicFetchPreventsDecorrelation() {
+ checkNondeterministicFetchPreventsDecorrelation(false);
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testNondeterministicFetchPreventsTopDownDecorrelation() {
+ checkNondeterministicFetchPreventsDecorrelation(true);
+ }
+
+ private void checkNondeterministicFetchPreventsDecorrelation(boolean
enableTopDown) {
+ final String sql = "select t.deptno, e.ename\n"
+ + "from (select distinct deptno from emp) t,\n"
+ + "lateral (select ename from emp\n"
+ + " where emp.deptno = t.deptno\n"
+ + " order by sal\n"
+ + " fetch next (rand_integer(2) + 1) rows only) e";
+
+ final RelOptFixture fixture = sql(sql)
+ .withRule() // empty program
+ .withLateDecorrelate(true)
+ .withTopDownGeneralDecorrelate(enableTopDown);
+ if (enableTopDown) {
+ fixture.check();
+ } else {
+ fixture.checkUnchanged();
+ }
+ }
+
@Test void testTopDownGeneralDecorrelateForFilterSome() {
final String sql = "select empno from emp where "
+ "empno > SOME(select empno from emp_b where emp.ename =
emp_b.ename)";
diff --git
a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 6b3255e653..6ce401502c 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -1263,6 +1263,15 @@ public static void checkActualAndReferenceFiles() {
sql(sql).ok();
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchWithExpression() {
+ final String sql =
+ "select empno from emp fetch next (1 + abs(-2)) rows only";
+ sql(sql).ok();
+ }
+
/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-439">[CALCITE-439]
* SqlValidatorUtil.uniquify() may not terminate under some conditions</a>.
*/
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 018c27e898..2e472a968c 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -10747,6 +10747,22 @@ void testGroupExpressionEquivalenceParams() {
.rewritesTo(expected);
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionType() {
+ sql("select name from dept fetch next (^upper('x')^) rows only")
+ .fails("FETCH expression must have a numeric type; "
+ + "actual type is 'CHAR\\(1\\) NOT NULL'");
+ sql("select name from dept fetch next (^'x'^) rows only")
+ .fails("FETCH expression must have a numeric type; "
+ + "actual type is 'CHAR\\(1\\) NOT NULL'");
+ sql("select name from dept fetch next 1.5 rows only").ok();
+ sql("select name from dept "
+ + "fetch next (^row_number() over ()^) rows only")
+ .fails("Windowed aggregate expression is illegal in FETCH clause");
+ }
+
@Test void testRewriteWithOffsetWithoutOrderBy() {
final String sql = "select name from dept offset 2";
final String expected = "SELECT `NAME`\n"
diff --git
a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableMergeUnionTest.java
b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableMergeUnionTest.java
index 68bb56cf36..44055f7074 100644
---
a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableMergeUnionTest.java
+++
b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableMergeUnionTest.java
@@ -78,6 +78,36 @@ class EnumerableMergeUnionTest {
"empid=45; name=Pascal");
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void mergeUnionDoesNotPushNonDeterministicFetch() {
+ tester(false,
+ new HrSchemaBig(),
+ "select * from (select empid, name from emps "
+ + "union all select empid, name from emps) "
+ + "order by empid fetch next (rand_integer(10)) rows only")
+ .explainContains("EnumerableLimitSort(sort0=[$0], dir0=[ASC], "
+ + "fetch=[RAND_INTEGER(10)])\n"
+ + " EnumerableMergeUnion(all=[true])\n"
+ + " EnumerableSort(sort0=[$0], dir0=[ASC])\n");
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void mergeUnionPushesParameterizedFetchExpression() {
+ tester(false,
+ new HrSchemaBig(),
+ "select * from (select empid, name from emps "
+ + "union all select empid, name from emps) "
+ + "order by empid fetch next (? + 1) rows only")
+ .explainContains("EnumerableLimit(fetch=[+(?0, 1)])\n"
+ + " EnumerableMergeUnion(all=[true])\n"
+ + " EnumerableLimitSort(sort0=[$0], dir0=[ASC], "
+ + "fetch=[+(?0, 1)])\n");
+ }
+
@Test void mergeUnionAllOrderByName() {
tester(false,
new HrSchemaBig(),
diff --git
a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index ab26ca8524..32f423c3e1 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -2950,6 +2950,46 @@ LogicalProject(NAME=[$1])
LogicalFilter(condition=[<=($3, 1)])
LogicalProject(SAL=[$5], EXPR$1=[EXTRACT(FLAG(YEAR), $4)],
DEPTNO=[$7], rn=[ROW_NUMBER() OVER (PARTITION BY $7 ORDER BY
EXTRACT(FLAG(YEAR), $4) NULLS LAST, $5 DESC NULLS FIRST)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testDecorrelateProjectWithFetchExpression">
+ <Resource name="sql">
+ <![CDATA[SELECT name, (SELECT sal FROM emp where dept.deptno =
emp.deptno order by sal fetch next (1 + 0) rows only) FROM dept]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject(variablesSet=[[$cor0]], NAME=[$1], EXPR$1=[$SCALAR_QUERY({
+LogicalSort(sort0=[$0], dir0=[ASC], fetch=[+(1, 0)])
+ LogicalProject(SAL=[$5])
+ LogicalFilter(condition=[=($cor0.DEPTNO, $7)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+})])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ <Resource name="planMid">
+ <![CDATA[
+LogicalProject(NAME=[$1], EXPR$1=[$2])
+ LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{0}])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalAggregate(group=[{}], agg#0=[SINGLE_VALUE($0)])
+ LogicalSort(sort0=[$0], dir0=[ASC], fetch=[+(1, 0)])
+ LogicalProject(SAL=[$5])
+ LogicalFilter(condition=[=($cor0.DEPTNO, $7)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalProject(NAME=[$1], EXPR$1=[$3])
+ LogicalJoin(condition=[=($0, $2)], joinType=[left])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalAggregate(group=[{0}], agg#0=[SINGLE_VALUE($1)])
+ LogicalProject(DEPTNO=[$1], SAL=[$0])
+ LogicalFilter(condition=[<=($2, 1)])
+ LogicalProject(SAL=[$5], DEPTNO=[$7], rn=[ROW_NUMBER() OVER
(PARTITION BY $7 ORDER BY $5 NULLS LAST)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
</TestCase>
@@ -11305,6 +11345,95 @@ LogicalProject(USER=[USER])
LogicalAggregate(group=[{0}], EXPR$1=[SUM($1)])
LogicalProject(NAME=[$1], DEPTNO=[$0])
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testNondeterministicFetchPreventsDecorrelation">
+ <Resource name="sql">
+ <![CDATA[select t.deptno, e.ename
+from (select distinct deptno from emp) t,
+lateral (select ename from emp
+ where emp.deptno = t.deptno
+ order by sal
+ fetch next (rand_integer(2) + 1) rows only) e]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject(DEPTNO=[$0], ENAME=[$1])
+ LogicalCorrelate(correlation=[$cor0], joinType=[inner],
requiredColumns=[{0}])
+ LogicalAggregate(group=[{0}])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(ENAME=[$0])
+ LogicalSort(sort0=[$1], dir0=[ASC], fetch=[+(RAND_INTEGER(2), 1)])
+ LogicalProject(ENAME=[$1], SAL=[$5])
+ LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ <Resource name="planMid">
+ <![CDATA[
+LogicalProject(DEPTNO=[$0], ENAME=[$1])
+ LogicalCorrelate(correlation=[$cor0], joinType=[inner],
requiredColumns=[{0}])
+ LogicalAggregate(group=[{0}])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(ENAME=[$0])
+ LogicalSort(sort0=[$1], dir0=[ASC], fetch=[+(RAND_INTEGER(2), 1)])
+ LogicalProject(ENAME=[$1], SAL=[$5])
+ LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testNondeterministicFetchPreventsTopDownDecorrelation">
+ <Resource name="sql">
+ <![CDATA[select t.deptno, e.ename
+from (select distinct deptno from emp) t,
+lateral (select ename from emp
+ where emp.deptno = t.deptno
+ order by sal
+ fetch next (rand_integer(2) + 1) rows only) e]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject(DEPTNO=[$0], ENAME=[$1])
+ LogicalCorrelate(correlation=[$cor0], joinType=[inner],
requiredColumns=[{0}])
+ LogicalAggregate(group=[{0}])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(ENAME=[$0])
+ LogicalSort(sort0=[$1], dir0=[ASC], fetch=[+(RAND_INTEGER(2), 1)])
+ LogicalProject(ENAME=[$1], SAL=[$5])
+ LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ <Resource name="planMid">
+ <![CDATA[
+LogicalProject(DEPTNO=[$0], ENAME=[$1])
+ LogicalCorrelate(correlation=[$cor0], joinType=[inner],
requiredColumns=[{0}])
+ LogicalAggregate(group=[{0}])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(ENAME=[$0])
+ LogicalSort(sort0=[$1], dir0=[ASC], fetch=[+(RAND_INTEGER(2), 1)])
+ LogicalProject(ENAME=[$1], SAL=[$5])
+ LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{0}])
+ LogicalAggregate(group=[{0}])
+ LogicalProject(DEPTNO=[$7])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalProject(ENAME=[$0])
+ LogicalSort(sort0=[$1], dir0=[ASC], fetch=[+(RAND_INTEGER(2), 1)])
+ LogicalProject(ENAME=[$1], SAL=[$5])
+ LogicalFilter(condition=[=($7, $cor0.DEPTNO)])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
</TestCase>
@@ -19923,7 +20052,55 @@ LogicalSort(sort0=[$0], dir0=[ASC], fetch=[0])
]]>
</Resource>
</TestCase>
- <TestCase name="testSortWithDynamicParam">
+ <TestCase name="testSortUnionTransposePushesParameterizedFetchExpression">
+ <Resource name="sql">
+ <![CDATA[select a.name from dept a
+union all
+select b.name from dept b
+order by name fetch next (? + 1) rows only]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalSort(sort0=[$0], dir0=[ASC], fetch=[+(?0, 1)])
+ LogicalUnion(all=[true])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalSort(sort0=[$0], dir0=[ASC], fetch=[+(?0, 1)])
+ LogicalUnion(all=[true])
+ LogicalSort(sort0=[$0], dir0=[ASC], fetch=[+(?0, 1)])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalSort(sort0=[$0], dir0=[ASC], fetch=[+(?0, 1)])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testSortUnionTransposeWithNonDeterministicFetch">
+ <Resource name="sql">
+ <![CDATA[select a.name from dept a
+union all
+select b.name from dept b
+order by name fetch next (rand_integer(10)) rows only]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalSort(sort0=[$0], dir0=[ASC], fetch=[RAND_INTEGER(10)])
+ LogicalUnion(all=[true])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testSortWithDynamicParamPushesOnce">
<Resource name="sql">
<![CDATA[SELECT x.sal
FROM (SELECT emp1.sal
@@ -19957,14 +20134,16 @@ LogicalSort(fetch=[?2])
LogicalProject(SAL=[$0])
LogicalSort(fetch=[?2])
LogicalUnion(all=[true])
- LogicalProject(SAL=[$0])
- LogicalProject(SAL=[$5])
- LogicalSort(fetch=[?0])
- LogicalTableScan(table=[[CATALOG, SALES, EMP]])
- LogicalProject(SAL=[$0])
- LogicalProject(SAL=[$5])
- LogicalSort(fetch=[?1])
- LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalSort(fetch=[?2])
+ LogicalProject(SAL=[$0])
+ LogicalProject(SAL=[$5])
+ LogicalSort(fetch=[?0])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+ LogicalSort(fetch=[?2])
+ LogicalProject(SAL=[$0])
+ LogicalProject(SAL=[$5])
+ LogicalSort(fetch=[?1])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
</TestCase>
diff --git
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index 217b2bbc03..6e98c11baa 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -2601,6 +2601,18 @@ LogicalSort(fetch=[5])
LogicalSort(fetch=[?0])
LogicalProject(EMPNO=[$0])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testFetchWithExpression">
+ <Resource name="sql">
+ <![CDATA[select empno from emp fetch next (1 + abs(-2)) rows only]]>
+ </Resource>
+ <Resource name="plan">
+ <![CDATA[
+LogicalSort(fetch=[+(1, ABS(-2))])
+ LogicalProject(EMPNO=[$0])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
</TestCase>
diff --git a/core/src/test/resources/sql/fetch.iq
b/core/src/test/resources/sql/fetch.iq
new file mode 100644
index 0000000000..8f4b0dd53d
--- /dev/null
+++ b/core/src/test/resources/sql/fetch.iq
@@ -0,0 +1,183 @@
+# fetch.iq
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you 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
+#
+# http://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.
+#
+
+!use post
+!set outputformat mysql
+
+# FETCH accepts a parenthesized arithmetic expression.
+select *
+from (values (1), (2), (3), (4)) as t(x)
+fetch next (1 + abs(-2)) rows only;
++---+
+| X |
++---+
+| 1 |
+| 2 |
+| 3 |
++---+
+(3 rows)
+
+!ok
+
+# FETCH accepts a parenthesized scalar expression.
+select *
+from (values (1), (2), (3), (4)) as t(x)
+fetch next (abs(2)) rows only;
++---+
+| X |
++---+
+| 1 |
+| 2 |
++---+
+(2 rows)
+
+!ok
+
+# FETCH values are not restricted to the BIGINT range.
+select *
+from (values (1), (2), (3), (4)) as t(x)
+fetch next (cast(9223372036854775808 as decimal(20, 0)) + 1) rows only;
++---+
+| X |
++---+
+| 1 |
+| 2 |
+| 3 |
+| 4 |
++---+
+(4 rows)
+
+!ok
+
+# FETCH expression cannot be negative.
+select *
+from (values (1), (2), (3)) as t(x)
+fetch next (0 - 1) rows only;
+FETCH must not be negative
+!error
+
+# FETCH expression cannot evaluate to NULL.
+select *
+from (values (1), (2), (3)) as t(x)
+fetch next (cast(null as integer)) rows only;
+FETCH expression evaluated to NULL
+!error
+
+# FETCH expression may have a fractional numeric type.
+select *
+from (values (1), (2), (3)) as t(x)
+fetch next (1.5) rows only;
++---+
+| X |
++---+
+| 1 |
+| 2 |
++---+
+(2 rows)
+
+!ok
+
+# FETCH expression cannot reference input columns.
+select *
+from (values (1), (2), (3)) as t(x)
+fetch next (x) rows only;
+FETCH expression cannot reference table column 'X'
+!error
+
+# Expressions without parentheses are not allowed in FETCH.
+select *
+from (values (1), (2), (3)) as t(x)
+fetch next 1 + 2 rows only;
+Encountered "+"
+!error
+
+# FETCH expression works with a table source.
+select deptno, dname
+from dept
+order by deptno
+fetch next (1 + 1) rows only;
++--------+-------------+
+| DEPTNO | DNAME |
++--------+-------------+
+| 10 | Sales |
+| 20 | Marketing |
++--------+-------------+
+(2 rows)
+
+!ok
+
+# FETCH expression works together with OFFSET on a table source.
+select deptno, dname
+from dept
+order by deptno
+offset 1 rows
+fetch next (1 + 1) rows only;
++--------+-------------+
+| DEPTNO | DNAME |
++--------+-------------+
+| 20 | Marketing |
+| 30 | Engineering |
++--------+-------------+
+(2 rows)
+
+!ok
+
+# FETCH expression may contain a scalar function on a table source.
+select deptno
+from dept
+order by deptno
+fetch next (abs(-3)) rows only;
++--------+
+| DEPTNO |
++--------+
+| 10 |
+| 20 |
+| 30 |
++--------+
+(3 rows)
+
+!ok
+
+# FETCH expression cannot reference columns of a table source.
+select deptno, dname
+from dept
+order by deptno
+fetch next (deptno) rows only;
+FETCH expression cannot reference table column 'DEPTNO'
+!error
+
+# FETCH expression cannot reference columns even inside a larger expression.
+select deptno, dname
+from dept
+order by deptno
+fetch next (deptno + 1) rows only;
+FETCH expression cannot reference table column 'DEPTNO'
+!error
+
+# FETCH expression may be zero on a table source.
+select deptno
+from dept
+order by deptno
+fetch next (2 - 2) rows only;
++--------+
+| DEPTNO |
++--------+
++--------+
+(0 rows)
+
+!ok
diff --git a/server/src/test/java/org/apache/calcite/test/ServerTest.java
b/server/src/test/java/org/apache/calcite/test/ServerTest.java
index 355d39de7d..39d434f23a 100644
--- a/server/src/test/java/org/apache/calcite/test/ServerTest.java
+++ b/server/src/test/java/org/apache/calcite/test/ServerTest.java
@@ -43,6 +43,7 @@
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
+import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
@@ -452,6 +453,42 @@ static Connection connect() throws SQLException {
}
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7592">[CALCITE-7592]
+ * Add expression support for FETCH</a>. */
+ @Test void testFetchExpressionCannotReferenceInputColumn() throws Exception {
+ try (Connection c = connect();
+ Statement s = c.createStatement()) {
+ s.execute("create table person (id int not null, name varchar(20))");
+ try (PreparedStatement p =
+ c.prepareStatement("insert into person (id, name) values (?,
?)")) {
+ p.setInt(1, 1);
+ p.setString(2, "foo");
+ assertThat(p.executeUpdate(), is(1));
+ }
+
+ SQLException e =
+ assertThrows(
+ SQLException.class, () -> s.executeQuery("select * from person "
+ + "fetch next id rows only"));
+ assertThat(e.getMessage(), containsString("Encountered \"id\""));
+
+ e =
+ assertThrows(
+ SQLException.class, () -> s.executeQuery("select * from person "
+ + "fetch next (id) rows only"));
+ assertThat(e.getMessage(),
+ containsString("FETCH expression cannot reference table column
'ID'"));
+
+ e =
+ assertThrows(
+ SQLException.class, () -> s.executeQuery("select * from person "
+ + "fetch next (1 + id) rows only"));
+ assertThat(e.getMessage(),
+ containsString("FETCH expression cannot reference table column
'ID'"));
+ }
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-6022">[CALCITE-6022]
* Support "CREATE TABLE ... LIKE" DDL in server module</a>. */
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 904ab04619..56fbf8a86c 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -427,8 +427,13 @@ ## Grammar
"SELECT x, y FROM t ORDER BY x, y"
An optional trailing ASC / DESC and NULLS FIRST / NULLS LAST applies to all
keys.
-In *query*, *count* and *start* may each be either an unsigned numeric literal
-or a dynamic parameter whose value is numeric.
+In *query*, *start* may be either an unsigned numeric literal or a dynamic
+parameter whose value is numeric. The *count* in a LIMIT clause may be either
+an unsigned numeric literal or a dynamic parameter whose value is numeric. The
+*count* in a FETCH clause may be an unsigned numeric literal, a dynamic
+parameter whose value is numeric, or a scalar expression enclosed in
+parentheses. A FETCH *count* expression cannot reference columns from the query
+input, and cannot contain aggregate functions, window functions, or
sub-queries.
Support for decimal or non-integer values is adapter-dependent.
An aggregate query is a query that contains a GROUP BY or a HAVING
diff --git
a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 4058d716bf..394aecf26f 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -4104,12 +4104,31 @@ void checkPeriodPredicate(Checker checker) {
+ "FROM `FOO`\n"
+ "OFFSET ? ROWS\n"
+ "FETCH NEXT ? ROWS ONLY");
+ // CALCITE-7592: Arithmetic and scalar expressions are allowed within
parentheses.
+ sql("select a from foo fetch next (1 + abs(-2)) rows only")
+ .ok("SELECT `A`\n"
+ + "FROM `FOO`\n"
+ + "FETCH NEXT (1 + ABS(-2)) ROWS ONLY");
+ // Expressions without parentheses are not allowed.
+ sql("select a from foo fetch next 1 ^+^ 2 rows only")
+ .fails("(?s).*Encountered \"\\+\" at .*");
+ sql("select a from foo fetch next ? ^+^ abs(2) rows only")
+ .fails("(?s).*Encountered \"\\+\" at .*");
// missing ROWS after FETCH
sql("select a from foo offset 1 fetch next 3 ^only^")
.fails("(?s).*Encountered \"only\" at .*");
// FETCH before OFFSET is illegal
sql("select a from foo fetch next 3 rows only ^offset^ 1")
.fails("(?s).*Encountered \"offset\" at .*");
+ // Subqueries are not allowed in FETCH
+ sql("select a from foo fetch next ^select^ 2 rows only")
+ .fails("(?s).*Encountered \"select\" at .*");
+ sql("select a from foo fetch next (^select^ 2) rows only")
+ .fails("(?s).*Encountered \"select\" at .*");
+ sql("select a from foo fetch next (^select^ ?) rows only")
+ .fails("(?s).*Encountered \"select\" at .*");
+ sql("select a from foo fetch next (^select^ max(a) from foo) rows only")
+ .fails("(?s).*Encountered \"select\" at .*");
}
/**