This is an automated email from the ASF dual-hosted git repository.
mrhhsg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 69d25572db1 [fix](ntile) Accept constant expressions that fold to a
positive integer as NTILE bucket (#68503)
69d25572db1 is described below
commit 69d25572db19d3f91d7bec4f0b83e07e98ccf06f
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Oct 15 15:59:53 2026 +0800
[fix](ntile) Accept constant expressions that fold to a positive integer as
NTILE bucket (#68503)
### What problem does this PR solve?
Issue Number: None
Related PR: #58200
Problem Summary:
`ntile(1 + 1) OVER (ORDER BY k)` was rejected at analysis time, although
the bucket is a constant expression that folds to `2`:
```
mysql> SELECT ntile(1 + 1) OVER (ORDER BY k) AS bucket
-> FROM (SELECT 1 AS k UNION ALL SELECT 2 UNION ALL SELECT 3) t;
ERROR 1105 (HY000): errCode = 2, detailMessage = The bucket parameter of
NTILE
must be a constant positive integer: ntile((1 + 1))
```
LAG/LEAD (#58200) and NTH_VALUE already accept such expressions, because
their literal checks run after constant folding.
Root cause: `Ntile.checkLegalityBeforeTypeCoercion` runs while the
function
is being bound in the analyzer, before `FoldConstant` has turned `1 + 1`
into a literal, and it required the argument to already be a `Literal`.
Every non-literal constant expression therefore failed the check.
Fix: keep the integral-type, LARGEINT and constant checks as they are,
then
fold the bucket for validation with
`ExpressionUtils.foldConstantArgument`
(the shared helper introduced in #68579) and require the folded value to
be
an integer literal greater than zero. Constant expressions such as `1 +
1`,
`cast(5 - 2 as bigint)` or `abs(-3)` are now accepted, while `0`, `1 -
1`,
`1 - 2`, a NULL result such as `cast('abc' as int)`, a non-constant
column
and a LARGEINT bucket are still rejected with the existing error
messages.
The backend registers `ntile` only for a non-nullable integer argument
and
reads the bucket count from the argument column, so the bucket must
still be
a literal when the plan is translated. A `checkLegalityAfterRewrite` is
added
that requires a positive integer literal after rewrite (the same
approach LAG
and LEAD use). When constant folding is skipped with
`debug_skip_fold_constant=true`, an unfolded expression such as
`cast('3' as int)` is now rejected with the NTILE message instead of
failing
in the backend with `Agg Function ntile(int) is not implemented`.
After the fix the query above returns `1, 1, 2`.
### Release note
None
### Check List (For Author)
- Test:
- Unit Test: `NtileBucketTest` covers a literal bucket, foldable
constant expressions, expressions folded to zero/negative/NULL,
non-constant, non-integral and LARGEINT buckets, and the
after-rewrite check for literal and unfolded buckets.
- Regression test: `test_ntile_function` adds `ntile(1 + 1)`,
`ntile(cast(5 - 2 as bigint))`, `ntile(abs(-3))` with generated
output, error cases for `ntile(1 - 1)`, `ntile(1 - 2)` and
`ntile(cast('abc' as int))`, and cases with
`debug_skip_fold_constant=true`.
- Baseline update: merged current master and reused the shared
constant-argument helper. The net PR diff passes `git diff --check`; FE
UT and regression were not rerun after this update because
`thirdparty/installed/bin/protoc` is unavailable in this worktree. CI is
pending.
- Behavior changed: Yes (NTILE now accepts a constant bucket expression
that folds to a positive integer; such queries previously failed)
- Does this need documentation: No
---
.../glue/translator/ExpressionTranslator.java | 9 ++
.../analysis/ReplaceExpressionByChildOutput.java | 8 +-
.../trees/expressions/functions/window/Ntile.java | 29 +++-
.../ReplaceExpressionByChildOutputTest.java | 27 ++++
.../functions/window/NtileBucketTest.java | 178 +++++++++++++++++++++
.../window_functions/test_ntile_function.out | 121 ++++++++++++++
.../window_functions/test_ntile_function.groovy | 73 +++++++++
7 files changed, 438 insertions(+), 7 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/ExpressionTranslator.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/ExpressionTranslator.java
index 948238357a4..7676cfb82c6 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/ExpressionTranslator.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/ExpressionTranslator.java
@@ -112,6 +112,7 @@ import
org.apache.doris.nereids.trees.expressions.functions.udf.JavaUdtf;
import org.apache.doris.nereids.trees.expressions.functions.udf.PythonUdaf;
import org.apache.doris.nereids.trees.expressions.functions.udf.PythonUdf;
import org.apache.doris.nereids.trees.expressions.functions.udf.PythonUdtf;
+import org.apache.doris.nereids.trees.expressions.functions.window.Ntile;
import
org.apache.doris.nereids.trees.expressions.functions.window.WindowFunction;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionVisitor;
@@ -484,6 +485,14 @@ public class ExpressionTranslator extends
DefaultExpressionVisitor<Expr, PlanTra
return in;
}
+ @Override
+ public Expr visitNtile(Ntile ntile, PlanTranslatorContext context) {
+ // The rewrite rules that fold the bucket and run
checkLegalityAfterRewrite on window expressions can be
+ // disabled, so check again before the bucket reaches the backend,
which uses it as a divisor.
+ ntile.checkLegalityAfterRewrite();
+ return visitWindowFunction(ntile, context);
+ }
+
@Override
public Expr visitWindowFunction(WindowFunction function,
PlanTranslatorContext context) {
// translate argument types from DataType to Type
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ReplaceExpressionByChildOutput.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ReplaceExpressionByChildOutput.java
index cd53086f966..1dc703d5584 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ReplaceExpressionByChildOutput.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ReplaceExpressionByChildOutput.java
@@ -72,7 +72,13 @@ public class ReplaceExpressionByChildOutput implements
AnalysisRuleFactory {
for (NamedExpression expr : output) {
if (expr instanceof Alias) {
Alias alias = (Alias) expr;
- sMap.put(alias.child(), alias.toSlot());
+ // replacing a constant sub-expression anywhere it
structurally recurs, e.g. inside
+ // a function argument that must fold to a literal (NTILE's
bucket, LAG/LEAD's offset),
+ // gives no benefit since a constant is folded once anyway,
and can only break such a
+ // position wherever the constant happens to also be an
unrelated output value.
+ if (!alias.child().isConstant()) {
+ sMap.put(alias.child(), alias.toSlot());
+ }
}
}
return sMap;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/window/Ntile.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/window/Ntile.java
index bbb915cdcb8..c02f96bda84 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/window/Ntile.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/window/Ntile.java
@@ -22,6 +22,7 @@ import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.functions.AlwaysNotNullable;
import
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
import org.apache.doris.nereids.trees.expressions.shape.LeafExpression;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
@@ -30,6 +31,7 @@ import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.IntegerType;
import org.apache.doris.nereids.types.SmallIntType;
import org.apache.doris.nereids.types.TinyIntType;
+import org.apache.doris.nereids.util.ExpressionUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
@@ -80,12 +82,27 @@ public class Ntile extends WindowFunction implements
LeafExpression, AlwaysNotNu
throw new AnalysisException(
"The bucket of NTILE must be a constant value: " +
this.toSql());
}
- if (buckets instanceof Literal) {
- if (((Literal) buckets).getDouble() <= 0) {
- throw new AnalysisException(
- "The bucket parameter of NTILE must be a constant positive
integer: " + this.toSql());
- }
- } else {
+ // The bucket may be a constant expression such as `1 + 1`, which is
folded to a literal only by the
+ // rewrite phase after this check runs. Reject early a bucket that FE
already evaluates to a non-positive
+ // or NULL value. A bucket FE can not evaluate, e.g. `3 % 2`, may
still be folded by BE when
+ // enable_fold_constant_by_be is set, so it is left to
checkLegalityAfterRewrite.
+ Expression evaluated = ExpressionUtils.foldConstantArgument(buckets);
+ if (evaluated instanceof Literal) {
+ checkPositiveBucket(evaluated);
+ }
+ }
+
+ @Override
+ public void checkLegalityAfterRewrite() {
+ // The backend reads the bucket from the argument column and registers
ntile only for a non-nullable
+ // integer argument, so the bucket must have been folded to a literal
before the plan is translated.
+ // That does not happen when constant folding is skipped, e.g. with
debug_skip_fold_constant=true, or
+ // when the configured folding can not evaluate the bucket.
+ checkPositiveBucket(getArgument(0));
+ }
+
+ private void checkPositiveBucket(Expression bucket) {
+ if (!(bucket instanceof IntegerLikeLiteral) || ((IntegerLikeLiteral)
bucket).getLongValue() <= 0) {
throw new AnalysisException(
"The bucket parameter of NTILE must be a constant positive
integer: " + this.toSql());
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ReplaceExpressionByChildOutputTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ReplaceExpressionByChildOutputTest.java
index 32387911455..011cc1d524d 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ReplaceExpressionByChildOutputTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ReplaceExpressionByChildOutputTest.java
@@ -18,10 +18,13 @@
package org.apache.doris.nereids.rules.analysis;
import org.apache.doris.nereids.properties.OrderKey;
+import org.apache.doris.nereids.trees.expressions.Add;
import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
import org.apache.doris.nereids.trees.plans.logical.LogicalHaving;
@@ -59,6 +62,30 @@ public class ReplaceExpressionByChildOutputTest implements
MemoPatternMatchSuppo
);
}
+ @Test
+ void testSortProjectDoesNotReplaceConstantSubExpression() {
+ // an unrelated constant output column, `2 AS two` or `1 + 1 AS two`,
must not make the rule reuse
+ // its slot for the structurally equal constant nested inside a
different order-by sub-expression,
+ // e.g. a function argument that has to fold to a literal (such as
NTILE's bucket)
+ SlotReference col1 = new SlotReference("col1", IntegerType.INSTANCE);
+ LogicalOlapScan logicalOlapScan =
PlanConstructor.newLogicalOlapScan(0, "t1", 0);
+ for (Expression constant : ImmutableList.of(new IntegerLiteral(2),
+ new Add(new IntegerLiteral(1), new IntegerLiteral(1)))) {
+ Alias alias = new Alias(constant, "two");
+ LogicalProject<Plan> logicalProject = new
LogicalProject<>(ImmutableList.of(alias), logicalOlapScan);
+ Add orderKeyExpr = new Add(constant, col1);
+ List<OrderKey> orderKeys = ImmutableList.of(new
OrderKey(orderKeyExpr, true, true));
+ LogicalSort<LogicalProject<Plan>> logicalSort = new
LogicalSort<>(orderKeys, logicalProject);
+
+ PlanChecker.from(MemoTestUtils.createConnectContext(), logicalSort)
+ .applyBottomUp(new ReplaceExpressionByChildOutput())
+ .matchesFromRoot(
+ logicalSort(logicalProject()).when(sort ->
+
sort.getOrderKeys().get(0).getExpr().equals(orderKeyExpr))
+ );
+ }
+ }
+
@Test
void testSortAggregate() {
SlotReference slotReference = new SlotReference("col1",
IntegerType.INSTANCE);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/window/NtileBucketTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/window/NtileBucketTest.java
new file mode 100644
index 00000000000..622ab8f6734
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/window/NtileBucketTest.java
@@ -0,0 +1,178 @@
+// 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.
+
+package org.apache.doris.nereids.trees.expressions.functions.window;
+
+import org.apache.doris.analysis.FunctionCallExpr;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.glue.translator.ExpressionTranslator;
+import org.apache.doris.nereids.glue.translator.PlanTranslatorContext;
+import org.apache.doris.nereids.trees.expressions.Add;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.Mod;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.Subtract;
+import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.LargeIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.IntegerType;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+
+public class NtileBucketTest {
+
+ private static final String POSITIVE_INTEGER_MESSAGE =
+ "The bucket parameter of NTILE must be a constant positive
integer";
+
+ @Test
+ public void testLiteralBucketIsAccepted() {
+ Ntile ntile = new Ntile(new IntegerLiteral(3));
+
+ Assertions.assertDoesNotThrow(ntile::checkLegalityBeforeTypeCoercion);
+ }
+
+ @Test
+ public void testFoldableConstantExpressionIsAccepted() {
+ Ntile add = new Ntile(new Add(new IntegerLiteral(1), new
IntegerLiteral(1)));
+ Ntile cast = new Ntile(new Cast(new Subtract(new IntegerLiteral(5),
new IntegerLiteral(2)),
+ BigIntType.INSTANCE));
+
+ Assertions.assertDoesNotThrow(add::checkLegalityBeforeTypeCoercion);
+ Assertions.assertDoesNotThrow(cast::checkLegalityBeforeTypeCoercion);
+ }
+
+ @Test
+ public void testConstantExpressionFoldedToNonPositiveIsRejected() {
+ Ntile zero = new Ntile(new Subtract(new IntegerLiteral(1), new
IntegerLiteral(1)));
+ Ntile negative = new Ntile(new Subtract(new IntegerLiteral(1), new
IntegerLiteral(2)));
+
+ AnalysisException zeroException = Assertions.assertThrows(
+ AnalysisException.class,
zero::checkLegalityBeforeTypeCoercion);
+
Assertions.assertTrue(zeroException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ AnalysisException negativeException = Assertions.assertThrows(
+ AnalysisException.class,
negative::checkLegalityBeforeTypeCoercion);
+
Assertions.assertTrue(negativeException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ }
+
+ @Test
+ public void testNonPositiveLiteralIsRejected() {
+ Ntile zero = new Ntile(new IntegerLiteral(0));
+ Ntile negative = new Ntile(new IntegerLiteral(-1));
+
+ AnalysisException zeroException = Assertions.assertThrows(
+ AnalysisException.class,
zero::checkLegalityBeforeTypeCoercion);
+
Assertions.assertTrue(zeroException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ AnalysisException negativeException = Assertions.assertThrows(
+ AnalysisException.class,
negative::checkLegalityBeforeTypeCoercion);
+
Assertions.assertTrue(negativeException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ }
+
+ @Test
+ public void testConstantExpressionFoldedToNullIsRejected() {
+ Ntile nullBucket = new Ntile(new Cast(new StringLiteral("abc"),
IntegerType.INSTANCE));
+ Ntile nullLiteral = new Ntile(new NullLiteral(IntegerType.INSTANCE));
+
+ AnalysisException castException = Assertions.assertThrows(
+ AnalysisException.class,
nullBucket::checkLegalityBeforeTypeCoercion);
+
Assertions.assertTrue(castException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ AnalysisException nullException = Assertions.assertThrows(
+ AnalysisException.class,
nullLiteral::checkLegalityBeforeTypeCoercion);
+
Assertions.assertTrue(nullException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ }
+
+ @Test
+ public void testNonConstantBucketIsRejected() {
+ Ntile ntile = new Ntile(new SlotReference("k1", IntegerType.INSTANCE));
+
+ AnalysisException exception = Assertions.assertThrows(
+ AnalysisException.class,
ntile::checkLegalityBeforeTypeCoercion);
+ Assertions.assertTrue(exception.getMessage().contains("The bucket of
NTILE must be a constant value"));
+ }
+
+ @Test
+ public void testNonIntegralBucketIsRejected() {
+ Ntile ntile = new Ntile(new DoubleLiteral(2.0));
+
+ AnalysisException exception = Assertions.assertThrows(
+ AnalysisException.class,
ntile::checkLegalityBeforeTypeCoercion);
+ Assertions.assertTrue(exception.getMessage().contains("The bucket of
NTILE must be a integer"));
+ }
+
+ @Test
+ public void testLiteralBucketIsAcceptedAfterRewrite() {
+ Ntile ntile = new Ntile(new IntegerLiteral(3));
+
+ Assertions.assertDoesNotThrow(ntile::checkLegalityAfterRewrite);
+ }
+
+ @Test
+ public void testUnfoldedBucketIsRejectedAfterRewrite() {
+ Ntile unfolded = new Ntile(new Add(new IntegerLiteral(1), new
IntegerLiteral(1)));
+ Ntile zero = new Ntile(new IntegerLiteral(0));
+
+ AnalysisException unfoldedException = Assertions.assertThrows(
+ AnalysisException.class, unfolded::checkLegalityAfterRewrite);
+
Assertions.assertTrue(unfoldedException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ AnalysisException zeroException = Assertions.assertThrows(
+ AnalysisException.class, zero::checkLegalityAfterRewrite);
+
Assertions.assertTrue(zeroException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ }
+
+ @Test
+ public void testConstantExpressionNotFoldableOnFeIsLeftToRewrite() {
+ // FE has no executor for `%`, but BE can fold it when
enable_fold_constant_by_be is set,
+ // so it is accepted at binding time and must be a positive literal
after rewrite.
+ Ntile ntile = new Ntile(new Mod(new IntegerLiteral(3), new
IntegerLiteral(2)));
+
+ Assertions.assertDoesNotThrow(ntile::checkLegalityBeforeTypeCoercion);
+ AnalysisException exception = Assertions.assertThrows(
+ AnalysisException.class, ntile::checkLegalityAfterRewrite);
+
Assertions.assertTrue(exception.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ }
+
+ @Test
+ public void testTranslateChecksBucket() {
+ FunctionCallExpr translated = (FunctionCallExpr)
ExpressionTranslator.translate(
+ new Ntile(new BigIntLiteral(3)), new PlanTranslatorContext());
+ Assertions.assertEquals("ntile", translated.getFnName().getFunction());
+
+ AnalysisException unfoldedException =
Assertions.assertThrows(AnalysisException.class,
+ () -> ExpressionTranslator.translate(new Ntile(new Mod(new
BigIntLiteral(3), new BigIntLiteral(3))),
+ new PlanTranslatorContext()));
+
Assertions.assertTrue(unfoldedException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ AnalysisException zeroException =
Assertions.assertThrows(AnalysisException.class,
+ () -> ExpressionTranslator.translate(new Ntile(new
BigIntLiteral(0)), new PlanTranslatorContext()));
+
Assertions.assertTrue(zeroException.getMessage().contains(POSITIVE_INTEGER_MESSAGE));
+ }
+
+ @Test
+ public void testLargeIntBucketIsRejected() {
+ Ntile ntile = new Ntile(new
LargeIntLiteral(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE)));
+
+ AnalysisException exception = Assertions.assertThrows(
+ AnalysisException.class,
ntile::checkLegalityBeforeTypeCoercion);
+ Assertions.assertTrue(exception.getMessage().contains(
+ "The bucket of NTILE must be an integer within the range of
BIGINT"));
+ }
+}
diff --git
a/regression-test/data/query_p0/sql_functions/window_functions/test_ntile_function.out
b/regression-test/data/query_p0/sql_functions/window_functions/test_ntile_function.out
index 00657bf3ec4..b41b7468dc0 100644
---
a/regression-test/data/query_p0/sql_functions/window_functions/test_ntile_function.out
+++
b/regression-test/data/query_p0/sql_functions/window_functions/test_ntile_function.out
@@ -43,3 +43,124 @@
3 3 1 2
3 4 5 3
+-- !select_const_expr --
+1 1 1 1
+1 1 1 1
+1 1 3 1
+1 2 1 1
+1 3 1 2
+1 3 2 2
+1 3 3 2
+4 3 1 1
+5 4 3 1
+
+-- !select_const_expr --
+1 1 1 1
+1 1 1 1
+1 1 3 1
+1 2 1 2
+1 3 1 2
+1 3 2 3
+1 3 3 3
+4 3 1 1
+5 4 3 1
+
+-- !select_const_expr --
+1 1 1 1
+1 1 1 1
+1 1 3 1
+1 2 1 2
+1 3 1 2
+1 3 2 3
+1 3 3 3
+4 3 1 1
+5 4 3 1
+
+-- !select_const_alias_collision --
+2 1 1 1 1
+2 1 1 1 1
+2 1 1 3 1
+2 1 2 1 1
+2 4 3 1 1
+2 5 4 3 1
+2 1 3 1 2
+2 1 3 2 2
+2 1 3 3 2
+
+-- !select_const_alias_collision --
+2 1 1 1 0
+2 1 1 1 0
+2 1 1 3 0
+2 1 2 1 0
+2 1 3 1 0
+2 1 3 2 0
+2 1 3 3 0
+2 4 3 1 0
+2 5 4 3 0
+
+-- !select_const_alias_collision --
+2 1 1 1 0
+2 1 1 1 0
+2 1 1 3 0
+2 1 2 1 0
+2 1 3 1 0
+2 1 3 2 0
+2 1 3 3 0
+2 4 3 1 0
+2 5 4 3 0
+
+-- !select_const_alias_collision --
+3 1 1 1 0
+3 1 1 1 0
+3 1 1 3 0
+3 1 2 1 0
+3 1 3 1 0
+3 1 3 2 0
+3 1 3 3 0
+3 4 3 1 0
+3 5 4 3 0
+
+-- !select_skip_fold --
+1 1 1 1
+1 1 1 1
+1 1 3 1
+1 2 1 1
+1 3 1 2
+1 3 2 2
+1 3 3 2
+4 3 1 1
+5 4 3 1
+
+-- !select_fold_by_be --
+1 1 1 1
+1 1 1 1
+1 1 3 1
+1 2 1 1
+1 3 1 2
+1 3 2 2
+1 3 3 2
+4 3 1 1
+5 4 3 1
+
+-- !select_fold_by_be --
+1 1 1 1
+1 1 1 1
+1 1 3 1
+1 2 1 2
+1 3 1 2
+1 3 2 3
+1 3 3 3
+4 3 1 1
+5 4 3 1
+
+-- !select_no_window_rewrite --
+1 1 1 1
+1 1 1 1
+1 1 3 1
+1 2 1 1
+1 3 1 2
+1 3 2 2
+1 3 3 2
+4 3 1 1
+5 4 3 1
+
diff --git
a/regression-test/suites/query_p0/sql_functions/window_functions/test_ntile_function.groovy
b/regression-test/suites/query_p0/sql_functions/window_functions/test_ntile_function.groovy
index 124eebfce64..855cc10a317 100644
---
a/regression-test/suites/query_p0/sql_functions/window_functions/test_ntile_function.groovy
+++
b/regression-test/suites/query_p0/sql_functions/window_functions/test_ntile_function.groovy
@@ -71,6 +71,27 @@ suite("test_ntile_function") {
qt_select "select k2, k1, k3, ntile(3) over (order by k2,k3,k1) as ntile
from ${tableName} order by k2, k3, k1, ntile;"
qt_select "select k3, k2, k1, ntile(3) over (partition by k3 order by
k3,k2,k1) as ntile from ${tableName} order by k3, k2, k1, ntile;"
+ // the bucket may be a constant expression that folds to a positive integer
+ qt_select_const_expr "select k1, k2, k3, ntile(1 + 1) over (partition by
k1 order by k2,k3) as ntile from ${tableName} order by k1, k2, k3, ntile;"
+ qt_select_const_expr "select k1, k2, k3, ntile(cast(5 - 2 as bigint)) over
(partition by k1 order by k2,k3) as ntile from ${tableName} order by k1, k2,
k3, ntile;"
+ qt_select_const_expr "select k1, k2, k3, ntile(abs(-3)) over (partition by
k1 order by k2,k3) as ntile from ${tableName} order by k1, k2, k3, ntile;"
+
+ // the bucket literal must stay a literal even when an unrelated projected
column happens to
+ // have the same constant value, e.g. `2 AS two`; rewriting the order-by
key must not reuse that
+ // column's slot inside ntile's own bucket argument just because the
literal values match
+ qt_select_const_alias_collision "select 2 as two, k1, k2, k3, ntile(2)
over (partition by k1 order by k2,k3) as ntile " +
+ "from ${tableName} order by ntile(2) over (partition by k1 order
by k2,k3), k1, k2, k3;"
+
+ // the same collision with the window only in order by: the selected
constant matches the bucket, and the
+ // repeatedly used volatile column `r` keeps the projection from being
merged back, so a bucket rewritten
+ // to that column's slot would stay a slot; floor(rand()) is always 0,
which keeps the result stable
+ qt_select_const_alias_collision "select 2 as two, k1, k2, k3,
floor(rand()) as r from ${tableName} " +
+ "order by ntile(2) over (order by k1,k2,k3), r + r, k1, k2, k3;"
+ qt_select_const_alias_collision "select 1 + 1 as two, k1, k2, k3,
floor(rand()) as r from ${tableName} " +
+ "order by ntile(1 + 1) over (order by k1,k2,k3), r + r, k1, k2,
k3;"
+ qt_select_const_alias_collision "select cast(5 - 2 as bigint) as three,
k1, k2, k3, floor(rand()) as r from ${tableName} " +
+ "order by ntile(cast(5 - 2 as bigint)) over (order by k1,k2,k3), r
+ r, k1, k2, k3;"
+
test {
sql "select k1, k2, k3, ntile(0) over (partition by k1 order by k2) as
ntile from ${tableName} order by k1, k2, k3 desc;"
exception "positive"
@@ -81,11 +102,63 @@ suite("test_ntile_function") {
exception "The bucket of NTILE must be a constant value"
}
+ test {
+ sql "select k1, k2, k3, ntile(1 - 1) over (partition by k1 order by
k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
+ exception "The bucket parameter of NTILE must be a constant positive
integer"
+ }
+
+ test {
+ sql "select k1, k2, k3, ntile(1 - 2) over (partition by k1 order by
k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
+ exception "The bucket parameter of NTILE must be a constant positive
integer"
+ }
+
+ test {
+ sql "select k1, k2, k3, ntile(cast('abc' as int)) over (partition by
k1 order by k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
+ exception "The bucket parameter of NTILE must be a constant positive
integer"
+ }
+
test {
sql "select k1, k2, k3, ntile(170141183460469231731687303715884105727)
over (partition by k1 order by k2) as ntile from ${tableName} order by k1, k2,
k3 desc;"
exception "The bucket of NTILE must be an integer within the range of
BIGINT, but got LARGEINT"
}
+ // when constant folding is skipped, a literal bucket still works while an
unfolded constant expression
+ // is rejected by the planner instead of reaching the backend
+ sql "set debug_skip_fold_constant=true"
+ qt_select_skip_fold "select k1, k2, k3, ntile(2) over (partition by k1
order by k2,k3) as ntile from ${tableName} order by k1, k2, k3, ntile;"
+ test {
+ sql "select k1, k2, k3, ntile(1 + 1) over (partition by k1 order by
k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
+ exception "The bucket parameter of NTILE must be a constant positive
integer"
+ }
+ test {
+ sql "select k1, k2, k3, ntile(cast('3' as int)) over (partition by k1
order by k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
+ exception "The bucket parameter of NTILE must be a constant positive
integer"
+ }
+ sql "set debug_skip_fold_constant=false"
+
+ // FE can not evaluate `%` or crc32, but BE folds them when
enable_fold_constant_by_be is set
+ sql "set enable_fold_constant_by_be=true"
+ qt_select_fold_by_be "select k1, k2, k3, ntile(5 % 3) over (partition by
k1 order by k2,k3) as ntile from ${tableName} order by k1, k2, k3, ntile;"
+ qt_select_fold_by_be "select k1, k2, k3, ntile(crc32('a') % 3 + 3) over
(partition by k1 order by k2,k3) as ntile from ${tableName} order by k1, k2,
k3, ntile;"
+ test {
+ sql "select k1, k2, k3, ntile(3 % 3) over (partition by k1 order by
k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
+ exception "The bucket parameter of NTILE must be a constant positive
integer"
+ }
+ sql "set enable_fold_constant_by_be=false"
+ test {
+ sql "select k1, k2, k3, ntile(5 % 3) over (partition by k1 order by
k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
+ exception "The bucket parameter of NTILE must be a constant positive
integer"
+ }
+
+ // the bucket is still checked when translating the plan if the rewrites
that fold and check it are disabled
+ sql "set
disable_nereids_rules='REWRITE_PROJECT_EXPRESSION,REWRITE_WINDOW_EXPRESSION'"
+ qt_select_no_window_rewrite "select k1, k2, k3, ntile(2) over (partition
by k1 order by k2,k3) as ntile from ${tableName} order by k1, k2, k3, ntile;"
+ test {
+ sql "select k1, k2, k3, ntile(1 + 1) over (partition by k1 order by
k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
+ exception "The bucket parameter of NTILE must be a constant positive
integer"
+ }
+ sql "set disable_nereids_rules=''"
+
test {
sql "select k1, k2, k3, ntile(cast(3 as largeint)) over (partition by
k1 order by k2) as ntile from ${tableName} order by k1, k2, k3 desc;"
exception "The bucket of NTILE must be an integer within the range of
BIGINT, but got LARGEINT"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]