This is an automated email from the ASF dual-hosted git repository.

yiguolei 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 ee9d26bcb5d [fix](function) Require the array_sort comparator to 
return -1, 0 or 1 (#68612)
ee9d26bcb5d is described below

commit ee9d26bcb5d7baf45a2b1ae8d87684eb87c78592
Author: Chenyang Sun <[email protected]>
AuthorDate: Thu Oct 15 16:53:49 2026 +0800

    [fix](function) Require the array_sort comparator to return -1, 0 or 1 
(#68612)
    
    ```sql
    SELECT array_sort((x, y) -> x < y, [3, 2, 1]);
    -- before: [FATAL_ERROR]assert cast err ... Bad cast from 
type:doris::ColumnNullable* to doris::ColumnVector<(doris::PrimitiveType)3> 
const* (a debug/ASAN BE aborts)
    -- after:  When using lambda as the parameter of array_sort, the lambda 
must return -1, 0 or 1 for less than, equal to or greater than, but it returns 
BOOLEAN
    ```
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../exprs/lambda_function/varray_sort_function.cpp |  63 +++++++++--
 .../lambda_function/array_map_function_test.cpp    | 119 +++++++++++++++++++++
 .../expressions/functions/scalar/ArraySort.java    |   6 ++
 .../nereids/rules/analysis/BindFunctionTest.java   |  13 +++
 .../test_array_sort_comparator_type.out            |  10 ++
 .../test_array_sort_comparator_type.groovy         |  86 +++++++++++++++
 6 files changed, 291 insertions(+), 6 deletions(-)

diff --git a/be/src/exprs/lambda_function/varray_sort_function.cpp 
b/be/src/exprs/lambda_function/varray_sort_function.cpp
index 978d037cbcf..14f7e4981dd 100644
--- a/be/src/exprs/lambda_function/varray_sort_function.cpp
+++ b/be/src/exprs/lambda_function/varray_sort_function.cpp
@@ -34,6 +34,9 @@
 #include "core/column/column_vector.h"
 #include "core/custom_allocator.h"
 #include "core/data_type/data_type.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/primitive_type.h"
+#include "core/value/large_int_value.h"
 #include "exec/common/util.hpp"
 #include "exprs/lambda_function/lambda_execution_context.h"
 #include "exprs/lambda_function/lambda_function.h"
@@ -95,6 +98,16 @@ public:
 
         DCHECK_EQ(children.size(), 2);
 
+        // The comparator must return -1, 0 or 1. FE rejects a non-integer 
comparator, but an older
+        // FE does not check it.
+        const auto& comparator_type = children[0]->get_child(0)->data_type();
+        const auto comparator_result_type = 
remove_nullable(comparator_type)->get_primitive_type();
+        if (!is_int(comparator_result_type)) {
+            return Status::InvalidArgument(
+                    "array_sort comparator must return -1, 0 or 1, but it 
returns {}",
+                    comparator_type->get_name());
+        }
+
         // 1. get data, we need to obtain this actual data and type.
         ColumnPtr column_ptr;
         RETURN_IF_ERROR(
@@ -145,11 +158,11 @@ public:
 
         /**
          *  suppose the data_type is nullable(int). The first two rows are the 
parameter columns, and the
-         *  last row is the result column(type: tinyint). every column's size 
is 1. the lambda_block is 
+         *  last row is the result column(an integer type). every column's 
size is 1. the lambda_block is
          *  row  data  nullmap    type
          *   0    10      0    nullable(int)
          *   1    20      1    nullable(int)
-         *   2   1/-1/0  ...     tinyint
+         *   2   1/-1/0  ...     integer
          *  The size of a column is always 1; we only need to use it to store 
the specific values ​​in the array for comparison.
         */
         Block lambda_block;
@@ -222,12 +235,11 @@ public:
                         ColumnPtr raw_res_col = 
lambda_block.get_by_position(lambda_res_id).column;
                         ColumnPtr full_res_col = 
raw_res_col->convert_to_full_column_if_const();
 
-                        // only -1, 0, 1
-                        long cmp =
-                                assert_cast<const 
ColumnInt8*>(full_res_col.get())->get_data()[0];
+                        bool is_less = 
_is_negative_comparator_result(*full_res_col,
+                                                                      
comparator_result_type);
                         lambda_block.erase_tail(lambda_result_base);
 
-                        return cmp < 0;
+                        return is_less;
                     };
 
                     // The comparator is user SQL and may violate strict weak 
ordering, or
@@ -268,6 +280,45 @@ public:
     }
 
 private:
+    // Returns whether the comparator result in the first row is -1. Any value 
other than -1, 0 or 1
+    // fails the query.
+    static bool _is_negative_comparator_result(const IColumn& column, 
PrimitiveType type) {
+        const IColumn* values = &column;
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(&column)) {
+            if (nullable->is_null_at(0)) {
+                throw Exception(Status::InvalidArgument(
+                        "array_sort comparator returns NULL, but it must 
return -1, 0 or 1"));
+            }
+            values = &nullable->get_nested_column();
+        }
+        Int128 result = 0;
+        switch (type) {
+        case TYPE_TINYINT:
+            result = assert_cast<const ColumnInt8&>(*values).get_element(0);
+            break;
+        case TYPE_SMALLINT:
+            result = assert_cast<const ColumnInt16&>(*values).get_element(0);
+            break;
+        case TYPE_INT:
+            result = assert_cast<const ColumnInt32&>(*values).get_element(0);
+            break;
+        case TYPE_BIGINT:
+            result = assert_cast<const ColumnInt64&>(*values).get_element(0);
+            break;
+        case TYPE_LARGEINT:
+            result = assert_cast<const ColumnInt128&>(*values).get_element(0);
+            break;
+        default:
+            DORIS_CHECK(false) << "array_sort comparator must return -1, 0 or 
1";
+        }
+        if (result != -1 && result != 0 && result != 1) {
+            throw Exception(Status::InvalidArgument(
+                    "array_sort comparator returns {}, but it must return -1, 
0 or 1",
+                    LargeIntValue::to_string(result)));
+        }
+        return result < 0;
+    }
+
     Status _set_comparator_argument_gap(const VExprSPtr& expr,
                                         const std::vector<std::string>* 
argument_names) const {
         if (expr->is_column_ref()) {
diff --git a/be/test/exprs/lambda_function/array_map_function_test.cpp 
b/be/test/exprs/lambda_function/array_map_function_test.cpp
index 2e815617707..137994e06bf 100644
--- a/be/test/exprs/lambda_function/array_map_function_test.cpp
+++ b/be/test/exprs/lambda_function/array_map_function_test.cpp
@@ -25,6 +25,7 @@
 #include <vector>
 
 #include "common/config.h"
+#include "common/exception.h"
 #include "core/assert_cast.h"
 #include "core/block/block.h"
 #include "core/column/column_array.h"
@@ -1810,6 +1811,124 @@ TEST(ArrayMapFunctionTest, 
NestedArraySortComparatorCapturingOuterArgumentReturn
     EXPECT_NE(status.to_string().find("captured column ref 'i'"), 
std::string::npos);
 }
 
+// Builds array_sort((x, y) -> body, arr).
+static VExprSPtr make_array_sort_call(const DataTypePtr& comparator_type, 
const VExprSPtr& body,
+                                      ColumnPtr array_column) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto root = VLambdaFunctionCallExpr::create_shared(
+            make_lambda_call_node(array_int_type, 2, "array_sort"));
+    auto comparator =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(comparator_type, {"x", 
"y"}));
+    comparator->add_child(body);
+    root->add_child(comparator);
+    root->add_child(
+            std::make_shared<MockColumnExpr>(std::move(array_column), 
array_int_type, "arr"));
+    return root;
+}
+
+TEST(ArrayMapFunctionTest, ArraySortComparatorReturningIntSorts) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+
+    // This mirrors array_sort((x, y) -> x - y, [1, 0]). The comparator 
returns -1 or 1 in an INT
+    // column, so the INT result must be read instead of a TINYINT one.
+    auto subtract = std::make_shared<MockSubtractExpr>(int_type);
+    subtract->add_child(VColumnRef::create_shared(make_column_ref_node(0, "x", 
int_type)));
+    subtract->add_child(VColumnRef::create_shared(make_column_ref_node(1, "y", 
int_type)));
+    auto root = make_array_sort_call(int_type, subtract, 
make_int_array_column({{1, 0}}));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    block.insert({make_int_column({0}), int_type, "ordinary_input"});
+
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 
block.rows(), result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+
+    const auto& values = get_int_array_values(result);
+    ASSERT_EQ(values.size(), 2);
+    EXPECT_EQ(values.get_element(0), 0);
+    EXPECT_EQ(values.get_element(1), 1);
+}
+
+TEST(ArrayMapFunctionTest, ArraySortComparatorReturningOutOfRangeReturnsError) 
{
+    auto int_type = std::make_shared<DataTypeInt32>();
+
+    // The comparator always returns 2, like array_sort((x, y) -> 2, arr). 
Only -1, 0 and 1 are
+    // allowed.
+    auto body = std::make_shared<MockColumnExpr>(ColumnInt32::create(1, 2), 
int_type, "two");
+    auto root = make_array_sort_call(int_type, body, 
make_int_array_column({{2, 1}}));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    block.insert({make_int_column({0}), int_type, "ordinary_input"});
+
+    ColumnPtr result;
+    try {
+        auto status = root->execute_column(&context, &block, nullptr, 
block.rows(), result);
+        FAIL() << "array_sort should fail, but it returns " << 
status.to_string();
+    } catch (const Exception& e) {
+        EXPECT_NE(std::string(e.what()).find(
+                          "array_sort comparator returns 2, but it must return 
-1, 0 or 1"),
+                  std::string::npos)
+                << e.what();
+    }
+}
+
+TEST(ArrayMapFunctionTest, ArraySortComparatorReturningNullReturnsError) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto nullable_int_type = make_nullable(int_type);
+
+    // The comparator always returns NULL, like array_sort((x, y) -> x - y, 
[2, NULL]).
+    auto null_result = ColumnNullable::create(ColumnInt32::create(1, 0), 
ColumnUInt8::create(1, 1));
+    auto body = std::make_shared<MockColumnExpr>(std::move(null_result), 
nullable_int_type,
+                                                 "null_result");
+    auto root = make_array_sort_call(nullable_int_type, body, 
make_int_array_column({{2, 1}}));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    block.insert({make_int_column({0}), int_type, "ordinary_input"});
+
+    ColumnPtr result;
+    try {
+        auto status = root->execute_column(&context, &block, nullptr, 
block.rows(), result);
+        FAIL() << "array_sort should fail, but it returns " << 
status.to_string();
+    } catch (const Exception& e) {
+        EXPECT_NE(std::string(e.what()).find("array_sort comparator returns 
NULL"),
+                  std::string::npos)
+                << e.what();
+    }
+}
+
+TEST(ArrayMapFunctionTest, ArraySortComparatorReturningBooleanReturnsError) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto bool_type = std::make_shared<DataTypeUInt8>();
+
+    // An older FE may send array_sort((x, y) -> x < y, arr), whose comparator 
returns BOOLEAN.
+    auto body =
+            std::make_shared<MockColumnExpr>(ColumnUInt8::create(1, 1), 
bool_type, "bool_result");
+    auto root = make_array_sort_call(bool_type, body, 
make_int_array_column({{2, 1}}));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    block.insert({make_int_column({0}), int_type, "ordinary_input"});
+
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 
block.rows(), result);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find("array_sort comparator must return -1, 0 
or 1"),
+              std::string::npos)
+            << status.to_string();
+}
+
 TEST(ArrayMapFunctionTest,
      
NestedArraySortComparatorNestedLambdaCapturingComparatorArgumentReturnsError) {
     auto int_type = std::make_shared<DataTypeInt32>();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArraySort.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArraySort.java
index 0af1201c745..2bfcc6ea6a9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArraySort.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArraySort.java
@@ -79,6 +79,12 @@ public class ArraySort extends ScalarFunction
             if 
(!ArrayFunctionUtils.isSupportedByArraySortLambdaFunction(sourceType.getItemType()))
 {
                 throw new AnalysisException("array_sort does not support 
types: " + sourceType.toSql());
             }
+            // The comparator returns -1, 0 or 1 for less than, equal to or 
greater than.
+            if (!lambda.getRetType().isIntegralType()) {
+                throw new AnalysisException("When using lambda as the 
parameter of array_sort, the lambda must"
+                        + " return -1, 0 or 1 for less than, equal to or 
greater than, but it returns "
+                        + lambda.getRetType().toSql());
+            }
         }
     }
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindFunctionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindFunctionTest.java
index 3697af015dc..2761ebe6e26 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindFunctionTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/BindFunctionTest.java
@@ -139,6 +139,19 @@ public class BindFunctionTest extends TestWithFeService 
implements MemoPatternMa
                 exception.getMessage());
     }
 
+    @Test
+    void testArraySortComparatorMustReturnInteger() {
+        // The comparator returns -1, 0 or 1 for less than, equal to or 
greater than.
+        String sql = "SELECT array_sort((x, y) -> x < y, arr2) FROM t_arr";
+        AnalysisException exception = 
Assertions.assertThrows(AnalysisException.class,
+                () -> PlanChecker.from(connectContext).analyze(sql));
+        Assertions.assertTrue(exception.getMessage().contains("the lambda must 
return -1, 0 or 1 for less than,"
+                + " equal to or greater than, but it returns BOOLEAN"), 
exception.getMessage());
+
+        // Any integer type passes analysis. BE checks that the value is -1, 0 
or 1.
+        PlanChecker.from(connectContext).analyze("SELECT array_sort((x, y) -> 
x - y, arr2) FROM t_arr");
+    }
+
     private static Lambda innermostLambda(Plan plan) {
         List<Lambda> innermostLambdas = plan.<Plan>collect(node -> 
true).stream()
                 .flatMap(node -> node.getExpressions().stream())
diff --git 
a/regression-test/data/query_p0/sql_functions/array_functions/test_array_sort_comparator_type.out
 
b/regression-test/data/query_p0/sql_functions/array_functions/test_array_sort_comparator_type.out
new file mode 100644
index 00000000000..24ee4d9c5b0
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/array_functions/test_array_sort_comparator_type.out
@@ -0,0 +1,10 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !integer_comparator --
+[1, 2, 3]      [3, 2, 1]       [0, 128, 256]   [1, 2, 3]       [1, 2, 3]       
[1, 2, 3]
+
+-- !integer_comparator_table --
+1      [1, 2, 3]       [9000000000, 0, -9000000000]
+2      [0, 128, 256]   [2, 1]
+3      []      []
+4      \N      \N
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/array_functions/test_array_sort_comparator_type.groovy
 
b/regression-test/suites/query_p0/sql_functions/array_functions/test_array_sort_comparator_type.groovy
new file mode 100644
index 00000000000..5b771370d95
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/array_functions/test_array_sort_comparator_type.groovy
@@ -0,0 +1,86 @@
+// 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.
+
+suite("test_array_sort_comparator_type") {
+    // The comparator of array_sort returns -1, 0 or 1 for less than, equal to 
or greater than.
+    // A comparator that does not return an integer is rejected when the query 
is analyzed.
+    test {
+        sql "SELECT array_sort((x, y) -> x < y, [3, 2, 1])"
+        exception "the lambda must return -1, 0 or 1 for less than, equal to 
or greater than, but it returns BOOLEAN"
+    }
+    test {
+        sql "SELECT array_sort((x, y) -> x - y, [3.5, 2.5, 1.5])"
+        exception "the lambda must return -1, 0 or 1"
+    }
+    test {
+        sql "SELECT array_sort((x, y) -> concat(x, y), ['b', 'a'])"
+        exception "the lambda must return -1, 0 or 1"
+    }
+
+    // Any integer type works if the comparator returns -1, 0 or 1. The 
elements of an array are
+    // nullable, so sign(x - y) returns a nullable TINYINT.
+    order_qt_integer_comparator """
+        SELECT array_sort((x, y) -> sign(x - y), [3, 1, 2]),
+               array_sort((x, y) -> sign(y - x), [3, 1, 2]),
+               array_sort((x, y) -> sign(x - y), [256, 128, 0]),
+               array_sort((x, y) -> cast(sign(x - y) AS INT), [3, 1, 2]),
+               array_sort((x, y) -> cast(sign(x - y) AS BIGINT), [3, 1, 2]),
+               array_sort((x, y) -> cast(sign(x - y) AS LARGEINT), [3, 1, 2])
+    """
+
+    // A comparator that returns another value or NULL fails the query.
+    test {
+        sql "SELECT array_sort((x, y) -> 5, [3, 1, 2])"
+        exception "array_sort comparator returns 5, but it must return -1, 0 
or 1"
+    }
+    test {
+        sql "SELECT array_sort((x, y) -> x - y, [30, 10, 20])"
+        exception "but it must return -1, 0 or 1"
+    }
+    test {
+        sql "SELECT array_sort((x, y) -> sign(x - y), [3, NULL, 1])"
+        exception "array_sort comparator returns NULL"
+    }
+
+    sql "DROP TABLE IF EXISTS test_array_sort_comparator_type"
+    sql """
+        CREATE TABLE test_array_sort_comparator_type (
+            id INT,
+            a ARRAY<INT>,
+            b ARRAY<BIGINT>
+        ) DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """
+        INSERT INTO test_array_sort_comparator_type VALUES
+            (1, [3, 1, 2], [9000000000, -9000000000, 0]),
+            (2, [256, 128, 0], [1, 2]),
+            (3, [], []),
+            (4, NULL, NULL)
+    """
+    // The elements of a table column are nullable, so the comparator result 
is nullable too. It
+    // works as long as the comparator does not return NULL.
+    order_qt_integer_comparator_table """
+        SELECT id, array_sort((x, y) -> sign(x - y), a), array_sort((x, y) -> 
sign(y - x), b)
+        FROM test_array_sort_comparator_type
+    """
+    test {
+        sql "SELECT id, array_sort((x, y) -> x - y, a) FROM 
test_array_sort_comparator_type"
+        exception "but it must return -1, 0 or 1"
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to