+ )
+ { 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 80c46ec3f9ea..71dc7e602624 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}.
*
* 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) {
*
*
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 02fd54bdad86..de1f94d562d5 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 325fe687ba4d..97d9fd8169b7 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 7d47e639b78e..57f864794aa9 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 71d9f2b22e42..0f393a3e68d8 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 {
+ 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 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 e728c22e1ede..869f1ad50e78 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 869d34333547..2cb710f39808 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 e83f4c1da9f4..3e7824e1aac6 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 1f6502243626..5b096289382e 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 0404fa7bc51c..dc91fca54ecb 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;
@@ -1191,7 +1192,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);
@@ -1249,13 +1250,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 037a4d605459..f69810a14d42 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 5d70c5e0dec4..5cb60bc90652 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
@@ -41,7 +41,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;
@@ -519,9 +518,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 4310d6d65576..df967e56aad6 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 9bcf026fc656..08563cdcdb86 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 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 416825ee926d..93b6af657c43 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 3604e98dfd5b..b592093a5aef 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(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(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 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 retainDeterministic(List list) {
List 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 c6e1a4dbdcc5..e5932b63768d 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 validatorContext(int a0, int a1,
@BaseMessage("Values passed to {0} operator must have compatible types")
ExInst incompatibleValueType(String a0);
+ @BaseMessage("FETCH expression must have a numeric type; actual type is ''{0}''")
+ ExInst fetchExpressionMustBeNumeric(String type);
+
+ @BaseMessage("FETCH expression cannot reference table column ''{0}''")
+ ExInst fetchExpressionCannotReferenceColumn(String column);
+
+ @BaseMessage("FETCH expression evaluated to NULL")
+ ExInst fetchExpressionEvaluatedToNull();
+
@BaseMessage("Values in expression list must have compatible types")
ExInst 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 869976f0d42a..663aee63b0db 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
@@ -1081,7 +1081,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);
@@ -1091,13 +1102,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 82376ae576ab..f31276413600 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 5d667fe502e3..b1ac0f53961a 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() {
+ @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
@@ -4469,6 +4497,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 a5a817579372..b4999b455bef 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 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 291eb619d0ed..4139cf4b2b99 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 36d56a9f0488..9c8db91ab4c7 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -79,13 +79,16 @@
import org.apache.calcite.rex.RexExecutor;
import org.apache.calcite.rex.RexFieldCollation;
import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLambda;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexOver;
import org.apache.calcite.rex.RexShuttle;
import org.apache.calcite.rex.RexSimplify;
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 +111,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 +3805,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 +3815,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 fieldCollations =
registrar.registerFieldCollations(nodes);
@@ -3884,6 +3892,44 @@ public RelBuilder sortLimit(@Nullable RexNode offsetNode, @Nullable RexNode fetc
return this;
}
+ private static boolean isValidFetchExpression(RexNode node) {
+ return Boolean.TRUE.equals(
+ node.accept(
+ new RexVisitorImpl<@Nullable Boolean>(true) {
+ @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;
+ }
+
+ @Override public Boolean visitOver(RexOver over) {
+ return false;
+ }
+
+ @Override public Boolean visitSubQuery(RexSubQuery subQuery) {
+ return false;
+ }
+
+ @Override public Boolean visitLambda(RexLambda lambda) {
+ return false;
+ }
+ }));
+ }
+
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 f4f16d73266a..19f3ef47a8d3 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 40b825939df9..70370d7bb1ab 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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 f056ee9d4fc8..92d325d8ef62 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
@@ -4918,6 +4918,74 @@ private SqlDialect nonOrdinalDialect() {
.withSybase().ok(expectedSybase);
}
+ /** Test case for
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 60fd60a83751..bf506ceb121a 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;
@@ -3593,6 +3594,35 @@ private void assertTypeAndToString(
hasSize(0));
}
+ /** Test case for
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 0c5ab19b2ec4..bf2aba123156 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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 e53f1027f78d..30aa2ff77fdf 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;
@@ -5577,6 +5580,108 @@ private static RelNode buildCorrelateWithJoin(JoinRelType type, RelBuilder build
assertThat(mq.getMaxRowCount(planAfter), is(Double.POSITIVE_INFINITY));
}
+ /** Test case for
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 9c3c30e3448e..460e066051fe 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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 d94ae4652659..b4398ed49864 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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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"
@@ -5981,10 +6009,9 @@ private void checkEmptyJoin(RelOptFixture f) {
}
/** Test case for
- * [CALCITE-6647]
- * SortUnionTransposeRule should not push SORT past a UNION when SORT's fetch is DynamicParam
- . */
- @Test void testSortWithDynamicParam() {
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @Test void testSortWithDynamicParamPushesOnce() {
HepProgramBuilder builder = new HepProgramBuilder();
builder.addRuleClass(SortProjectTransposeRule.class);
builder.addRuleClass(SortUnionTransposeRule.class);
@@ -9714,6 +9741,19 @@ private void checkSemiJoinRuleOnAntiJoin(RelOptRule rule) {
.check();
}
+ /** Test case for
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 [CALCITE-7289]
* Select NULL subquery throwing exception. */
@Test void testNullSelect() {
@@ -12202,6 +12242,39 @@ private static RelNode applyAggregateRemoveLiteralAggRule(RelNode rel) {
.check();
}
+ /** Test case for
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @Test void testNondeterministicFetchPreventsDecorrelation() {
+ checkNondeterministicFetchPreventsDecorrelation(false);
+ }
+
+ /** Test case for
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @Test void testNondeterministicFetchPreventsTopDownDecorrelation() {
+ checkNondeterministicFetchPreventsDecorrelation(true);
+ }
+
+ private void checkNondeterministicFetchPreventsDecorrelation(boolean topDown) {
+ 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(topDown);
+ if (topDown) {
+ 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 6b3255e653c7..6ce401502c93 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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @Test void testFetchWithExpression() {
+ final String sql =
+ "select empno from emp fetch next (1 + abs(-2)) rows only";
+ sql(sql).ok();
+ }
+
/** Test case for
* [CALCITE-439]
* SqlValidatorUtil.uniquify() may not terminate under some conditions. */
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 228d90026b22..5879bbbf9952 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -10658,6 +10658,22 @@ void testGroupExpressionEquivalenceParams() {
.rewritesTo(expected);
}
+ /** Test case for
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 68bb56cf366d..44055f707462 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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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 64061b8910f8..01a975ac8638 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]])
+]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -11285,6 +11325,95 @@ LogicalProject(USER=[USER])
LogicalAggregate(group=[{0}], EXPR$1=[SUM($1)])
LogicalProject(NAME=[$1], DEPTNO=[$0])
LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -19903,7 +20032,55 @@ LogicalSort(sort0=[$0], dir0=[ASC], fetch=[0])
]]>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 f2ab8fec37e7..9bdfc36442a4 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]])
+]]>
+
+
+
+
+
+
+
+
diff --git a/core/src/test/resources/sql/fetch.iq b/core/src/test/resources/sql/fetch.iq
new file mode 100644
index 000000000000..8f4b0dd53d58
--- /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 355d39de7d63..39d434f23ae9 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
+ * [CALCITE-7592]
+ * Add expression support for FETCH. */
+ @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
* [CALCITE-6022]
* Support "CREATE TABLE ... LIKE" DDL in server module. */
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 8ff9857cb8c0..b1f67eeef1ee 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -427,8 +427,13 @@ in the order that they appear in the list; for example:
"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 e61cc66d5662..372cd1189d28 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
@@ -4075,12 +4075,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 .*");
}
/**