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

HappenLee 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 f1a168db108 [fix](be) Fix nested array_map lambda captures (#64563)
f1a168db108 is described below

commit f1a168db108fbce8bbbb157333754b432df99e5a
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Jul 16 13:26:37 2026 +0800

    [fix](be) Fix nested array_map lambda captures (#64563)
    
    Problem Summary: Nested `array_map` expressions can capture variables
    from an outer lambda while also defining their own lambda arguments. The
    BE previously assigned lambda argument column offsets by recursively
    mutating every `VColumnRef` under the outer lambda. This could make an
    inner lambda argument reuse the outer lambda block offset and read the
    wrong column during `INSERT ... SELECT`, causing a BE crash. This change
    serializes lambda argument metadata to BE, preserves captured input
    columns in lambda blocks, and resolves lambda arguments by the current
    runtime lambda scope while respecting nested-lambda shadowing.
    
    `array_sort` comparator arguments are intentionally position-based
    because FE may distinguish the two comparator arguments only by column
    id. A comparator that captures an outer lambda argument cannot be
    represented in the current two-column comparator block and could
    otherwise be silently resolved as one of the comparator arguments. This
    PR now returns a clear error for unsupported captured column or slot
    references in `array_sort` comparators.
    
    A follow-up refactor moves `LambdaExecutionContext` into
    `be/src/exprs/lambda_function/lambda_execution_context.h`.
    `VExprContext` now keeps the lambda execution context as a private
    implementation detail and includes the concrete definition only from
    `vexpr_context.cpp`, while lambda functions and `VColumnRef` include the
    dedicated lambda header where they need the concrete type.
    
    ### Release note
    
    Fix nested `array_map` lambda captures and reject unsupported captured
    references in `array_sort` comparators.
    
    ### Check List (For Author)
    
    - Test:
    - Unit Test: `./run-be-ut.sh --run
    
--filter=ArrayMapFunctionTest.NamedLambdaWithFewerArgumentsThanArraysUsesDeclaredBindings:ArrayMapFunctionTest.*:VColumnRefTest.*`
    - Unit Test: `./run-be-ut.sh --run
    
--filter=ArrayMapFunctionTest.NestedArraySortComparatorCapturingOuterArgumentReturnsError:ArrayMapFunctionTest.NestedArraySortInsideArrayMapSkipsArrayMapArgumentInference`
        - Unit Test: `./run-be-ut.sh --run --filter=ArrayMapFunctionTest.*`
        - Build: `./build.sh --be --fe`
        - Build: `./build.sh --be`
    - Regression test: `./run-regression-test.sh --run -d
    query_p0/sql_functions/array_functions -s test_nested_array_map_insert
    -forceGenOut`
    - Regression test: `./run-regression-test.sh --run -d
    query_p0/sql_functions/array_functions -s test_nested_array_map_insert`
    - Regression test: `./run-regression-test.sh --run -d
    query_p0/sql_functions/array_functions -s test_nested_array_map`
    - Regression test: `./run-regression-test.sh --run -d
    query_p0/virtual_slot_ref -s fix_array_type_and_lambda_func`
    - Regression test: `./run-regression-test.sh --run -d
    query_p0/virtual_slot_ref`
    - Regression test: `./run-regression-test.sh --run -d
    query_p0/sql_functions/array_functions`
    - Format check: `./build-support/clang-format.sh`;
    `./build-support/check-format.sh`; `git diff --check`
    - Static analysis: attempted `./build-support/run-clang-tidy.sh
    --build-dir be/ut_build_ASAN --base HEAD`, but local clang-tidy analysis
    was blocked by existing/local diagnostics including missing `stddef.h`,
    unmatched `NOLINTEND` in `be/src/core/types.h`, and pre-existing
    function-size/cognitive-complexity diagnostics.
    - Static analysis: attempted `build-support/run-clang-tidy.sh
    --build-dir be/build_Release`, but it is still blocked by the same
    existing/toolchain diagnostics.
    - Behavior changed: Yes. Nested lambda arguments are resolved by
    FE-provided lambda metadata when available; old nested lambda plans
    without metadata fail fast; unsupported captured references in
    `array_sort` comparators now return an error instead of being silently
    position-bound.
    - Does this need documentation: No
---
 .../lambda_function/lambda_execution_context.h     |  116 ++
 be/src/exprs/lambda_function/lambda_function.h     |    2 +-
 .../exprs/lambda_function/varray_map_function.cpp  |  245 ++--
 .../exprs/lambda_function/varray_sort_function.cpp |  179 ++-
 be/src/exprs/vcolumn_ref.h                         |   60 +-
 be/src/exprs/vexpr_context.cpp                     |   12 +
 be/src/exprs/vexpr_context.h                       |   39 +-
 be/src/exprs/vlambda_function_call_expr.h          |    2 +-
 be/src/exprs/vlambda_function_expr.h               |   14 +-
 be/src/exprs/vslot_ref.h                           |    1 +
 .../lambda_function/array_map_function_test.cpp    | 1409 ++++++++++++++++++++
 be/test/exprs/vcolumn_ref_test.cpp                 |  148 ++
 .../apache/doris/analysis/ExprToThriftVisitor.java |    1 +
 .../doris/analysis/ExprToThriftBehaviorTest.java   |   16 +
 gensrc/thrift/Exprs.thrift                         |    4 +
 .../array_functions/test_nested_array_map.out      |   13 +
 .../array_functions/test_nested_array_map.groovy   |   90 ++
 17 files changed, 2226 insertions(+), 125 deletions(-)

diff --git a/be/src/exprs/lambda_function/lambda_execution_context.h 
b/be/src/exprs/lambda_function/lambda_execution_context.h
new file mode 100644
index 00000000000..6068c3e47d1
--- /dev/null
+++ b/be/src/exprs/lambda_function/lambda_execution_context.h
@@ -0,0 +1,116 @@
+// 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.
+
+#pragma once
+
+#include <glog/logging.h>
+
+#include <ranges>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace doris {
+
+class LambdaExecutionContext {
+public:
+    struct Binding {
+        std::string name;
+        int column_position = -1;
+    };
+
+    struct Frame {
+        bool bind_by_name = true;
+        bool parent_bindings_visible = true;
+        std::vector<Binding> argument_bindings;
+    };
+
+    struct ResolveResult {
+        bool searched_named_scope = false;
+        bool found = false;
+        int column_position = -1;
+    };
+
+    class FrameGuard {
+    public:
+        FrameGuard(LambdaExecutionContext& context, Frame frame) : 
_context(&context) {
+            _context->push_frame(std::move(frame));
+        }
+
+        FrameGuard(FrameGuard&& other) = delete;
+        FrameGuard& operator=(FrameGuard&& other) = delete;
+        FrameGuard(const FrameGuard&) = delete;
+        FrameGuard& operator=(const FrameGuard&) = delete;
+
+        ~FrameGuard() { release(); }
+
+    private:
+        void release() {
+            if (_context != nullptr) {
+                _context->pop_frame();
+                _context = nullptr;
+            }
+        }
+
+        LambdaExecutionContext* _context;
+    };
+
+    void push_frame(Frame frame) { _frames.push_back(std::move(frame)); }
+
+    void pop_frame() {
+        DCHECK(!_frames.empty());
+        _frames.pop_back();
+    }
+
+    ResolveResult resolve_column_position(const std::string& name) const {
+        ResolveResult result;
+        for (const auto& frame : std::ranges::reverse_view(_frames)) {
+            result.searched_named_scope |= frame.bind_by_name;
+            for (const auto& argument_binding :
+                 std::ranges::reverse_view(frame.argument_bindings)) {
+                if (argument_binding.name == name) {
+                    result.found = true;
+                    result.column_position = argument_binding.column_position;
+                    return result;
+                }
+            }
+            if (!frame.parent_bindings_visible) {
+                break;
+            }
+        }
+        return result;
+    }
+
+    void collect_visible_binding_column_positions(std::set<int>& 
column_positions) const {
+        for (const auto& _frame : std::ranges::reverse_view(_frames)) {
+            for (const auto& binding : _frame.argument_bindings) {
+                if (binding.column_position >= 0) {
+                    column_positions.insert(binding.column_position);
+                }
+            }
+            if (!_frame.parent_bindings_visible) {
+                break;
+            }
+        }
+    }
+
+private:
+    std::vector<Frame> _frames;
+};
+
+} // namespace doris
diff --git a/be/src/exprs/lambda_function/lambda_function.h 
b/be/src/exprs/lambda_function/lambda_function.h
index 451b61569e9..31976617a54 100644
--- a/be/src/exprs/lambda_function/lambda_function.h
+++ b/be/src/exprs/lambda_function/lambda_function.h
@@ -31,7 +31,7 @@ public:
 
     virtual std::string get_name() const = 0;
 
-    virtual doris::Status prepare(RuntimeState* state) {
+    virtual doris::Status prepare(RuntimeState* state, const VExprSPtrs& 
children) {
         batch_size = state->batch_size();
         return Status::OK();
     }
diff --git a/be/src/exprs/lambda_function/varray_map_function.cpp 
b/be/src/exprs/lambda_function/varray_map_function.cpp
index db82c465000..1c22c23b7c8 100644
--- a/be/src/exprs/lambda_function/varray_map_function.cpp
+++ b/be/src/exprs/lambda_function/varray_map_function.cpp
@@ -17,9 +17,11 @@
 
 #include <algorithm>
 #include <memory>
+#include <set>
 #include <string>
 #include <vector>
 
+#include "common/check.h"
 #include "common/status.h"
 #include "core/assert_cast.h"
 #include "core/block/block.h"
@@ -28,6 +30,7 @@
 #include "core/block/columns_with_type_and_name.h"
 #include "core/column/column.h"
 #include "core/column/column_array.h"
+#include "core/column/column_nothing.h"
 #include "core/column/column_nullable.h"
 #include "core/column/column_vector.h"
 #include "core/data_type/data_type.h"
@@ -36,18 +39,17 @@
 #include "core/data_type/data_type_number.h"
 #include "exec/common/util.hpp"
 #include "exprs/aggregate/aggregate_function.h"
+#include "exprs/lambda_function/lambda_execution_context.h"
 #include "exprs/lambda_function/lambda_function.h"
 #include "exprs/lambda_function/lambda_function_factory.h"
 #include "exprs/vcolumn_ref.h"
-#include "exprs/vslot_ref.h"
+#include "exprs/vexpr_context.h"
+#include "exprs/vlambda_function_expr.h"
 
 namespace doris {
-class VExprContext;
 
 // extend a block with all required parameters
 struct LambdaArgs {
-    // the lambda function need the column ids of all the slots
-    std::vector<int> output_slot_ref_indexs;
     // which line is extended to the original block
     int64_t current_row_idx = 0;
     // when a block is filled, the array may be truncated, recording where it 
was truncated
@@ -76,34 +78,18 @@ public:
 
     std::string get_name() const override { return name; }
 
+    Status prepare(RuntimeState* state, const VExprSPtrs& children) override {
+        RETURN_IF_ERROR(LambdaFunction::prepare(state, children));
+        DCHECK_GE(children.size(), 2);
+
+        return _prepare_lambda_argument_binding(children[0], children.size() - 
1,
+                                                _lambda_argument_binding);
+    }
+
     Status execute(VExprContext* context, const Block* block, const Selector* 
expr_selector,
                    size_t count, ColumnPtr& result_column, const DataTypePtr& 
result_type,
                    const VExprSPtrs& children) const override {
         LambdaArgs args_info;
-        // collect used slot ref in lambda function body
-        std::vector<int>& output_slot_ref_indexs = 
args_info.output_slot_ref_indexs;
-        _collect_slot_ref_column_id(children[0], output_slot_ref_indexs);
-
-        int gap = 0;
-        if (!output_slot_ref_indexs.empty()) {
-            auto max_id = std::ranges::max_element(output_slot_ref_indexs);
-            gap = *max_id + 1;
-            _set_column_ref_column_id(children[0], gap);
-        }
-
-        std::vector<std::string> names(gap);
-        DataTypes data_types(gap);
-
-        for (int i = 0; i < gap; ++i) {
-            if (_contains_column_id(output_slot_ref_indexs, i)) {
-                names[i] = block->get_by_position(i).name;
-                data_types[i] = block->get_by_position(i).type;
-            } else {
-                // padding some mock data to hold the position, like call 
block#rows function need
-                names[i] = "temp";
-                data_types[i] = std::make_shared<DataTypeUInt8>();
-            }
-        }
 
         ///* array_map(lambda,arg1,arg2,.....) *///
         //1. child[1:end]->execute(src_block)
@@ -126,6 +112,7 @@ public:
         ColumnPtr first_array_offsets = nullptr;
         //2. get the result column from executed expr, and the needed is 
nested column of array
         std::vector<ColumnPtr> lambda_datas(arguments.size());
+        DataTypes lambda_argument_types(arguments.size());
 
         for (int i = 0; i < arguments.size(); ++i) {
             const auto& array_column_type_name = arguments[i];
@@ -153,7 +140,6 @@ public:
 
             // here is the array column
             const auto& col_array = assert_cast<const 
ColumnArray&>(*column_array);
-            const auto& col_type = assert_cast<const 
DataTypeArray&>(*type_array);
 
             if (i == 0) {
                 nested_array_column_rows = col_array.get_data_ptr()->size();
@@ -180,15 +166,71 @@ public:
                 }
             }
             lambda_datas[i] = col_array.get_data_ptr();
-            names.push_back("R" + array_column_type_name.name);
-            data_types.push_back(col_type.get_nested_type());
+            const auto& col_type = assert_cast<const 
DataTypeArray&>(*type_array);
+            lambda_argument_types[i] = col_type.get_nested_type();
+        }
+        std::set<int> required_input_column_ids;
+        children[0]->collect_slot_column_ids(required_input_column_ids);
+        
context->lambda_execution_context().collect_visible_binding_column_positions(
+                required_input_column_ids);
+        const int lambda_argument_base =
+                required_input_column_ids.empty() ? 0 : 
*required_input_column_ids.rbegin() + 1;
+        if (!_lambda_argument_binding.bind_by_name) {
+            RETURN_IF_ERROR(
+                    _set_legacy_lambda_argument_gap(children[0]->get_child(0), 
lambda_argument_base,
+                                                    
_lambda_argument_binding.argument_size));
+        }
+        std::vector<std::string> names(lambda_argument_base);
+        DataTypes data_types(lambda_argument_base);
+        std::vector<bool> materialized_input_columns(lambda_argument_base, 
false);
+        names.reserve(lambda_argument_base + arguments.size());
+        data_types.reserve(lambda_argument_base + arguments.size());
+        for (int column_id : required_input_column_ids) {
+            if (column_id < 0 || static_cast<size_t>(column_id) >= 
block->columns()) {
+                return Status::InternalError(
+                        "array_map lambda input column id {} is outside input 
block, block={}",
+                        column_id, block->dump_structure());
+            }
+            materialized_input_columns[column_id] = true;
+            names[column_id] = block->get_by_position(column_id).name;
+            data_types[column_id] = block->get_by_position(column_id).type;
+        }
+        for (int i = 0; i < lambda_argument_base; ++i) {
+            if (!materialized_input_columns[i]) {
+                // Keep sparse input positions stable for SlotRef/parent 
lambda bindings without
+                // materializing unrelated wide-table columns into every 
lambda batch.
+                names[i] = "temp";
+                data_types[i] = std::make_shared<DataTypeUInt8>();
+            }
+        }
+        for (int i = 0; i < arguments.size(); ++i) {
+            const auto& array_column_type_name = arguments[i];
+            if (_lambda_argument_binding.bind_by_name &&
+                i < _lambda_argument_binding.names.size()) {
+                names.push_back(_lambda_argument_binding.names[i]);
+            } else {
+                names.push_back("R" + array_column_type_name.name);
+            }
+            data_types.push_back(lambda_argument_types[i]);
+        }
+
+        LambdaExecutionContext::Frame lambda_frame;
+        lambda_frame.bind_by_name = _lambda_argument_binding.bind_by_name;
+        lambda_frame.parent_bindings_visible = true;
+        for (int i = 0; i < _lambda_argument_binding.argument_size; ++i) {
+            const int column_position = lambda_argument_base + i;
+            if (_lambda_argument_binding.bind_by_name) {
+                lambda_frame.argument_bindings.push_back(
+                        {_lambda_argument_binding.names[i], column_position});
+            }
         }
+        LambdaExecutionContext::FrameGuard 
lambda_frame_guard(context->lambda_execution_context(),
+                                                              
std::move(lambda_frame));
 
         // if column_array is NULL, we know the array_data_column will not 
write any data,
         // so the column is empty. eg : (x) -> concat('|',x + "1"). if still 
execute the lambda function, will cause the bolck rows are not equal
         // the x column is empty, but "|" is const literal, size of column is 
1, so the block rows is 1, but the x column is empty, will be coredump.
-        if (std::any_of(lambda_datas.begin(), lambda_datas.end(),
-                        [](const auto& v) { return v->empty(); })) {
+        if (std::ranges::any_of(lambda_datas, [](const auto& v) { return 
v->empty(); })) {
             DataTypePtr nested_type;
             bool is_nullable = result_type->is_nullable();
             if (is_nullable) {
@@ -231,33 +273,26 @@ public:
                 if (mem_reuse) {
                     columns[i] = 
lambda_block.get_by_position(i).column->assert_mutable();
                 } else {
-                    if (_contains_column_id(output_slot_ref_indexs, i) || i >= 
gap) {
-                        // TODO: maybe could create const column, so not 
insert_many_from when extand data
-                        // but now here handle batch_size of array nested data 
every time, so maybe have different rows
-                        columns[i] = data_types[i]->create_column();
-                    } else {
-                        columns[i] = data_types[i]
-                                             
->create_column_const_with_default_value(0)
-                                             ->assert_mutable();
-                    }
+                    columns[i] = data_types[i]->create_column();
                 }
             }
             // batch_size of array nested data every time inorder to avoid 
memory overflow
-            while (columns[gap]->size() < batch_size) {
-                long max_step = batch_size - columns[gap]->size();
+            while (columns[lambda_argument_base]->size() < batch_size) {
+                long max_step = batch_size - 
columns[lambda_argument_base]->size();
                 long current_step = std::min(
                         max_step, (long)(args_info.cur_size - 
args_info.current_offset_in_array));
                 size_t pos = args_info.array_start + 
args_info.current_offset_in_array;
                 for (int i = 0; i < arguments.size() && current_step > 0; ++i) 
{
-                    columns[gap + i]->insert_range_from(*lambda_datas[i], pos, 
current_step);
+                    columns[lambda_argument_base + 
i]->insert_range_from(*lambda_datas[i], pos,
+                                                                         
current_step);
                 }
                 args_info.current_offset_in_array += current_step;
                 args_info.current_repeat_times += current_step;
                 if (args_info.current_offset_in_array >= args_info.cur_size) {
                     args_info.current_row_eos = true;
                 }
-                _extend_data(columns, block, args_info.current_repeat_times, 
gap,
-                             args_info.current_row_idx, 
output_slot_ref_indexs);
+                _repeat_input_columns(columns, block, 
args_info.current_repeat_times,
+                                      materialized_input_columns, 
args_info.current_row_idx);
                 args_info.current_repeat_times = 0;
                 if (args_info.current_row_eos) {
                     //current row is end of array, move to next row
@@ -329,52 +364,104 @@ public:
     }
 
 private:
-    bool _contains_column_id(const std::vector<int>& output_slot_ref_indexs, 
int id) const {
-        const auto it = std::find(output_slot_ref_indexs.begin(), 
output_slot_ref_indexs.end(), id);
-        return it != output_slot_ref_indexs.end();
+    struct LambdaArgumentBinding {
+        bool bind_by_name = true;
+        size_t argument_size = 0;
+        std::vector<std::string> names;
+    };
+
+    Status _prepare_lambda_argument_binding(const VExprSPtr& expr, size_t 
expected_argument_size,
+                                            LambdaArgumentBinding& 
argument_binding) const {
+        DORIS_CHECK_EQ(expr->node_type(), TExprNodeType::LAMBDA_FUNCTION_EXPR);
+        const auto* lambda_expr = assert_cast<const 
VLambdaFunctionExpr*>(expr.get());
+
+        argument_binding.argument_size = 0;
+        argument_binding.names.clear();
+        argument_binding.bind_by_name = lambda_expr->has_argument_names();
+
+        if (!argument_binding.bind_by_name) {
+            if (_contains_nested_lambda_call(expr->get_child(0))) {
+                return Status::InternalError(
+                        "Cannot resolve nested lambda argument without lambda 
metadata");
+            }
+            argument_binding.argument_size = expected_argument_size;
+            argument_binding.names.resize(expected_argument_size);
+            return Status::OK();
+        }
+
+        argument_binding.names = lambda_expr->argument_names();
+        if (argument_binding.names.size() > expected_argument_size) {
+            return Status::InternalError(
+                    "lambda argument metadata size exceeds parameter size, 
maximum={}, actual={}",
+                    expected_argument_size, argument_binding.names.size());
+        }
+        argument_binding.argument_size = argument_binding.names.size();
+        if (std::ranges::any_of(argument_binding.names,
+                                [](const auto& argument_name) { return 
argument_name.empty(); })) {
+            return Status::InternalError("lambda argument metadata contains 
empty name");
+        }
+        return Status::OK();
     }
 
-    void _set_column_ref_column_id(VExprSPtr expr, int gap) const {
-        for (const auto& child : expr->children()) {
-            if (child->is_column_ref()) {
-                auto* ref = static_cast<VColumnRef*>(child.get());
-                ref->set_gap(gap);
-            } else {
-                _set_column_ref_column_id(child, gap);
+    Status _set_legacy_lambda_argument_gap(const VExprSPtr& expr, int 
lambda_argument_base,
+                                           size_t argument_size) const {
+        if (expr->is_column_ref()) {
+            auto* ref = static_cast<VColumnRef*>(expr.get());
+            DORIS_CHECK_GE(ref->column_id(), 0);
+            DORIS_CHECK_LT(static_cast<size_t>(ref->column_id()), 
argument_size);
+            const int argument_index = ref->column_id();
+            ref->set_gap(lambda_argument_base + argument_index - 
ref->column_id());
+        } else {
+            for (const auto& child : expr->children()) {
+                RETURN_IF_ERROR(_set_legacy_lambda_argument_gap(child, 
lambda_argument_base,
+                                                                
argument_size));
             }
         }
+        return Status::OK();
     }
 
-    void _collect_slot_ref_column_id(VExprSPtr expr,
-                                     std::vector<int>& output_slot_ref_indexs) 
const {
-        for (const auto& child : expr->children()) {
-            if (child->is_slot_ref()) {
-                const auto* ref = static_cast<VSlotRef*>(child.get());
-                output_slot_ref_indexs.push_back(ref->column_id());
-            } else {
-                _collect_slot_ref_column_id(child, output_slot_ref_indexs);
-            }
+    bool _is_lambda_call_with_lambda_expr(const VExprSPtr& expr) const {
+        return expr->node_type() == TExprNodeType::LAMBDA_FUNCTION_CALL_EXPR &&
+               !expr->children().empty() &&
+               expr->children()[0]->node_type() == 
TExprNodeType::LAMBDA_FUNCTION_EXPR;
+    }
+
+    bool _contains_nested_lambda_call(const VExprSPtr& expr) const {
+        if (_is_lambda_call_with_lambda_expr(expr)) {
+            return true;
         }
+        return std::ranges::any_of(expr->children(), [this](const auto& child) 
{
+            return _contains_nested_lambda_call(child);
+        });
     }
 
-    void _extend_data(std::vector<MutableColumnPtr>& columns, const Block* 
block,
-                      int current_repeat_times, int size, int64_t 
current_row_idx,
-                      const std::vector<int>& output_slot_ref_indexs) const {
-        if (!current_repeat_times || !size) {
+    void _repeat_input_columns(std::vector<MutableColumnPtr>& columns, const 
Block* block,
+                               int repeat_times,
+                               const std::vector<bool>& 
materialized_input_columns,
+                               int64_t row_idx) const {
+        if (!repeat_times || materialized_input_columns.empty()) {
             return;
         }
-        for (int i = 0; i < size; i++) {
-            if (_contains_column_id(output_slot_ref_indexs, i)) {
-                auto src_column =
-                        
block->get_by_position(i).column->convert_to_full_column_if_const();
-                columns[i]->insert_many_from(*src_column, current_row_idx, 
current_repeat_times);
-            } else {
-                // must be column const
-                DCHECK(is_column_const(*columns[i]));
-                columns[i]->resize(columns[i]->size() + current_repeat_times);
+        for (size_t i = 0; i < materialized_input_columns.size(); i++) {
+            if (!materialized_input_columns[i]) {
+                columns[i]->resize(columns[i]->size() + repeat_times);
+                continue;
             }
+            DORIS_CHECK(block != nullptr);
+            auto src_column = 
block->get_by_position(i).column->convert_to_full_column_if_const();
+            if (check_and_get_column<ColumnNothing>(src_column.get())) {
+                // A ColumnNothing in the outer block is a placeholder for an 
unmaterialized
+                // virtual column. Keep it as a placeholder in the lambda 
block as well, so
+                // VirtualSlotRef can still materialize it lazily if the 
lambda body reads it.
+                if (!check_and_get_column<ColumnNothing>(columns[i].get())) {
+                    columns[i] = ColumnNothing::create(columns[i]->size());
+                }
+            }
+            columns[i]->insert_many_from(*src_column, row_idx, repeat_times);
         }
     }
+
+    LambdaArgumentBinding _lambda_argument_binding;
 };
 
 void register_function_array_map(doris::LambdaFunctionFactory& factory) {
diff --git a/be/src/exprs/lambda_function/varray_sort_function.cpp 
b/be/src/exprs/lambda_function/varray_sort_function.cpp
index 6d146094003..dfd2ed1a8ae 100644
--- a/be/src/exprs/lambda_function/varray_sort_function.cpp
+++ b/be/src/exprs/lambda_function/varray_sort_function.cpp
@@ -17,6 +17,12 @@
 
 #include <glog/logging.h>
 
+#include <algorithm>
+#include <string>
+#include <type_traits>
+#include <variant>
+#include <vector>
+
 #include "common/status.h"
 #include "core/assert_cast.h"
 #include "core/block/block.h"
@@ -28,14 +34,16 @@
 #include "core/column/column_vector.h"
 #include "core/data_type/data_type.h"
 #include "exec/common/util.hpp"
+#include "exprs/lambda_function/lambda_execution_context.h"
 #include "exprs/lambda_function/lambda_function.h"
 #include "exprs/lambda_function/lambda_function_factory.h"
+#include "exprs/vcolumn_ref.h"
 #include "exprs/vexpr.h"
+#include "exprs/vexpr_context.h"
+#include "exprs/vlambda_function_expr.h"
 
 namespace doris {
 
-class VExprContext;
-
 using ConstColumnVariant =
         std::variant<const ColumnUInt8*, const ColumnInt8*, const 
ColumnInt16*, const ColumnInt32*,
                      const ColumnInt64*, const ColumnInt128*, const 
ColumnFloat32*,
@@ -66,6 +74,17 @@ public:
 
     std::string get_name() const override { return name; }
 
+    Status prepare(RuntimeState* state, const VExprSPtrs& children) override {
+        RETURN_IF_ERROR(LambdaFunction::prepare(state, children));
+        DCHECK_EQ(children.size(), 2);
+        DORIS_CHECK_EQ(children[0]->node_type(), 
TExprNodeType::LAMBDA_FUNCTION_EXPR);
+        const auto* lambda_expr = assert_cast<const 
VLambdaFunctionExpr*>(children[0].get());
+        const std::vector<std::string>* argument_names =
+                lambda_expr->has_argument_names() ? 
&lambda_expr->argument_names() : nullptr;
+        
RETURN_IF_ERROR(_set_comparator_argument_gap(children[0]->get_child(0), 
argument_names));
+        return Status::OK();
+    }
+
     Status execute(VExprContext* context, const Block* block, const Selector* 
expr_selector,
                    size_t count, ColumnPtr& result_column, const DataTypePtr& 
result_type,
                    const VExprSPtrs& children) const override {
@@ -129,9 +148,9 @@ public:
          *   1    20      1    nullable(int)
          *   2   1/-1/0  ...     tinyint
          *  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;
-        for (int i = 0; i <= 2; i++) {
+        for (int i = 0; i < 2; i++) {
             
lambda_block.insert(ColumnWithTypeAndName(nested_nullable_column.clone_empty(),
                                                       
col_type.get_nested_type(), "temp"));
         }
@@ -148,7 +167,14 @@ public:
             temp_nullmap_data[i]->resize(1);
         };
 
-        int lambda_res_id = 2;
+        // array_sort's comparator arguments are represented by ColumnRef 
column ids 0 and 1.
+        // They are position-based instead of name-based because FE may reuse 
the first
+        // argument name for the second comparator ColumnRef and distinguish 
them only by id.
+        LambdaExecutionContext::Frame lambda_frame;
+        lambda_frame.bind_by_name = false;
+        lambda_frame.parent_bindings_visible = false;
+        LambdaExecutionContext::FrameGuard 
lambda_frame_guard(context->lambda_execution_context(),
+                                                              
std::move(lambda_frame));
 
         // 3. sort array by executing lambda function
         // During the sorting process, the parameter columns of lambda_block 
are first populated using prepare_lambda_input,
@@ -175,12 +201,14 @@ public:
                         }
                     };
 
+                    const int lambda_result_base = 
static_cast<int>(lambda_block.columns());
                     for (int row = 0; row < input_rows; ++row) {
                         auto start = off_data[row - 1];
                         auto end = off_data[row];
                         std::sort(&permutation[start], &permutation[end], 
[&](size_t i, size_t j) {
                             prepare_lambda_input(i, 0);
                             prepare_lambda_input(j, 1);
+                            int lambda_res_id = lambda_result_base;
                             auto status =
                                     children[0]->execute(context, 
&lambda_block, &lambda_res_id);
                             if (!status.ok()) [[unlikely]] {
@@ -197,6 +225,7 @@ public:
                             // only -1, 0, 1
                             long cmp = assert_cast<const 
ColumnInt8*>(full_res_col.get())
                                                ->get_data()[0];
+                            lambda_block.erase_tail(lambda_result_base);
 
                             return cmp < 0;
                         });
@@ -221,6 +250,146 @@ public:
         return Status::OK();
     }
 
+private:
+    Status _set_comparator_argument_gap(const VExprSPtr& expr,
+                                        const std::vector<std::string>* 
argument_names) const {
+        if (expr->is_column_ref()) {
+            auto* ref = static_cast<VColumnRef*>(expr.get());
+            RETURN_IF_ERROR(_validate_comparator_argument_ref(*ref, 
argument_names));
+            ref->set_gap(0);
+            return Status::OK();
+        }
+
+        if (expr->is_slot_ref() || expr->is_virtual_slot_ref()) {
+            return Status::NotSupported(
+                    "array_sort comparator only supports its own lambda 
arguments, but found "
+                    "captured slot ref '{}'",
+                    expr->expr_name());
+        }
+
+        if (_is_lambda_call_with_lambda_expr(expr)) {
+            // array_sort comparator arguments live in a position-based, 
comparator-local frame
+            // that is invisible to nested lambda frames. Reject unsupported 
nested captures during
+            // prepare, otherwise execution would later fail with an internal 
missing-column error.
+            // For example, array_sort((x, y) -> array_map(z -> z + x, 
nested_arr), arr) is
+            // rejected because the inner array_map lambda captures the 
comparator-local x; while
+            // array_sort((x, y) -> array_map(x -> x + 1, nested_arr), arr) is 
still valid because
+            // the inner x is array_map's own argument and shadows the 
comparator argument.
+            
RETURN_IF_ERROR(_reject_nested_lambda_capture_of_comparator_argument(
+                    assert_cast<const 
VLambdaFunctionExpr*>(expr->children()[0].get()),
+                    argument_names));
+            for (int i = 1; i < expr->children().size(); ++i) {
+                
RETURN_IF_ERROR(_set_comparator_argument_gap(expr->children()[i], 
argument_names));
+            }
+            return Status::OK();
+        }
+
+        for (const auto& child : expr->children()) {
+            RETURN_IF_ERROR(_set_comparator_argument_gap(child, 
argument_names));
+        }
+        return Status::OK();
+    }
+
+    Status _reject_nested_lambda_capture_of_comparator_argument(
+            const VLambdaFunctionExpr* lambda_expr,
+            const std::vector<std::string>* comparator_argument_names) const {
+        if (!lambda_expr->has_argument_names()) {
+            return Status::InternalError(
+                    "Cannot validate nested lambda capture in array_sort 
comparator without lambda "
+                    "metadata");
+        }
+        return 
_reject_nested_lambda_capture_of_comparator_argument(lambda_expr->get_child(0),
+                                                                    
comparator_argument_names,
+                                                                    
lambda_expr->argument_names());
+    }
+
+    Status _reject_nested_lambda_capture_of_comparator_argument(
+            const VExprSPtr& expr, const std::vector<std::string>* 
comparator_argument_names,
+            const std::vector<std::string>& in_scope_lambda_argument_names) 
const {
+        // Names in in_scope_lambda_argument_names are declared by the nested 
lambda scopes that
+        // enclose expr. They can legally shadow array_sort comparator 
argument names, so a
+        // ColumnRef matching one of these names should be treated as a local 
nested-lambda
+        // argument instead of an unsupported capture from the array_sort 
comparator.
+        if (expr->is_column_ref()) {
+            if (std::ranges::find(in_scope_lambda_argument_names, 
expr->expr_name()) !=
+                in_scope_lambda_argument_names.end()) {
+                return Status::OK();
+            }
+            if (comparator_argument_names != nullptr &&
+                std::ranges::find(*comparator_argument_names, 
expr->expr_name()) !=
+                        comparator_argument_names->end()) {
+                return Status::NotSupported(
+                        "array_sort comparator does not support nested lambda 
capturing comparator "
+                        "argument '{}'",
+                        expr->expr_name());
+            }
+            return Status::NotSupported(
+                    "array_sort comparator only supports nested lambda 
arguments inside nested "
+                    "lambda bodies, but found captured column ref '{}'",
+                    expr->expr_name());
+        }
+
+        if (expr->is_slot_ref() || expr->is_virtual_slot_ref()) {
+            return Status::NotSupported(
+                    "array_sort comparator only supports nested lambda 
arguments inside nested "
+                    "lambda bodies, but found captured slot ref '{}'",
+                    expr->expr_name());
+        }
+
+        if (_is_lambda_call_with_lambda_expr(expr)) {
+            const auto* lambda_expr =
+                    assert_cast<const 
VLambdaFunctionExpr*>(expr->children()[0].get());
+            if (!lambda_expr->has_argument_names()) {
+                return Status::InternalError(
+                        "Cannot validate nested lambda capture in array_sort 
comparator without "
+                        "lambda metadata");
+            }
+            auto nested_in_scope_lambda_argument_names = 
in_scope_lambda_argument_names;
+            nested_in_scope_lambda_argument_names.insert(
+                    nested_in_scope_lambda_argument_names.end(),
+                    lambda_expr->argument_names().begin(), 
lambda_expr->argument_names().end());
+            
RETURN_IF_ERROR(_reject_nested_lambda_capture_of_comparator_argument(
+                    lambda_expr->get_child(0), comparator_argument_names,
+                    nested_in_scope_lambda_argument_names));
+            for (int i = 1; i < expr->children().size(); ++i) {
+                
RETURN_IF_ERROR(_reject_nested_lambda_capture_of_comparator_argument(
+                        expr->children()[i], comparator_argument_names,
+                        in_scope_lambda_argument_names));
+            }
+            return Status::OK();
+        }
+
+        for (const auto& child : expr->children()) {
+            
RETURN_IF_ERROR(_reject_nested_lambda_capture_of_comparator_argument(
+                    child, comparator_argument_names, 
in_scope_lambda_argument_names));
+        }
+        return Status::OK();
+    }
+
+    Status _validate_comparator_argument_ref(const VColumnRef& ref,
+                                             const std::vector<std::string>* 
argument_names) const {
+        if (ref.column_id() < 0 || ref.column_id() >= 2) {
+            return Status::NotSupported(
+                    "array_sort comparator only supports its own lambda 
arguments, but found "
+                    "column ref '{}' with invalid column id {}",
+                    ref.expr_name(), ref.column_id());
+        }
+        if (argument_names != nullptr &&
+            std::ranges::find(*argument_names, ref.expr_name()) == 
argument_names->end()) {
+            return Status::NotSupported(
+                    "array_sort comparator only supports its own lambda 
arguments, but found "
+                    "captured column ref '{}'",
+                    ref.expr_name());
+        }
+        return Status::OK();
+    }
+
+    bool _is_lambda_call_with_lambda_expr(const VExprSPtr& expr) const {
+        return expr->node_type() == TExprNodeType::LAMBDA_FUNCTION_CALL_EXPR &&
+               !expr->children().empty() &&
+               expr->children()[0]->node_type() == 
TExprNodeType::LAMBDA_FUNCTION_EXPR;
+    }
+
 #define DISPATCH_PRIMITIVE_TYPE(TYPE, COLUMN_CLASS)                 \
     case TYPE:                                                      \
         column_variant = &assert_cast<const COLUMN_CLASS&>(column); \
diff --git a/be/src/exprs/vcolumn_ref.h b/be/src/exprs/vcolumn_ref.h
index 33ade77defa..253c50f0627 100644
--- a/be/src/exprs/vcolumn_ref.h
+++ b/be/src/exprs/vcolumn_ref.h
@@ -18,8 +18,11 @@
 #pragma once
 #include <atomic>
 
+#include "common/exception.h"
 #include "exprs/function/function.h"
+#include "exprs/lambda_function/lambda_execution_context.h"
 #include "exprs/vexpr.h"
+#include "exprs/vexpr_context.h"
 #include "runtime/descriptors.h"
 #include "runtime/runtime_state.h"
 
@@ -59,14 +62,27 @@ public:
     Status execute_column_impl(VExprContext* context, const Block* block, 
const Selector* selector,
                                size_t count, ColumnPtr& result_column) const 
override {
         DCHECK(_open_finished || block == nullptr);
-        auto origin_column = block->get_by_position(_column_id + _gap).column;
+        const int column_position = _get_column_position(context, block);
+        if (column_position < 0 || column_position >= block->columns()) {
+            return Status::InternalError(
+                    "input block not contain column ref {}, column_id={}, 
gap={}, block={}",
+                    _column_name, _column_id, _gap.load(), 
block->dump_structure());
+        }
+        auto origin_column = block->get_by_position(column_position).column;
         result_column = filter_column_with_selector(origin_column, selector, 
count);
         return Status::OK();
     }
 
     DataTypePtr execute_type(const Block* block) const override {
         DCHECK(_open_finished || block == nullptr);
-        return block->get_by_position(_column_id + _gap).type;
+        const int column_position = 
_get_column_position_without_context(block);
+        if (column_position < 0 || column_position >= block->columns()) {
+            throw doris::Exception(
+                    ErrorCode::INTERNAL_ERROR,
+                    "input block not contain column ref {}, column_id={}, 
gap={}, block={}",
+                    _column_name, _column_id, _gap.load(), 
block->dump_structure());
+        }
+        return block->get_by_position(column_position).type;
     }
 
     bool is_constant() const override { return false; }
@@ -75,11 +91,11 @@ public:
 
     const std::string& expr_name() const override { return _column_name; }
 
-    void set_gap(int gap) {
-        if (_gap == 0) {
-            _gap = gap;
-        }
-    }
+    void set_gap(int gap) { _gap = gap; }
+
+    int get_gap() const { return _gap.load(); }
+
+    bool has_gap() const { return _gap.load() >= 0; }
 
     Status clone_node(VExprSPtr* cloned_expr) const override {
         DORIS_CHECK(cloned_expr != nullptr);
@@ -104,8 +120,36 @@ public:
     double execute_cost() const override { return 0.0; }
 
 private:
+    int _get_column_position(VExprContext* context, const Block* block) const {
+        const auto resolve_result =
+                
context->lambda_execution_context().resolve_column_position(_column_name);
+        if (resolve_result.found) {
+            return resolve_result.column_position;
+        }
+        if (resolve_result.searched_named_scope) {
+            return -1;
+        }
+        return _get_column_position_without_context(block);
+    }
+
+    int _get_column_position_without_context(const Block* block) const {
+        if (_gap.load() == -1) {
+            return _find_column_position_by_name(block);
+        }
+        return _column_id + _gap.load();
+    }
+
+    int _find_column_position_by_name(const Block* block) const {
+        for (int position = block->columns() - 1; position >= 0; --position) {
+            if (block->get_by_position(position).name == _column_name) {
+                return position;
+            }
+        }
+        return -1;
+    }
+
     int _column_id;
-    std::atomic<int> _gap = 0;
+    std::atomic<int> _gap = -1;
     std::string _column_name;
 };
 } // namespace doris
diff --git a/be/src/exprs/vexpr_context.cpp b/be/src/exprs/vexpr_context.cpp
index 91ed2305b38..13a2e0b3d44 100644
--- a/be/src/exprs/vexpr_context.cpp
+++ b/be/src/exprs/vexpr_context.cpp
@@ -19,7 +19,9 @@
 
 #include <algorithm>
 #include <cstdint>
+#include <memory>
 #include <string>
+#include <utility>
 
 #include "common/compiler_util.h" // IWYU pragma: keep
 #include "common/exception.h"
@@ -32,6 +34,7 @@
 #include "core/column/column_const.h"
 #include "exec/common/util.hpp"
 #include "exprs/function_context.h"
+#include "exprs/lambda_function/lambda_execution_context.h"
 #include "exprs/vexpr.h"
 #include "runtime/runtime_state.h"
 #include "runtime/thread_context.h"
@@ -45,6 +48,8 @@ class RowDescriptor;
 
 namespace doris {
 
+VExprContext::VExprContext(VExprSPtr expr) : _root(std::move(expr)) {}
+
 VExprContext::~VExprContext() {
     // In runtime filter, only create expr context to get expr root, will not 
call
     // prepare or open, so that it is not need to call close. And call close 
may core
@@ -59,6 +64,13 @@ VExprContext::~VExprContext() {
     }
 }
 
+LambdaExecutionContext& VExprContext::lambda_execution_context() {
+    if (!_lambda_execution_context) {
+        _lambda_execution_context = std::make_unique<LambdaExecutionContext>();
+    }
+    return *_lambda_execution_context;
+}
+
 Status VExprContext::execute(Block* block, int* result_column_id) {
     Status st;
     RETURN_IF_CATCH_EXCEPTION({
diff --git a/be/src/exprs/vexpr_context.h b/be/src/exprs/vexpr_context.h
index 2850149fbae..5bfd77f923e 100644
--- a/be/src/exprs/vexpr_context.h
+++ b/be/src/exprs/vexpr_context.h
@@ -22,6 +22,7 @@
 #include <algorithm>
 #include <cstddef>
 #include <memory>
+#include <string>
 #include <unordered_map>
 #include <utility>
 #include <vector>
@@ -56,6 +57,7 @@ class ColumnIterator;
 namespace doris {
 
 class ScoreRuntime;
+class LambdaExecutionContext;
 using ScoreRuntimeSPtr = std::shared_ptr<ScoreRuntime>;
 
 class IndexExecContext {
@@ -242,7 +244,7 @@ class VExprContext {
     ENABLE_FACTORY_CREATOR(VExprContext);
 
 public:
-    VExprContext(VExprSPtr expr) : _root(std::move(expr)) {}
+    VExprContext(VExprSPtr expr);
     ~VExprContext();
     [[nodiscard]] Status prepare(RuntimeState* state, const RowDescriptor& 
row_desc);
     [[nodiscard]] Status open(RuntimeState* state);
@@ -266,6 +268,8 @@ public:
 
     std::shared_ptr<IndexExecContext> get_index_context() const { return 
_index_context; }
 
+    LambdaExecutionContext& lambda_execution_context();
+
     /// Creates a FunctionContext, and returns the index that's passed to 
fn_context() to
     /// retrieve the created context. Exprs that need a FunctionContext should 
call this in
     /// Prepare() and save the returned index. 'varargs_buffer_size', if 
specified, is the
@@ -350,36 +354,9 @@ public:
 
     void clone_fn_contexts(VExprContext* other);
 
-    VExprContext& operator=(const VExprContext& other) {
-        if (this == &other) {
-            return *this;
-        }
+    VExprContext& operator=(const VExprContext& other) = delete;
 
-        _root = other._root;
-        _is_clone = other._is_clone;
-        _prepared = other._prepared;
-        _opened = other._opened;
-
-        for (const auto& fn : other._fn_contexts) {
-            _fn_contexts.emplace_back(fn->clone());
-        }
-
-        _last_result_column_id = other._last_result_column_id;
-        _depth_num = other._depth_num;
-        return *this;
-    }
-
-    VExprContext& operator=(VExprContext&& other) {
-        _root = other._root;
-        other._root = nullptr;
-        _is_clone = other._is_clone;
-        _prepared = other._prepared;
-        _opened = other._opened;
-        _fn_contexts = std::move(other._fn_contexts);
-        _last_result_column_id = other._last_result_column_id;
-        _depth_num = other._depth_num;
-        return *this;
-    }
+    VExprContext& operator=(VExprContext&& other) = delete;
 
     [[nodiscard]] static size_t get_memory_usage(const VExprContextSPtrs& 
contexts) {
         size_t usage = 0;
@@ -437,6 +414,8 @@ private:
     segment_v2::AnnRangeSearchRuntime _ann_range_search_runtime;
     bool _suitable_for_ann_index = true;
 
+    std::unique_ptr<LambdaExecutionContext> _lambda_execution_context;
+
     std::unique_ptr<RuntimeFilterSelectivity> _rf_selectivity =
             std::make_unique<RuntimeFilterSelectivity>();
 };
diff --git a/be/src/exprs/vlambda_function_call_expr.h 
b/be/src/exprs/vlambda_function_call_expr.h
index 64b8118ed60..a3764fc13fd 100644
--- a/be/src/exprs/vlambda_function_call_expr.h
+++ b/be/src/exprs/vlambda_function_call_expr.h
@@ -50,7 +50,7 @@ public:
             return Status::InternalError("Lambda Function {} is not 
implemented.",
                                          _fn.name.function_name);
         }
-        RETURN_IF_ERROR(_lambda_function->prepare(state));
+        RETURN_IF_ERROR(_lambda_function->prepare(state, _children));
         _prepare_finished = true;
         return Status::OK();
     }
diff --git a/be/src/exprs/vlambda_function_expr.h 
b/be/src/exprs/vlambda_function_expr.h
index 028489cb882..9b8f7474287 100644
--- a/be/src/exprs/vlambda_function_expr.h
+++ b/be/src/exprs/vlambda_function_expr.h
@@ -16,6 +16,9 @@
 // under the License.
 
 #pragma once
+#include <string>
+#include <vector>
+
 #include "common/global_types.h"
 #include "exprs/function/function.h"
 #include "exprs/vexpr.h"
@@ -25,7 +28,10 @@ class VLambdaFunctionExpr final : public VExpr {
     ENABLE_FACTORY_CREATOR(VLambdaFunctionExpr);
 
 public:
-    VLambdaFunctionExpr(const TExprNode& node) : VExpr(node) {}
+    VLambdaFunctionExpr(const TExprNode& node)
+            : VExpr(node),
+              _has_argument_names(node.__isset.lambda_argument_names),
+              _argument_names(node.lambda_argument_names) {}
     ~VLambdaFunctionExpr() override = default;
 
     Status prepare(RuntimeState* state, const RowDescriptor& desc, 
VExprContext* context) override {
@@ -58,9 +64,15 @@ public:
 
     const std::string& expr_name() const override { return _expr_name; }
 
+    bool has_argument_names() const { return _has_argument_names; }
+
+    const std::vector<std::string>& argument_names() const { return 
_argument_names; }
+
     uint64_t get_digest(uint64_t seed) const override { return 0; }
 
 private:
     const std::string _expr_name = "vlambda_function_expr";
+    const bool _has_argument_names;
+    const std::vector<std::string> _argument_names;
 };
 } // namespace doris
diff --git a/be/src/exprs/vslot_ref.h b/be/src/exprs/vslot_ref.h
index a67bdc1953c..680d8b36761 100644
--- a/be/src/exprs/vslot_ref.h
+++ b/be/src/exprs/vslot_ref.h
@@ -42,6 +42,7 @@ public:
 #ifdef BE_TEST
     VSlotRef() = default;
     void set_slot_id(int slot_id) { _slot_id = slot_id; }
+    void set_column_name(const std::string* column_name) { _column_name = 
column_name; }
 #endif
     void set_column_id(int column_id) { _column_id = column_id; }
     Status prepare(RuntimeState* state, const RowDescriptor& desc, 
VExprContext* context) override;
diff --git a/be/test/exprs/lambda_function/array_map_function_test.cpp 
b/be/test/exprs/lambda_function/array_map_function_test.cpp
new file mode 100644
index 00000000000..42469b52c3b
--- /dev/null
+++ b/be/test/exprs/lambda_function/array_map_function_test.cpp
@@ -0,0 +1,1409 @@
+// 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.
+
+#include <gen_cpp/Exprs_types.h>
+#include <gen_cpp/Types_types.h>
+#include <gtest/gtest.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_array.h"
+#include "core/column/column_const.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "exprs/vcolumn_ref.h"
+#include "exprs/vexpr_context.h"
+#include "exprs/vlambda_function_call_expr.h"
+#include "exprs/vlambda_function_expr.h"
+#include "exprs/vslot_ref.h"
+#include "runtime/descriptors.h"
+#include "runtime/runtime_state.h"
+
+namespace doris {
+
+class MockColumnExpr final : public VExpr {
+public:
+    MockColumnExpr(ColumnPtr column, DataTypePtr type, std::string name)
+            : VExpr(type, false),
+              _column(std::move(column)),
+              _type(std::move(type)),
+              _name(std::move(name)) {}
+
+    const std::string& expr_name() const override { return _name; }
+
+    Status execute_column_impl(VExprContext* /*context*/, const Block* 
/*block*/,
+                               const Selector* /*selector*/, size_t /*count*/,
+                               ColumnPtr& result_column) const override {
+        result_column = _column;
+        return Status::OK();
+    }
+
+    DataTypePtr execute_type(const Block* /*block*/) const override { return 
_type; }
+
+private:
+    ColumnPtr _column;
+    DataTypePtr _type;
+    std::string _name;
+};
+
+class MockConstColumnExpr final : public VExpr {
+public:
+    MockConstColumnExpr(ColumnPtr column, DataTypePtr type, std::string name)
+            : VExpr(type, false),
+              _column(std::move(column)),
+              _type(std::move(type)),
+              _name(std::move(name)) {}
+
+    const std::string& expr_name() const override { return _name; }
+
+    Status execute_column_impl(VExprContext* /*context*/, const Block* 
/*block*/,
+                               const Selector* /*selector*/, size_t count,
+                               ColumnPtr& result_column) const override {
+        result_column = ColumnConst::create(_column, count);
+        return Status::OK();
+    }
+
+    DataTypePtr execute_type(const Block* /*block*/) const override { return 
_type; }
+
+private:
+    ColumnPtr _column;
+    DataTypePtr _type;
+    std::string _name;
+};
+
+class MockBodyExpr final : public VExpr {
+public:
+    MockBodyExpr(DataTypePtr type, std::string name)
+            : VExpr(type, false), _type(std::move(type)), 
_name(std::move(name)) {}
+
+    const std::string& expr_name() const override { return _name; }
+
+    Status execute_column_impl(VExprContext* /*context*/, const Block* 
/*block*/,
+                               const Selector* /*selector*/, size_t /*count*/,
+                               ColumnPtr& /*result_column*/) const override {
+        return Status::InternalError("mock body should not be executed");
+    }
+
+    DataTypePtr execute_type(const Block* /*block*/) const override { return 
_type; }
+
+private:
+    DataTypePtr _type;
+    std::string _name;
+};
+
+class MockSubtractExpr final : public VExpr {
+public:
+    explicit MockSubtractExpr(DataTypePtr type) : VExpr(type, false), 
_type(std::move(type)) {}
+
+    const std::string& expr_name() const override { return _name; }
+
+    Status execute_column_impl(VExprContext* context, const Block* block, 
const Selector* selector,
+                               size_t count, ColumnPtr& result_column) const 
override {
+        ColumnPtr left;
+        ColumnPtr right;
+        RETURN_IF_ERROR(get_child(0)->execute_column(context, block, selector, 
count, left));
+        RETURN_IF_ERROR(get_child(1)->execute_column(context, block, selector, 
count, right));
+        left = left->convert_to_full_column_if_const();
+        right = right->convert_to_full_column_if_const();
+
+        const auto& left_data = _get_int_data(left);
+        const auto& right_data = _get_int_data(right);
+        auto result = ColumnInt32::create();
+        for (size_t i = 0; i < count; ++i) {
+            result->insert_value(left_data.get_element(i) - 
right_data.get_element(i));
+        }
+        result_column = std::move(result);
+        return Status::OK();
+    }
+
+    DataTypePtr execute_type(const Block* /*block*/) const override { return 
_type; }
+
+private:
+    const ColumnInt32& _get_int_data(const ColumnPtr& column) const {
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(column.get())) {
+            return assert_cast<const 
ColumnInt32&>(nullable->get_nested_column());
+        }
+        return assert_cast<const ColumnInt32&>(*column);
+    }
+
+    DataTypePtr _type;
+    std::string _name = "mock_subtract";
+};
+
+class MockAddExpr final : public VExpr {
+public:
+    explicit MockAddExpr(DataTypePtr type) : VExpr(type, false), 
_type(std::move(type)) {}
+
+    const std::string& expr_name() const override { return _name; }
+
+    Status execute_column_impl(VExprContext* context, const Block* block, 
const Selector* selector,
+                               size_t count, ColumnPtr& result_column) const 
override {
+        ColumnPtr left;
+        ColumnPtr right;
+        RETURN_IF_ERROR(get_child(0)->execute_column(context, block, selector, 
count, left));
+        RETURN_IF_ERROR(get_child(1)->execute_column(context, block, selector, 
count, right));
+        left = left->convert_to_full_column_if_const();
+        right = right->convert_to_full_column_if_const();
+
+        const auto& left_data = _get_int_data(left);
+        const auto& right_data = _get_int_data(right);
+        auto result = ColumnInt32::create();
+        for (size_t i = 0; i < count; ++i) {
+            result->insert_value(left_data.get_element(i) + 
right_data.get_element(i));
+        }
+        result_column = std::move(result);
+        return Status::OK();
+    }
+
+    DataTypePtr execute_type(const Block* /*block*/) const override { return 
_type; }
+
+private:
+    const ColumnInt32& _get_int_data(const ColumnPtr& column) const {
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(column.get())) {
+            return assert_cast<const 
ColumnInt32&>(nullable->get_nested_column());
+        }
+        return assert_cast<const ColumnInt32&>(*column);
+    }
+
+    DataTypePtr _type;
+    std::string _name = "mock_add";
+};
+
+class MockMultiplyExpr final : public VExpr {
+public:
+    explicit MockMultiplyExpr(DataTypePtr type) : VExpr(type, false), 
_type(std::move(type)) {}
+
+    const std::string& expr_name() const override { return _name; }
+
+    Status execute_column_impl(VExprContext* context, const Block* block, 
const Selector* selector,
+                               size_t count, ColumnPtr& result_column) const 
override {
+        ColumnPtr left;
+        ColumnPtr right;
+        RETURN_IF_ERROR(get_child(0)->execute_column(context, block, selector, 
count, left));
+        RETURN_IF_ERROR(get_child(1)->execute_column(context, block, selector, 
count, right));
+        left = left->convert_to_full_column_if_const();
+        right = right->convert_to_full_column_if_const();
+
+        const auto& left_data = _get_int_data(left);
+        const auto& right_data = _get_int_data(right);
+        auto result = ColumnInt32::create();
+        for (size_t i = 0; i < count; ++i) {
+            result->insert_value(left_data.get_element(i) * 
right_data.get_element(i));
+        }
+        result_column = std::move(result);
+        return Status::OK();
+    }
+
+    DataTypePtr execute_type(const Block* /*block*/) const override { return 
_type; }
+
+private:
+    const ColumnInt32& _get_int_data(const ColumnPtr& column) const {
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(column.get())) {
+            return assert_cast<const 
ColumnInt32&>(nullable->get_nested_column());
+        }
+        return assert_cast<const ColumnInt32&>(*column);
+    }
+
+    DataTypePtr _type;
+    std::string _name = "mock_multiply";
+};
+
+class MockCompareExpr final : public VExpr {
+public:
+    explicit MockCompareExpr(DataTypePtr type) : VExpr(type, false), 
_type(std::move(type)) {}
+
+    const std::string& expr_name() const override { return _name; }
+
+    Status execute_column_impl(VExprContext* context, const Block* block, 
const Selector* selector,
+                               size_t count, ColumnPtr& result_column) const 
override {
+        ColumnPtr left;
+        ColumnPtr right;
+        RETURN_IF_ERROR(get_child(0)->execute_column(context, block, selector, 
count, left));
+        RETURN_IF_ERROR(get_child(1)->execute_column(context, block, selector, 
count, right));
+        left = left->convert_to_full_column_if_const();
+        right = right->convert_to_full_column_if_const();
+
+        const auto& left_data = _get_int_data(left);
+        const auto& right_data = _get_int_data(right);
+        auto result = ColumnInt8::create();
+        for (size_t i = 0; i < count; ++i) {
+            const auto left_value = left_data.get_element(i);
+            const auto right_value = right_data.get_element(i);
+            int8_t compare_result = 0;
+            if (left_value < right_value) {
+                compare_result = -1;
+            } else if (left_value > right_value) {
+                compare_result = 1;
+            }
+            result->insert_value(compare_result);
+        }
+        result_column = std::move(result);
+        return Status::OK();
+    }
+
+    DataTypePtr execute_type(const Block* /*block*/) const override { return 
_type; }
+
+private:
+    const ColumnInt32& _get_int_data(const ColumnPtr& column) const {
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(column.get())) {
+            return assert_cast<const 
ColumnInt32&>(nullable->get_nested_column());
+        }
+        return assert_cast<const ColumnInt32&>(*column);
+    }
+
+    DataTypePtr _type;
+    std::string _name = "mock_compare";
+};
+
+static TExprNode make_lambda_call_node(const DataTypePtr& type, int 
num_children,
+                                       const std::string& function_name = 
"array_map") {
+    TExprNode node;
+    node.__set_node_type(TExprNodeType::LAMBDA_FUNCTION_CALL_EXPR);
+    node.__set_num_children(num_children);
+    node.__set_type(type->to_thrift());
+    node.__set_is_nullable(type->is_nullable());
+
+    TFunction fn;
+    TFunctionName fn_name;
+    fn_name.__set_function_name(function_name);
+    fn.__set_name(fn_name);
+    node.__set_fn(fn);
+    return node;
+}
+
+static TExprNode make_lambda_expr_node(const DataTypePtr& type,
+                                       const std::vector<std::string>& 
argument_names,
+                                       bool set_argument_names = true) {
+    TExprNode node;
+    node.__set_node_type(TExprNodeType::LAMBDA_FUNCTION_EXPR);
+    node.__set_num_children(1);
+    node.__set_type(type->to_thrift());
+    node.__set_is_nullable(type->is_nullable());
+    if (set_argument_names) {
+        node.__set_lambda_argument_names(argument_names);
+    }
+    return node;
+}
+
+static TExprNode make_column_ref_node(int column_id, const std::string& 
column_name,
+                                      const DataTypePtr& type) {
+    TExprNode node;
+    node.__set_node_type(TExprNodeType::COLUMN_REF);
+    node.__set_num_children(0);
+    node.__set_type(type->to_thrift());
+    node.__set_is_nullable(type->is_nullable());
+
+    TColumnRef column_ref;
+    column_ref.__set_column_id(column_id);
+    column_ref.__set_column_name(column_name);
+    node.__set_column_ref(column_ref);
+    return node;
+}
+
+static VExprSPtr make_slot_ref(int column_id, const std::string& column_name,
+                               const DataTypePtr& type) {
+    static std::vector<std::unique_ptr<std::string>> column_names;
+    column_names.push_back(std::make_unique<std::string>(column_name));
+
+    auto ref = VSlotRef::create_shared();
+    ref->set_node_type(TExprNodeType::SLOT_REF);
+    ref->set_slot_id(-1);
+    ref->set_column_id(column_id);
+    ref->set_column_name(column_names.back().get());
+    ref->data_type() = type;
+    return ref;
+}
+
+static ColumnPtr make_int_column(const std::vector<int32_t>& values) {
+    auto column = ColumnInt32::create();
+    for (auto value : values) {
+        column->insert_value(value);
+    }
+    return column;
+}
+
+static ColumnPtr make_int_array_column(const 
std::vector<std::vector<int32_t>>& rows) {
+    auto int_column = ColumnInt32::create();
+    auto offsets = ColumnArray::ColumnOffsets::create();
+    int64_t offset = 0;
+    for (const auto& row : rows) {
+        for (auto value : row) {
+            int_column->insert_value(value);
+        }
+        offset += row.size();
+        offsets->insert_value(offset);
+    }
+    auto int_null_map = ColumnUInt8::create(int_column->size(), 0);
+    auto nullable_int_column =
+            ColumnNullable::create(std::move(int_column), 
std::move(int_null_map));
+    return ColumnArray::create(std::move(nullable_int_column), 
std::move(offsets));
+}
+
+static ColumnPtr make_nested_int_array_column() {
+    // Two input rows:
+    //   row 0: [[1, 2], [3]]
+    //   row 1: [[4, 5]]
+    auto int_column = ColumnInt32::create();
+    for (int32_t value : {1, 2, 3, 4, 5}) {
+        int_column->insert_value(value);
+    }
+    auto int_null_map = ColumnUInt8::create(int_column->size(), 0);
+    auto nullable_int_column =
+            ColumnNullable::create(std::move(int_column), 
std::move(int_null_map));
+
+    auto inner_offsets = ColumnArray::ColumnOffsets::create();
+    for (int64_t offset : {2, 3, 5}) {
+        inner_offsets->insert_value(offset);
+    }
+    auto inner_array_column =
+            ColumnArray::create(std::move(nullable_int_column), 
std::move(inner_offsets));
+    auto inner_array_null_map = 
ColumnUInt8::create(inner_array_column->size(), 0);
+    auto nullable_inner_array_column =
+            ColumnNullable::create(std::move(inner_array_column), 
std::move(inner_array_null_map));
+
+    auto outer_offsets = ColumnArray::ColumnOffsets::create();
+    for (int64_t offset : {2, 3}) {
+        outer_offsets->insert_value(offset);
+    }
+    return ColumnArray::create(std::move(nullable_inner_array_column), 
std::move(outer_offsets));
+}
+
+static ColumnPtr make_nested_unsorted_int_array_column() {
+    // Two input rows:
+    //   row 0: [[2, 1], [3]]
+    //   row 1: [[5, 4]]
+    auto int_column = ColumnInt32::create();
+    for (int32_t value : {2, 1, 3, 5, 4}) {
+        int_column->insert_value(value);
+    }
+    auto int_null_map = ColumnUInt8::create(int_column->size(), 0);
+    auto nullable_int_column =
+            ColumnNullable::create(std::move(int_column), 
std::move(int_null_map));
+
+    auto inner_offsets = ColumnArray::ColumnOffsets::create();
+    for (int64_t offset : {2, 3, 5}) {
+        inner_offsets->insert_value(offset);
+    }
+    auto inner_array_column =
+            ColumnArray::create(std::move(nullable_int_column), 
std::move(inner_offsets));
+    auto inner_array_null_map = 
ColumnUInt8::create(inner_array_column->size(), 0);
+    auto nullable_inner_array_column =
+            ColumnNullable::create(std::move(inner_array_column), 
std::move(inner_array_null_map));
+
+    auto outer_offsets = ColumnArray::ColumnOffsets::create();
+    for (int64_t offset : {2, 3}) {
+        outer_offsets->insert_value(offset);
+    }
+    return ColumnArray::create(std::move(nullable_inner_array_column), 
std::move(outer_offsets));
+}
+
+static void open_expr(const VExprSPtr& expr, VExprContext* context) {
+    RuntimeState state;
+    RowDescriptor row_desc;
+    ASSERT_TRUE(expr->prepare(&state, row_desc, context).ok());
+    ASSERT_TRUE(expr->open(&state, context, 
FunctionContext::THREAD_LOCAL).ok());
+}
+
+TEST(ArrayMapFunctionTest, NestedLambdaWithSameArgumentNameUsesInnerScope) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"x"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"x"}));
+
+    // This mirrors array_map(x -> array_map(x -> x, x), nested_array). Both
+    // lambda arguments are named "x"; only their scopes and element types are
+    // different. The non-ordinal column ids cover FE plans where a lambda
+    // argument ColumnRef id is not the same as its argument position.
+    auto inner_x = VColumnRef::create_shared(make_column_ref_node(2, "x", 
int_type));
+    auto outer_x = VColumnRef::create_shared(make_column_ref_node(1, "x", 
array_int_type));
+
+    inner_lambda->add_child(inner_x);
+    inner_call->add_child(inner_lambda);
+    inner_call->add_child(outer_x);
+    outer_lambda->add_child(inner_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_nested_int_array_column(),
+                                                     nested_array_int_type, 
"nested_array"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    // Keep one ordinary input column before the array_map argument. The test
+    // verifies nested lambda gap calculation when lambda blocks need to 
preserve
+    // existing input columns as well as append current lambda arguments.
+    Block block;
+    block.insert({make_int_column({10, 20}), 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& outer_array = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(outer_array.size(), 2);
+    ASSERT_EQ(outer_array.get_offsets()[0], 2);
+    ASSERT_EQ(outer_array.get_offsets()[1], 3);
+
+    const auto* nullable_inner_arrays =
+            check_and_get_column<ColumnNullable>(&outer_array.get_data());
+    ASSERT_NE(nullable_inner_arrays, nullptr);
+    for (size_t i = 0; i < nullable_inner_arrays->size(); ++i) {
+        EXPECT_FALSE(nullable_inner_arrays->is_null_at(i));
+    }
+
+    const auto& inner_arrays =
+            assert_cast<const 
ColumnArray&>(nullable_inner_arrays->get_nested_column());
+    ASSERT_EQ(inner_arrays.size(), 3);
+    ASSERT_EQ(inner_arrays.get_offsets()[0], 2);
+    ASSERT_EQ(inner_arrays.get_offsets()[1], 3);
+    ASSERT_EQ(inner_arrays.get_offsets()[2], 5);
+
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&inner_arrays.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    for (size_t i = 0; i < nullable_values->size(); ++i) {
+        EXPECT_FALSE(nullable_values->is_null_at(i));
+    }
+
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 5);
+    EXPECT_EQ(values.get_element(0), 1);
+    EXPECT_EQ(values.get_element(1), 2);
+    EXPECT_EQ(values.get_element(2), 3);
+    EXPECT_EQ(values.get_element(3), 4);
+    EXPECT_EQ(values.get_element(4), 5);
+}
+
+TEST(ArrayMapFunctionTest, 
NamedLambdaWithFewerArgumentsThanArraysUsesDeclaredBindings) {
+    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, 
3));
+    auto lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"x"}));
+
+    // This mirrors a BE-compatible plan shape like array_map(x -> x, arr1, 
arr2).
+    // Only x is part of the lambda binding frame; the extra array input is 
still
+    // materialized for size/offset validation but must not require a lambda 
name.
+    auto x = VColumnRef::create_shared(make_column_ref_node(5, "x", int_type));
+
+    lambda->add_child(x);
+    root->add_child(lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{10, 
20}, {30}}),
+                                                     array_int_type, "arr1"));
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{1, 2}, 
{3}}),
+                                                     array_int_type, "arr2"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 2, result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+
+    const auto& result_array = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(result_array.size(), 2);
+    ASSERT_EQ(result_array.get_offsets()[0], 2);
+    ASSERT_EQ(result_array.get_offsets()[1], 3);
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&result_array.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 3);
+    EXPECT_EQ(values.get_element(0), 10);
+    EXPECT_EQ(values.get_element(1), 20);
+    EXPECT_EQ(values.get_element(2), 30);
+}
+
+TEST(ArrayMapFunctionTest, 
NestedArraySortInsideArrayMapSkipsArrayMapArgumentInference) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nullable_array_int_type = 
std::make_shared<DataTypeNullable>(array_int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"a"}));
+    auto sort_call = VLambdaFunctionCallExpr::create_shared(
+            make_lambda_call_node(nullable_array_int_type, 2, "array_sort"));
+    auto sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"a", 
"b"}));
+    auto compare = std::make_shared<MockCompareExpr>(int8_type);
+
+    // This mirrors array_map(a -> array_sort((a, b) -> a - b, a), 
nested_array).
+    // FE represents the second comparator argument by cloning the first
+    // ColumnRef and only changing column_id to 1, so both comparator 
ColumnRefs
+    // can have the same name. array_sort must keep its comparator arguments
+    // position-based instead of using array_map's name-based binding.
+    auto sort_a = VColumnRef::create_shared(make_column_ref_node(0, "a", 
int_type));
+    auto sort_b = VColumnRef::create_shared(make_column_ref_node(1, "a", 
int_type));
+    auto outer_a = VColumnRef::create_shared(make_column_ref_node(1, "a", 
array_int_type));
+
+    compare->add_child(sort_a);
+    compare->add_child(sort_b);
+    sort_lambda->add_child(compare);
+    sort_call->add_child(sort_lambda);
+    sort_call->add_child(outer_a);
+    outer_lambda->add_child(sort_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_nested_unsorted_int_array_column(),
+                                                     nested_array_int_type, 
"nested_array"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    block.insert({make_int_column({10, 20}), 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& outer_array = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(outer_array.size(), 2);
+    ASSERT_EQ(outer_array.get_offsets()[0], 2);
+    ASSERT_EQ(outer_array.get_offsets()[1], 3);
+
+    const auto* nullable_inner_arrays =
+            check_and_get_column<ColumnNullable>(&outer_array.get_data());
+    ASSERT_NE(nullable_inner_arrays, nullptr);
+
+    const auto& inner_arrays =
+            assert_cast<const 
ColumnArray&>(nullable_inner_arrays->get_nested_column());
+    ASSERT_EQ(inner_arrays.size(), 3);
+    ASSERT_EQ(inner_arrays.get_offsets()[0], 2);
+    ASSERT_EQ(inner_arrays.get_offsets()[1], 3);
+    ASSERT_EQ(inner_arrays.get_offsets()[2], 5);
+
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&inner_arrays.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 5);
+    EXPECT_EQ(values.get_element(0), 1);
+    EXPECT_EQ(values.get_element(1), 2);
+    EXPECT_EQ(values.get_element(2), 3);
+    EXPECT_EQ(values.get_element(3), 4);
+    EXPECT_EQ(values.get_element(4), 5);
+}
+
+TEST(ArrayMapFunctionTest, 
NestedArraySortComparatorCapturingOuterArgumentReturnsError) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"i"}));
+    auto sort_call = VLambdaFunctionCallExpr::create_shared(
+            make_lambda_call_node(array_int_type, 2, "array_sort"));
+    auto sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"x", 
"y"}));
+    auto add = std::make_shared<MockAddExpr>(int_type);
+
+    // This mirrors:
+    //   array_map(i -> array_sort((x, y) -> x + i, arr2), arr1)
+    // array_sort's comparator executes against a two-column temporary block 
that
+    // only contains x and y. Capturing i from the outer array_map would 
otherwise
+    // be silently resolved by column id as x.
+    auto sort_x = VColumnRef::create_shared(make_column_ref_node(0, "x", 
int_type));
+    auto outer_i = VColumnRef::create_shared(make_column_ref_node(0, "i", 
int_type));
+
+    add->add_child(sort_x);
+    add->add_child(outer_i);
+    sort_lambda->add_child(add);
+    sort_call->add_child(sort_lambda);
+    
sort_call->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{2,
 1}, {4, 3}}),
+                                                          array_int_type, 
"arr2"));
+    outer_lambda->add_child(sort_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{10, 
20}, {30}}),
+                                                     array_int_type, "arr1"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(
+            status.to_string().find("array_sort comparator only supports its 
own lambda arguments"),
+            std::string::npos);
+    EXPECT_NE(status.to_string().find("captured column ref 'i'"), 
std::string::npos);
+}
+
+TEST(ArrayMapFunctionTest,
+     
NestedArraySortComparatorNestedLambdaCapturingComparatorArgumentReturnsError) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    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 sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"x", 
"y"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"z"}));
+    auto add = std::make_shared<MockAddExpr>(int_type);
+
+    // This mirrors:
+    //   array_sort((x, y) -> array_map(z -> z + x, [1, 2]), arr)
+    // The inner array_map's lambda frame cannot see array_sort 
comparator-local x/y because
+    // array_sort intentionally uses a position-based anonymous comparator 
frame.
+    auto inner_z = VColumnRef::create_shared(make_column_ref_node(0, "z", 
int_type));
+    auto comparator_x = VColumnRef::create_shared(make_column_ref_node(0, "x", 
int_type));
+
+    add->add_child(inner_z);
+    add->add_child(comparator_x);
+    inner_lambda->add_child(add);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    sort_lambda->add_child(inner_call);
+    root->add_child(sort_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{2, 1}, 
{4, 3}}),
+                                                     array_int_type, "arr"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find(
+                      "array_sort comparator does not support nested lambda 
capturing comparator "
+                      "argument 'x'"),
+              std::string::npos);
+}
+
+TEST(ArrayMapFunctionTest, 
NestedArraySortComparatorNestedLambdaCanShadowComparatorArgument) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    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 sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"x", 
"y"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"x"}));
+
+    // This mirrors:
+    //   array_sort((x, y) -> array_map(x -> x, [1, 2]), arr)
+    // The inner array_map argument named x shadows the array_sort comparator 
argument x.
+    auto inner_x = VColumnRef::create_shared(make_column_ref_node(0, "x", 
int_type));
+
+    inner_lambda->add_child(inner_x);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    sort_lambda->add_child(inner_call);
+    root->add_child(sort_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{2, 1}, 
{4, 3}}),
+                                                     array_int_type, "arr"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_TRUE(status.ok()) << status.to_string();
+}
+
+TEST(ArrayMapFunctionTest,
+     
NestedArraySortComparatorNestedLambdaCapturingNonComparatorColumnReturnsError) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    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 sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"x", 
"y"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"z"}));
+    auto add = std::make_shared<MockAddExpr>(int_type);
+
+    // This mirrors:
+    //   array_sort((x, y) -> array_map(z -> z + outer_col, [1, 2]), arr)
+    // Only the nested lambda's own arguments are visible in nested lambda 
bodies.
+    auto inner_z = VColumnRef::create_shared(make_column_ref_node(0, "z", 
int_type));
+    auto outer_col = VColumnRef::create_shared(make_column_ref_node(2, 
"outer_col", int_type));
+
+    add->add_child(inner_z);
+    add->add_child(outer_col);
+    inner_lambda->add_child(add);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    sort_lambda->add_child(inner_call);
+    root->add_child(sort_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{2, 1}, 
{4, 3}}),
+                                                     array_int_type, "arr"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find(
+                      "array_sort comparator only supports nested lambda 
arguments inside nested "
+                      "lambda bodies, but found captured column ref 
'outer_col'"),
+              std::string::npos);
+}
+
+TEST(ArrayMapFunctionTest, 
NestedArraySortComparatorNestedLambdaCapturingSlotRefReturnsError) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    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 sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"x", 
"y"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"z"}));
+    auto add = std::make_shared<MockAddExpr>(int_type);
+
+    // This mirrors:
+    //   array_sort((x, y) -> array_map(z -> z + k1, [1, 2]), arr)
+    // SlotRefs from outside the nested lambda are not available inside 
array_sort's comparator.
+    auto inner_z = VColumnRef::create_shared(make_column_ref_node(0, "z", 
int_type));
+    auto k1 = make_slot_ref(0, "k1", int_type);
+
+    add->add_child(inner_z);
+    add->add_child(k1);
+    inner_lambda->add_child(add);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    sort_lambda->add_child(inner_call);
+    root->add_child(sort_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{2, 1}, 
{4, 3}}),
+                                                     array_int_type, "arr"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find(
+                      "array_sort comparator only supports nested lambda 
arguments inside nested "
+                      "lambda bodies, but found captured slot ref 'k1'"),
+              std::string::npos);
+}
+
+TEST(ArrayMapFunctionTest,
+     NestedArraySortComparatorNestedLambdaWithoutArgumentMetadataReturnsError) 
{
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    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 sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"x", 
"y"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = VLambdaFunctionExpr::create_shared(
+            make_lambda_expr_node(int_type, {"z"}, false 
/*set_argument_names*/));
+
+    // This mirrors an old FE plan where the nested array_map lambda has no 
argument metadata.
+    auto inner_z = VColumnRef::create_shared(make_column_ref_node(0, "z", 
int_type));
+
+    inner_lambda->add_child(inner_z);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    sort_lambda->add_child(inner_call);
+    root->add_child(sort_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{2, 1}, 
{4, 3}}),
+                                                     array_int_type, "arr"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find(
+                      "Cannot validate nested lambda capture in array_sort 
comparator without "
+                      "lambda metadata"),
+              std::string::npos);
+}
+
+TEST(ArrayMapFunctionTest,
+     
NestedArraySortComparatorTwoLevelNestedLambdaCanCaptureOuterNestedArgument) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root = VLambdaFunctionCallExpr::create_shared(
+            make_lambda_call_node(array_int_type, 2, "array_sort"));
+    auto sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"x", 
"y"}));
+    auto outer_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"z"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"q"}));
+    auto add = std::make_shared<MockAddExpr>(int_type);
+
+    // This mirrors:
+    //   array_sort((x, y) -> array_map(z -> array_map(q -> q + z, [1, 2]), 
[3, 4]), arr)
+    // q is local to the inner array_map, while z is declared by the enclosing 
nested array_map.
+    auto inner_q = VColumnRef::create_shared(make_column_ref_node(0, "q", 
int_type));
+    auto outer_z = VColumnRef::create_shared(make_column_ref_node(0, "z", 
int_type));
+
+    add->add_child(inner_q);
+    add->add_child(outer_z);
+    inner_lambda->add_child(add);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    outer_lambda->add_child(inner_call);
+    outer_call->add_child(outer_lambda);
+    
outer_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{3,
 4}}),
+                                                                
array_int_type, "outer_array"));
+    sort_lambda->add_child(outer_call);
+    root->add_child(sort_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{2, 1}, 
{4, 3}}),
+                                                     array_int_type, "arr"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_TRUE(status.ok()) << status.to_string();
+}
+
+TEST(ArrayMapFunctionTest,
+     
NestedArraySortComparatorNestedLambdaCallWithoutArgumentMetadataReturnsError) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto int8_type = std::make_shared<DataTypeInt8>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root = VLambdaFunctionCallExpr::create_shared(
+            make_lambda_call_node(array_int_type, 2, "array_sort"));
+    auto sort_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int8_type, {"x", 
"y"}));
+    auto outer_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"z"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = VLambdaFunctionExpr::create_shared(
+            make_lambda_expr_node(int_type, {"q"}, false 
/*set_argument_names*/));
+    auto add = std::make_shared<MockAddExpr>(int_type);
+
+    // This mirrors an old FE plan where a nested lambda call under another 
nested lambda has no
+    // argument metadata.
+    auto inner_q = VColumnRef::create_shared(make_column_ref_node(0, "q", 
int_type));
+    auto outer_z = VColumnRef::create_shared(make_column_ref_node(0, "z", 
int_type));
+
+    add->add_child(inner_q);
+    add->add_child(outer_z);
+    inner_lambda->add_child(add);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    outer_lambda->add_child(inner_call);
+    outer_call->add_child(outer_lambda);
+    
outer_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{3,
 4}}),
+                                                                
array_int_type, "outer_array"));
+    sort_lambda->add_child(outer_call);
+    root->add_child(sort_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{2, 1}, 
{4, 3}}),
+                                                     array_int_type, "arr"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find(
+                      "Cannot validate nested lambda capture in array_sort 
comparator without "
+                      "lambda metadata"),
+              std::string::npos);
+}
+
+TEST(ArrayMapFunctionTest, NestedLambdaCapturesOuterSlotRefFromInnerBody) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"x"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"y"}));
+    auto add = std::make_shared<MockAddExpr>(int_type);
+
+    // This mirrors:
+    //   array_map(x -> array_map(y -> y + k1, [1, 2]), arr)
+    // The outer array_map body does not reference k1 directly. The BE still
+    // needs to carry the full input block through the outer lambda block and
+    // inherit it into the inner lambda block.
+    auto inner_y = VColumnRef::create_shared(make_column_ref_node(0, "y", 
int_type));
+    auto k1 = make_slot_ref(0, "k1", int_type);
+
+    add->add_child(inner_y);
+    add->add_child(k1);
+    inner_lambda->add_child(add);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    outer_lambda->add_child(inner_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{10, 
20}, {30}}),
+                                                     array_int_type, 
"outer_array"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    block.insert({make_int_column({100, 200}), int_type, "k1"});
+
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 
block.rows(), result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+
+    const auto& outer_array = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(outer_array.size(), 2);
+    ASSERT_EQ(outer_array.get_offsets()[0], 2);
+    ASSERT_EQ(outer_array.get_offsets()[1], 3);
+
+    const auto* nullable_inner_arrays =
+            check_and_get_column<ColumnNullable>(&outer_array.get_data());
+    ASSERT_NE(nullable_inner_arrays, nullptr);
+    const auto& inner_arrays =
+            assert_cast<const 
ColumnArray&>(nullable_inner_arrays->get_nested_column());
+    ASSERT_EQ(inner_arrays.size(), 3);
+    ASSERT_EQ(inner_arrays.get_offsets()[0], 2);
+    ASSERT_EQ(inner_arrays.get_offsets()[1], 4);
+    ASSERT_EQ(inner_arrays.get_offsets()[2], 6);
+
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&inner_arrays.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 6);
+    EXPECT_EQ(values.get_element(0), 101);
+    EXPECT_EQ(values.get_element(1), 102);
+    EXPECT_EQ(values.get_element(2), 101);
+    EXPECT_EQ(values.get_element(3), 102);
+    EXPECT_EQ(values.get_element(4), 201);
+    EXPECT_EQ(values.get_element(5), 202);
+}
+
+TEST(ArrayMapFunctionTest, 
LegacySingleLambdaWithoutArgumentMetadataUsesColumnId) {
+    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));
+    auto lambda = VLambdaFunctionExpr::create_shared(
+            make_lambda_expr_node(int_type, {"x"}, false 
/*set_argument_names*/));
+
+    // Simulate an old FE plan without lambda_argument_names. Single-layer
+    // lambda can still use the ColumnRef id to bind argument position 0.
+    auto x = VColumnRef::create_shared(make_column_ref_node(0, "legacy_x", 
int_type));
+    lambda->add_child(x);
+    root->add_child(lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{10, 
20}}),
+                                                     array_int_type, "array"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 1, result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+
+    const auto& result_array = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(result_array.size(), 1);
+    ASSERT_EQ(result_array.get_offsets()[0], 2);
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&result_array.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 2);
+    EXPECT_EQ(values.get_element(0), 10);
+    EXPECT_EQ(values.get_element(1), 20);
+}
+
+TEST(ArrayMapFunctionTest, 
LegacyNestedLambdaWithoutArgumentMetadataReturnsError) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda = VLambdaFunctionExpr::create_shared(
+            make_lambda_expr_node(array_int_type, {"x"}, false 
/*set_argument_names*/));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = VLambdaFunctionExpr::create_shared(
+            make_lambda_expr_node(int_type, {"y"}, false 
/*set_argument_names*/));
+    auto subtract = std::make_shared<MockSubtractExpr>(int_type);
+
+    auto outer_x = VColumnRef::create_shared(make_column_ref_node(0, "x", 
int_type));
+    auto inner_y = VColumnRef::create_shared(make_column_ref_node(0, "y", 
int_type));
+
+    subtract->add_child(outer_x);
+    subtract->add_child(inner_y);
+    inner_lambda->add_child(subtract);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    outer_lambda->add_child(inner_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{10, 
20}}),
+                                                     array_int_type, 
"outer_array"));
+
+    VExprContext context(root);
+    RuntimeState state;
+    RowDescriptor row_desc;
+    auto status = root->prepare(&state, row_desc, &context);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find(
+                      "Cannot resolve nested lambda argument without lambda 
metadata"),
+              std::string::npos);
+}
+
+TEST(ArrayMapFunctionTest, NestedLambdaUsesOuterArgumentsAndInputSlotRefArray) 
{
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 3));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, {"x", 
"y"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"z"}));
+    auto subtract = std::make_shared<MockSubtractExpr>(int_type);
+    auto add = std::make_shared<MockAddExpr>(int_type);
+    auto multiply = std::make_shared<MockMultiplyExpr>(int_type);
+
+    // This mirrors:
+    //   array_map((x, y) -> array_map(z -> (y - z) * (x + z), arr3), arr1, 
arr2)
+    // arr3 is a sparse ordinary input SlotRef. Each lambda block must keep 
required
+    // input positions before appending its own arguments, so the inner 
array_map can
+    // use arr3 while resolving x/y/z from the nested lambda context by name.
+    auto y_for_subtract = VColumnRef::create_shared(make_column_ref_node(10, 
"y", int_type));
+    auto z_for_subtract = VColumnRef::create_shared(make_column_ref_node(0, 
"z", int_type));
+    auto x_for_add = VColumnRef::create_shared(make_column_ref_node(9, "x", 
int_type));
+    auto z_for_add = VColumnRef::create_shared(make_column_ref_node(0, "z", 
int_type));
+    auto arr3 = make_slot_ref(4, "arr3", array_int_type);
+
+    subtract->add_child(y_for_subtract);
+    subtract->add_child(z_for_subtract);
+    add->add_child(x_for_add);
+    add->add_child(z_for_add);
+    multiply->add_child(subtract);
+    multiply->add_child(add);
+    inner_lambda->add_child(multiply);
+    inner_call->add_child(inner_lambda);
+    inner_call->add_child(arr3);
+    outer_lambda->add_child(inner_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{10, 
20}, {30}}),
+                                                     array_int_type, "arr1"));
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{100, 
200}, {300}}),
+                                                     array_int_type, "arr2"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    block.insert({make_int_column({0, 0}), int_type, "unused0"});
+    block.insert({make_int_column({1, 1}), int_type, "unused1"});
+    block.insert({make_int_column({2, 2}), int_type, "unused2"});
+    block.insert({make_int_column({3, 3}), int_type, "unused3"});
+    block.insert({make_int_array_column({{1, 2}, {3}}), array_int_type, 
"arr3"});
+
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 
block.rows(), result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+
+    const auto& outer_array = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(outer_array.size(), 2);
+    ASSERT_EQ(outer_array.get_offsets()[0], 2);
+    ASSERT_EQ(outer_array.get_offsets()[1], 3);
+
+    const auto* nullable_inner_arrays =
+            check_and_get_column<ColumnNullable>(&outer_array.get_data());
+    ASSERT_NE(nullable_inner_arrays, nullptr);
+    const auto& inner_arrays =
+            assert_cast<const 
ColumnArray&>(nullable_inner_arrays->get_nested_column());
+    ASSERT_EQ(inner_arrays.size(), 3);
+    ASSERT_EQ(inner_arrays.get_offsets()[0], 2);
+    ASSERT_EQ(inner_arrays.get_offsets()[1], 4);
+    ASSERT_EQ(inner_arrays.get_offsets()[2], 5);
+
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&inner_arrays.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 5);
+    EXPECT_EQ(values.get_element(0), 1089);
+    EXPECT_EQ(values.get_element(1), 1176);
+    EXPECT_EQ(values.get_element(2), 4179);
+    EXPECT_EQ(values.get_element(3), 4356);
+    EXPECT_EQ(values.get_element(4), 9801);
+}
+
+TEST(ArrayMapFunctionTest, ThreeLevelNestedLambdaCapturesAllOuterArguments) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+    auto three_level_array_int_type = 
std::make_shared<DataTypeArray>(nested_array_int_type);
+
+    auto root = VLambdaFunctionCallExpr::create_shared(
+            make_lambda_call_node(three_level_array_int_type, 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(nested_array_int_type, 
{"x"}));
+    auto middle_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto middle_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"y"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"z"}));
+    auto add_zy = std::make_shared<MockAddExpr>(int_type);
+    auto add_zyx = std::make_shared<MockAddExpr>(int_type);
+
+    // This mirrors:
+    //   array_map(x -> array_map(y -> array_map(z -> z + y + x, [1, 2]),
+    //                           [10, 20]),
+    //             [100])
+    // It verifies LambdaExecutionContext works as a stack: the innermost z
+    // shadows nothing, y is resolved from the middle frame, and x is resolved
+    // from the outer frame.
+    auto z = VColumnRef::create_shared(make_column_ref_node(0, "z", int_type));
+    auto y = VColumnRef::create_shared(make_column_ref_node(7, "y", int_type));
+    auto x = VColumnRef::create_shared(make_column_ref_node(9, "x", int_type));
+
+    add_zy->add_child(z);
+    add_zy->add_child(y);
+    add_zyx->add_child(add_zy);
+    add_zyx->add_child(x);
+    inner_lambda->add_child(add_zyx);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    middle_lambda->add_child(inner_call);
+    middle_call->add_child(middle_lambda);
+    
middle_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{10,
 20}}),
+                                                                 
array_int_type, "middle_array"));
+    outer_lambda->add_child(middle_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{100}}),
 array_int_type,
+                                                     "outer_array"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 1, result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+
+    const auto& level3_arrays = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(level3_arrays.size(), 1);
+    ASSERT_EQ(level3_arrays.get_offsets()[0], 1);
+
+    const auto* nullable_level2_arrays =
+            check_and_get_column<ColumnNullable>(&level3_arrays.get_data());
+    ASSERT_NE(nullable_level2_arrays, nullptr);
+    const auto& level2_arrays =
+            assert_cast<const 
ColumnArray&>(nullable_level2_arrays->get_nested_column());
+    ASSERT_EQ(level2_arrays.size(), 1);
+    ASSERT_EQ(level2_arrays.get_offsets()[0], 2);
+
+    const auto* nullable_level1_arrays =
+            check_and_get_column<ColumnNullable>(&level2_arrays.get_data());
+    ASSERT_NE(nullable_level1_arrays, nullptr);
+    const auto& level1_arrays =
+            assert_cast<const 
ColumnArray&>(nullable_level1_arrays->get_nested_column());
+    ASSERT_EQ(level1_arrays.size(), 2);
+    ASSERT_EQ(level1_arrays.get_offsets()[0], 2);
+    ASSERT_EQ(level1_arrays.get_offsets()[1], 4);
+
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&level1_arrays.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 4);
+    EXPECT_EQ(values.get_element(0), 111);
+    EXPECT_EQ(values.get_element(1), 112);
+    EXPECT_EQ(values.get_element(2), 121);
+    EXPECT_EQ(values.get_element(3), 122);
+}
+
+TEST(ArrayMapFunctionTest, 
NestedLambdaCapturesOuterArgumentWithSameElementType) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"x"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"y"}));
+    auto subtract = std::make_shared<MockSubtractExpr>(int_type);
+
+    // This mirrors array_map(x -> array_map(y -> y - x, [1, 2]), [10, 20]).
+    // The captured outer x and the inner y both use INT type, so only names
+    // plus lambda scope can disambiguate them. The non-ordinal id of outer x
+    // covers FE plans where lambda argument ColumnRef ids are not 0-based.
+    auto outer_x = VColumnRef::create_shared(make_column_ref_node(1, "x", 
int_type));
+    auto inner_y = VColumnRef::create_shared(make_column_ref_node(0, "y", 
int_type));
+
+    subtract->add_child(inner_y);
+    subtract->add_child(outer_x);
+    inner_lambda->add_child(subtract);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    outer_lambda->add_child(inner_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{10, 
20}}),
+                                                     array_int_type, 
"outer_array"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    // No ordinary input column is needed by this expression. The outer lambda
+    // block only appends its own x argument, while the inner lambda resolves y
+    // from the nearest frame and x from the outer frame.
+    Block block;
+
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 1, result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+
+    const auto& outer_array = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(outer_array.size(), 1);
+    ASSERT_EQ(outer_array.get_offsets()[0], 2);
+
+    const auto* nullable_inner_arrays =
+            check_and_get_column<ColumnNullable>(&outer_array.get_data());
+    ASSERT_NE(nullable_inner_arrays, nullptr);
+    const auto& inner_arrays =
+            assert_cast<const 
ColumnArray&>(nullable_inner_arrays->get_nested_column());
+    ASSERT_EQ(inner_arrays.size(), 2);
+    ASSERT_EQ(inner_arrays.get_offsets()[0], 2);
+    ASSERT_EQ(inner_arrays.get_offsets()[1], 4);
+
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&inner_arrays.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 4);
+    EXPECT_EQ(values.get_element(0), -9);
+    EXPECT_EQ(values.get_element(1), -8);
+    EXPECT_EQ(values.get_element(2), -19);
+    EXPECT_EQ(values.get_element(3), -18);
+}
+
+TEST(ArrayMapFunctionTest, 
NestedLambdaCapturesOuterArgumentBeforeInnerArgument) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_int_type = std::make_shared<DataTypeArray>(int_type);
+    auto nested_array_int_type = 
std::make_shared<DataTypeArray>(array_int_type);
+
+    auto root =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(nested_array_int_type,
 2));
+    auto outer_lambda =
+            
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(array_int_type, 
{"x"}));
+    auto inner_call =
+            
VLambdaFunctionCallExpr::create_shared(make_lambda_call_node(array_int_type, 
2));
+    auto inner_lambda = 
VLambdaFunctionExpr::create_shared(make_lambda_expr_node(int_type, {"y"}));
+    auto subtract = std::make_shared<MockSubtractExpr>(int_type);
+
+    // This mirrors array_map(x -> array_map(y -> x - y, [1, 2]), [10, 20]).
+    // The outer x and inner y both use INT type. FE-provided lambda argument
+    // names must make x bind to the outer lambda even when x is visited before
+    // y in the inner lambda body. The non-ordinal id of outer x covers FE 
plans
+    // where lambda argument ColumnRef ids are not 0-based.
+    auto outer_x = VColumnRef::create_shared(make_column_ref_node(1, "x", 
int_type));
+    auto inner_y = VColumnRef::create_shared(make_column_ref_node(0, "y", 
int_type));
+
+    subtract->add_child(outer_x);
+    subtract->add_child(inner_y);
+    inner_lambda->add_child(subtract);
+    inner_call->add_child(inner_lambda);
+    
inner_call->add_child(std::make_shared<MockConstColumnExpr>(make_int_array_column({{1,
 2}}),
+                                                                
array_int_type, "inner_array"));
+    outer_lambda->add_child(inner_call);
+    root->add_child(outer_lambda);
+    
root->add_child(std::make_shared<MockColumnExpr>(make_int_array_column({{10, 
20}}),
+                                                     array_int_type, 
"outer_array"));
+
+    VExprContext context(root);
+    open_expr(root, &context);
+
+    Block block;
+
+    ColumnPtr result;
+    auto status = root->execute_column(&context, &block, nullptr, 1, result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+
+    const auto& outer_array = assert_cast<const ColumnArray&>(*result);
+    ASSERT_EQ(outer_array.size(), 1);
+    ASSERT_EQ(outer_array.get_offsets()[0], 2);
+
+    const auto* nullable_inner_arrays =
+            check_and_get_column<ColumnNullable>(&outer_array.get_data());
+    ASSERT_NE(nullable_inner_arrays, nullptr);
+    const auto& inner_arrays =
+            assert_cast<const 
ColumnArray&>(nullable_inner_arrays->get_nested_column());
+    ASSERT_EQ(inner_arrays.size(), 2);
+    ASSERT_EQ(inner_arrays.get_offsets()[0], 2);
+    ASSERT_EQ(inner_arrays.get_offsets()[1], 4);
+
+    const auto* nullable_values = 
check_and_get_column<ColumnNullable>(&inner_arrays.get_data());
+    ASSERT_NE(nullable_values, nullptr);
+    const auto& values = assert_cast<const 
ColumnInt32&>(nullable_values->get_nested_column());
+    ASSERT_EQ(values.size(), 4);
+    EXPECT_EQ(values.get_element(0), 9);
+    EXPECT_EQ(values.get_element(1), 8);
+    EXPECT_EQ(values.get_element(2), 19);
+    EXPECT_EQ(values.get_element(3), 18);
+}
+
+} // namespace doris
diff --git a/be/test/exprs/vcolumn_ref_test.cpp 
b/be/test/exprs/vcolumn_ref_test.cpp
new file mode 100644
index 00000000000..c8eaa609498
--- /dev/null
+++ b/be/test/exprs/vcolumn_ref_test.cpp
@@ -0,0 +1,148 @@
+// 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.
+
+#include "exprs/vcolumn_ref.h"
+
+#include <gen_cpp/Exprs_types.h>
+#include <gen_cpp/Types_types.h>
+#include <gtest/gtest.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "common/exception.h"
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_number.h"
+#include "exprs/vexpr_context.h"
+#include "runtime/descriptors.h"
+#include "runtime/runtime_state.h"
+
+namespace doris {
+
+static TTypeDesc make_int_type_desc() {
+    TTypeDesc type_desc;
+    TTypeNode type_node;
+    TScalarType scalar_type;
+    scalar_type.__set_type(TPrimitiveType::INT);
+    type_node.__set_type(TTypeNodeType::SCALAR);
+    type_node.__set_scalar_type(scalar_type);
+    type_desc.types.push_back(type_node);
+    return type_desc;
+}
+
+static TExprNode make_column_ref_node(int column_id, const std::string& 
column_name) {
+    TExprNode node;
+    node.__set_node_type(TExprNodeType::COLUMN_REF);
+    node.__set_num_children(0);
+    node.__set_type(make_int_type_desc());
+    node.__set_is_nullable(false);
+
+    TColumnRef column_ref;
+    column_ref.__set_column_id(column_id);
+    column_ref.__set_column_name(column_name);
+    node.__set_column_ref(column_ref);
+    return node;
+}
+
+static ColumnPtr make_int_column(const std::vector<int32_t>& values) {
+    auto column = ColumnInt32::create();
+    for (auto value : values) {
+        column->insert_value(value);
+    }
+    return column;
+}
+
+static Block make_int_block() {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    Block block;
+    block.insert({make_int_column({10, 11}), int_type, "c0"});
+    block.insert({make_int_column({20, 21}), int_type, "c1"});
+    block.insert({make_int_column({30, 31}), int_type, "c2"});
+    return block;
+}
+
+static void open_expr(const VExprSPtr& expr, VExprContext* context) {
+    RuntimeState state;
+    RowDescriptor row_desc;
+    ASSERT_TRUE(expr->prepare(&state, row_desc, context).ok());
+    ASSERT_TRUE(expr->open(&state, context, 
FunctionContext::THREAD_LOCAL).ok());
+}
+
+static std::vector<int32_t> get_int_values(const ColumnPtr& column) {
+    const auto* int_column = assert_cast<const ColumnInt32*>(column.get());
+    std::vector<int32_t> values;
+    for (auto value : int_column->get_data()) {
+        values.push_back(value);
+    }
+    return values;
+}
+
+TEST(VColumnRefTest, SetGapOverridesPreviousGap) {
+    auto ref = VColumnRef::create_shared(make_column_ref_node(0, "x"));
+    VExprContext context(ref);
+    open_expr(ref, &context);
+
+    auto block = make_int_block();
+    ColumnPtr result;
+
+    EXPECT_FALSE(ref->has_gap());
+    ref->set_gap(0);
+    EXPECT_TRUE(ref->has_gap());
+
+    ref->set_gap(1);
+    auto status = ref->execute_column(&context, &block, nullptr, block.rows(), 
result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+    EXPECT_EQ(get_int_values(result), std::vector<int32_t>({20, 21}));
+
+    result.reset();
+    ref->set_gap(2);
+    status = ref->execute_column(&context, &block, nullptr, block.rows(), 
result);
+    ASSERT_TRUE(status.ok()) << status.to_string();
+    EXPECT_EQ(get_int_values(result), std::vector<int32_t>({30, 31}));
+}
+
+TEST(VColumnRefTest, OutOfRangeColumnPositionReturnsError) {
+    auto ref = VColumnRef::create_shared(make_column_ref_node(1, "x"));
+    VExprContext context(ref);
+    open_expr(ref, &context);
+
+    auto block = make_int_block();
+    ref->set_gap(3);
+
+    ColumnPtr result;
+    auto status = ref->execute_column(&context, &block, nullptr, block.rows(), 
result);
+    EXPECT_FALSE(status.ok());
+    EXPECT_NE(status.to_string().find("input block not contain column ref x"), 
std::string::npos);
+    EXPECT_NE(status.to_string().find("column_id=1"), std::string::npos);
+    EXPECT_NE(status.to_string().find("gap=3"), std::string::npos);
+}
+
+TEST(VColumnRefTest, OutOfRangeExecuteTypeThrowsException) {
+    auto ref = VColumnRef::create_shared(make_column_ref_node(1, "x"));
+    VExprContext context(ref);
+    open_expr(ref, &context);
+
+    auto block = make_int_block();
+    ref->set_gap(3);
+
+    EXPECT_THROW({ static_cast<void>(ref->execute_type(&block)); }, Exception);
+}
+
+} // namespace doris
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/analysis/ExprToThriftVisitor.java 
b/fe/fe-core/src/main/java/org/apache/doris/analysis/ExprToThriftVisitor.java
index 9d5fd94ed3e..3df252e8fed 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/analysis/ExprToThriftVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/analysis/ExprToThriftVisitor.java
@@ -497,6 +497,7 @@ public class ExprToThriftVisitor extends ExprVisitor<Void, 
TExprNode> {
     @Override
     public Void visitLambdaFunctionExpr(LambdaFunctionExpr expr, TExprNode 
msg) {
         msg.setNodeType(TExprNodeType.LAMBDA_FUNCTION_EXPR);
+        msg.setLambdaArgumentNames(expr.getNames());
         return null;
     }
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/analysis/ExprToThriftBehaviorTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/analysis/ExprToThriftBehaviorTest.java
index 68e69cf3e27..ac40a4f174f 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/analysis/ExprToThriftBehaviorTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/analysis/ExprToThriftBehaviorTest.java
@@ -274,6 +274,22 @@ public class ExprToThriftBehaviorTest {
         Assertions.assertEquals(TExprNodeType.STRING_LITERAL, 
result.get(2).getNodes().get(0).node_type);
     }
 
+    @Test
+    public void testLambdaFunctionExprSerializesArgumentNames() {
+        SlotRef slotX = new SlotRef(null, "x");
+        SlotRef slotY = new SlotRef(null, "yy");
+        LambdaFunctionExpr expr = new LambdaFunctionExpr(
+                new IntLiteral(1L), Lists.newArrayList("x", "yy"),
+                Lists.newArrayList(slotX, slotY), false);
+
+        TExprNode node = firstNode(expr);
+
+        Assertions.assertEquals(TExprNodeType.LAMBDA_FUNCTION_EXPR, 
node.node_type);
+        Assertions.assertFalse(node.isSetLabel());
+        Assertions.assertTrue(node.isSetLambdaArgumentNames());
+        Assertions.assertEquals(Lists.newArrayList("x", "yy"), 
node.getLambdaArgumentNames());
+    }
+
     // ======================== Helpers ========================
 
     private static TExprNode firstNode(Expr expr) {
diff --git a/gensrc/thrift/Exprs.thrift b/gensrc/thrift/Exprs.thrift
index a17cd140c93..812a65740a7 100644
--- a/gensrc/thrift/Exprs.thrift
+++ b/gensrc/thrift/Exprs.thrift
@@ -329,6 +329,10 @@ struct TExprNode {
   39: optional bool is_cast_nullable
   40: optional TSearchParam search_param
   41: optional bool short_circuit_evaluation
+  // Lambda argument names in the current lambda scope. It is used by BE to
+  // distinguish current-scope lambda arguments from captured outer lambda
+  // arguments when nested lambda expressions contain duplicated column ids.
+  42: optional list<string> lambda_argument_names
 }
 
 // A flattened representation of a tree of Expr nodes, obtained by depth-first
diff --git 
a/regression-test/data/query_p0/sql_functions/array_functions/test_nested_array_map.out
 
b/regression-test/data/query_p0/sql_functions/array_functions/test_nested_array_map.out
new file mode 100644
index 00000000000..32404ea64dc
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/array_functions/test_nested_array_map.out
@@ -0,0 +1,13 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !select --
+1      [5, 7, 9]
+2      [11, 22, 3]
+
+-- !select2 --
+[[-9, -8], [-19, -18]]
+
+-- !select_nested_array_sortby --
+[[1, 3, 5, 9], [5, 3, 9, 1], [9, 5, 3, 1]]
+
+-- !select_same_name_shadow --
+[[2, 3], [4, 5]]
diff --git 
a/regression-test/suites/query_p0/sql_functions/array_functions/test_nested_array_map.groovy
 
b/regression-test/suites/query_p0/sql_functions/array_functions/test_nested_array_map.groovy
new file mode 100644
index 00000000000..7741ce47ff2
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/array_functions/test_nested_array_map.groovy
@@ -0,0 +1,90 @@
+// 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_nested_array_map") {
+    sql "DROP TABLE IF EXISTS test_nested_array_map_insert_src"
+    sql "DROP TABLE IF EXISTS test_nested_array_map_insert_dst"
+
+    sql """
+        CREATE TABLE test_nested_array_map_insert_src (
+            id INT,
+            bucket_counts ARRAY<BIGINT>
+        ) ENGINE=OLAP
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1"
+        )
+    """
+
+    sql """
+        CREATE TABLE test_nested_array_map_insert_dst (
+            id INT,
+            bucket_counts ARRAY<BIGINT>
+        ) ENGINE=OLAP
+        DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1"
+        )
+    """
+
+    sql """
+        INSERT INTO test_nested_array_map_insert_src VALUES
+            (1, [1, 2, 3]),
+            (1, [4, 5, 6]),
+            (2, [10, 20]),
+            (2, [1, 2, 3])
+    """
+
+    sql """
+        INSERT INTO test_nested_array_map_insert_dst (id, bucket_counts)
+        WITH rollup_grouped AS (
+            SELECT
+                id,
+                ARRAY_AGG(bucket_counts) AS bucket_count_arrays,
+                MAX(ARRAY_SIZE(bucket_counts)) AS max_bucket_len
+            FROM test_nested_array_map_insert_src
+            GROUP BY id
+        )
+        SELECT
+            id,
+            ARRAY_MAP(
+                i -> ARRAY_SUM(ARRAY_MAP(a -> COALESCE(a[CAST(i AS INT)], 0), 
bucket_count_arrays)),
+                ARRAY_RANGE(1, max_bucket_len + 1)
+            ) AS bucket_counts
+        FROM rollup_grouped
+    """
+
+    order_qt_select """
+        SELECT id, bucket_counts
+        FROM test_nested_array_map_insert_dst
+        ORDER BY id
+    """
+
+    qt_select2 """
+        select array_map(x -> array_map(y -> y - x, [1, 2]), [10, 20]);
+    """
+
+    qt_select_nested_array_sortby """
+        select array_map(x -> array_sortby(y -> abs(y - x), [5, 1, 3, 9]), [2, 
6, 8]);
+    """
+
+    qt_select_same_name_shadow """
+        select array_map(x -> array_map(x -> x + 1, x), [[1, 2], [3, 4]]);
+    """
+}


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

Reply via email to