github-actions[bot] commented on code in PR #66085:
URL: https://github.com/apache/doris/pull/66085#discussion_r3655092228


##########
be/src/exec/operator/operator.cpp:
##########
@@ -325,77 +325,35 @@ Status OperatorXBase::do_projections(RuntimeState* state, 
Block* origin_block,
     }
     Block input_block = *origin_block;
 
-    size_t bytes_usage = 0;
-    ColumnsWithTypeAndName new_columns;
+    std::vector<int> result_column_ids;

Review Comment:
   [P1] Keep projection allocations in the next-round reservation estimate
   
   This rewrite removes every update to `local_state->_estimate_memory_usage`. 
`OperatorX::get_reserve_mem_size()` reads that value, and `PipelineTask` 
consumes/resets it before the next `get_block_after_projects()` to decide 
whether to pause/revoke/spill. Moving a result into `output_block` after 
expression evaluation does not remove the allocation: a large string/complex 
intermediate or final projection will now reserve only the minimum and can 
allocate through the workload-group pressure gate. Please keep an observed or 
sound pre-execution estimate for newly allocated projection data (without 
counting genuinely shared pass-through storage) and cover a repeated large 
variable-width projection with reservation enabled.



##########
be/src/exec/operator/operator.cpp:
##########
@@ -325,77 +325,35 @@ Status OperatorXBase::do_projections(RuntimeState* state, 
Block* origin_block,
     }
     Block input_block = *origin_block;
 
-    size_t bytes_usage = 0;
-    ColumnsWithTypeAndName new_columns;
+    std::vector<int> result_column_ids;
     for (const auto& projections : local_state->_intermediate_projections) {
-        if (projections.empty()) {
-            return Status::InternalError("meet empty intermediate projection, 
node id: {}",
-                                         node_id());
-        }
-        new_columns.resize(projections.size());
+        result_column_ids.resize(projections.size());
         for (int i = 0; i < projections.size(); i++) {
-            RETURN_IF_ERROR(projections[i]->execute(&input_block, 
new_columns[i]));
-            if (new_columns[i].column->size() != rows) {
-                return Status::InternalError(
-                        "intermediate projection result column size {} not 
equal input rows {}, "
-                        "expr: {}",
-                        new_columns[i].column->size(), rows,
-                        projections[i]->root()->debug_string());
-            }
-        }
-        Block tmp_block {new_columns};
-        bytes_usage += tmp_block.allocated_bytes();
-        input_block.swap(tmp_block);
-    }
-
-    if (input_block.rows() != rows) {
-        return Status::InternalError(
-                "after intermediate projections input block rows {} not equal 
origin rows {}, "
-                "input_block: {}",
-                input_block.rows(), rows, input_block.dump_structure());
-    }
-    auto insert_column_datas = [&](auto& to, ColumnPtr& from, size_t rows) {
-        if (is_column_nullable(*to) && !is_column_nullable(*from)) {
-            if (_keep_origin || !from->is_exclusive()) {
-                auto& null_column = reinterpret_cast<ColumnNullable&>(*to);
-                null_column.get_nested_column().insert_range_from(*from, 0, 
rows);
-                null_column.get_null_map_column().get_data().resize_fill(rows, 
0);
-                bytes_usage += null_column.allocated_bytes();
-            } else {
-                to = make_nullable(from, false)->assert_mutable();
-            }
-        } else {
-            if (_keep_origin || !from->is_exclusive()) {
-                to->insert_range_from(*from, 0, rows);
-                bytes_usage += from->allocated_bytes();
-            } else {
-                to = from->assert_mutable();
-            }
+            RETURN_IF_ERROR(projections[i]->execute(&input_block, 
&result_column_ids[i]));
         }
-    };
+        input_block.shuffle_columns(result_column_ids);
+    }
 
+    DCHECK_EQ(rows, input_block.rows());
     auto scoped_mutable_block = 
VectorizedUtils::build_scoped_mutable_mem_reuse_block(
             output_block, *_output_row_descriptor);
     auto& mutable_block = scoped_mutable_block.mutable_block();
     auto& mutable_columns = mutable_block.mutable_columns();
-    if (rows != 0) {
-        DCHECK_EQ(mutable_columns.size(), local_state->_projections.size()) << 
debug_string();
-        for (int i = 0; i < mutable_columns.size(); ++i) {
-            ColumnPtr column_ptr;
-            
RETURN_IF_ERROR(local_state->_projections[i]->execute(&input_block, 
column_ptr));
-            if (column_ptr->size() != rows) {
-                return Status::InternalError(
-                        "projection result column size {} not equal input rows 
{}, expr: {}",
-                        column_ptr->size(), rows,
-                        local_state->_projections[i]->root()->debug_string());
-            }
-            column_ptr = column_ptr->convert_to_full_column_if_const();
-            bytes_usage += column_ptr->allocated_bytes();
-            insert_column_datas(mutable_columns[i], column_ptr, rows);
+    DCHECK_EQ(mutable_columns.size(), local_state->_projections.size()) << 
debug_string();
+
+    for (int i = 0; i < mutable_columns.size(); ++i) {

Review Comment:
   [P2] Avoid cloning pass-through slots while discarding the reusable buffer
   
   For a final slot ref, `origin_block`, the shallow `input_block`, and 
`column_ptr` all own the same column when `IColumn::mutate()` runs, so 
`shallow_mutate()` takes its deep-clone path. Meanwhile `mutable_columns[i]` is 
the retained-capacity output column cleared by `PipelineTask`, and this 
assignment destroys it. A slot-only projection therefore performs a fresh 
allocation plus full-column copy on every block. Please publish the immutable 
result without requiring mutable ownership, or release the source owners before 
a real transfer, and add a reused-block slot projection benchmark/test.



##########
be/src/exec/operator/operator.cpp:
##########
@@ -325,77 +325,35 @@ Status OperatorXBase::do_projections(RuntimeState* state, 
Block* origin_block,
     }
     Block input_block = *origin_block;
 
-    size_t bytes_usage = 0;
-    ColumnsWithTypeAndName new_columns;
+    std::vector<int> result_column_ids;
     for (const auto& projections : local_state->_intermediate_projections) {
-        if (projections.empty()) {
-            return Status::InternalError("meet empty intermediate projection, 
node id: {}",
-                                         node_id());
-        }
-        new_columns.resize(projections.size());
+        result_column_ids.resize(projections.size());
         for (int i = 0; i < projections.size(); i++) {
-            RETURN_IF_ERROR(projections[i]->execute(&input_block, 
new_columns[i]));
-            if (new_columns[i].column->size() != rows) {
-                return Status::InternalError(
-                        "intermediate projection result column size {} not 
equal input rows {}, "
-                        "expr: {}",
-                        new_columns[i].column->size(), rows,
-                        projections[i]->root()->debug_string());
-            }
-        }
-        Block tmp_block {new_columns};
-        bytes_usage += tmp_block.allocated_bytes();
-        input_block.swap(tmp_block);
-    }
-
-    if (input_block.rows() != rows) {
-        return Status::InternalError(
-                "after intermediate projections input block rows {} not equal 
origin rows {}, "
-                "input_block: {}",
-                input_block.rows(), rows, input_block.dump_structure());
-    }
-    auto insert_column_datas = [&](auto& to, ColumnPtr& from, size_t rows) {
-        if (is_column_nullable(*to) && !is_column_nullable(*from)) {
-            if (_keep_origin || !from->is_exclusive()) {
-                auto& null_column = reinterpret_cast<ColumnNullable&>(*to);
-                null_column.get_nested_column().insert_range_from(*from, 0, 
rows);
-                null_column.get_null_map_column().get_data().resize_fill(rows, 
0);
-                bytes_usage += null_column.allocated_bytes();
-            } else {
-                to = make_nullable(from, false)->assert_mutable();
-            }
-        } else {
-            if (_keep_origin || !from->is_exclusive()) {
-                to->insert_range_from(*from, 0, rows);
-                bytes_usage += from->allocated_bytes();
-            } else {
-                to = from->assert_mutable();
-            }
+            RETURN_IF_ERROR(projections[i]->execute(&input_block, 
&result_column_ids[i]));
         }
-    };
+        input_block.shuffle_columns(result_column_ids);
+    }
 
+    DCHECK_EQ(rows, input_block.rows());
     auto scoped_mutable_block = 
VectorizedUtils::build_scoped_mutable_mem_reuse_block(
             output_block, *_output_row_descriptor);
     auto& mutable_block = scoped_mutable_block.mutable_block();
     auto& mutable_columns = mutable_block.mutable_columns();
-    if (rows != 0) {
-        DCHECK_EQ(mutable_columns.size(), local_state->_projections.size()) << 
debug_string();
-        for (int i = 0; i < mutable_columns.size(); ++i) {
-            ColumnPtr column_ptr;
-            
RETURN_IF_ERROR(local_state->_projections[i]->execute(&input_block, 
column_ptr));
-            if (column_ptr->size() != rows) {
-                return Status::InternalError(
-                        "projection result column size {} not equal input rows 
{}, expr: {}",
-                        column_ptr->size(), rows,
-                        local_state->_projections[i]->root()->debug_string());
-            }
-            column_ptr = column_ptr->convert_to_full_column_if_const();
-            bytes_usage += column_ptr->allocated_bytes();
-            insert_column_datas(mutable_columns[i], column_ptr, rows);
+    DCHECK_EQ(mutable_columns.size(), local_state->_projections.size()) << 
debug_string();
+
+    for (int i = 0; i < mutable_columns.size(); ++i) {
+        ColumnPtr column_ptr;
+        RETURN_IF_ERROR(local_state->_projections[i]->execute(&input_block, 
column_ptr));
+        column_ptr = column_ptr->convert_to_full_column_if_const();
+        if (mutable_columns[i]->is_nullable() != column_ptr->is_nullable()) {
+            throw Exception(ErrorCode::INTERNAL_ERROR, "Nullable mismatch");
         }
-        DCHECK(mutable_block.rows() == rows);
+        mutable_columns[i] = IColumn::mutate(std::move(column_ptr));

Review Comment:
   [P1] Validate the result against the destination type before replacing its 
pointer
   
   This block keeps the `DataType` created from `_output_row_descriptor`, but 
`VSlotRef::execute_type(block)` intentionally validates a returned slot column 
against the runtime input type, which can differ from the expression's 
declared/output type. With equal nullability (for example declared/output `INT` 
but runtime `BIGINT`), the check above passes and this installs `ColumnInt64` 
under `DataTypeInt32`. The old typed `insert_range_from` failed loudly through 
release-enabled `assert_cast`; please validate against the destination type or 
materialize into the typed destination before replacing it.



##########
be/src/exec/operator/operator.cpp:
##########
@@ -325,77 +325,35 @@ Status OperatorXBase::do_projections(RuntimeState* state, 
Block* origin_block,
     }
     Block input_block = *origin_block;
 
-    size_t bytes_usage = 0;
-    ColumnsWithTypeAndName new_columns;
+    std::vector<int> result_column_ids;
     for (const auto& projections : local_state->_intermediate_projections) {
-        if (projections.empty()) {
-            return Status::InternalError("meet empty intermediate projection, 
node id: {}",
-                                         node_id());
-        }
-        new_columns.resize(projections.size());
+        result_column_ids.resize(projections.size());
         for (int i = 0; i < projections.size(); i++) {
-            RETURN_IF_ERROR(projections[i]->execute(&input_block, 
new_columns[i]));
-            if (new_columns[i].column->size() != rows) {
-                return Status::InternalError(
-                        "intermediate projection result column size {} not 
equal input rows {}, "
-                        "expr: {}",
-                        new_columns[i].column->size(), rows,
-                        projections[i]->root()->debug_string());
-            }
-        }
-        Block tmp_block {new_columns};
-        bytes_usage += tmp_block.allocated_bytes();
-        input_block.swap(tmp_block);
-    }
-
-    if (input_block.rows() != rows) {
-        return Status::InternalError(
-                "after intermediate projections input block rows {} not equal 
origin rows {}, "
-                "input_block: {}",
-                input_block.rows(), rows, input_block.dump_structure());
-    }
-    auto insert_column_datas = [&](auto& to, ColumnPtr& from, size_t rows) {
-        if (is_column_nullable(*to) && !is_column_nullable(*from)) {
-            if (_keep_origin || !from->is_exclusive()) {
-                auto& null_column = reinterpret_cast<ColumnNullable&>(*to);
-                null_column.get_nested_column().insert_range_from(*from, 0, 
rows);
-                null_column.get_null_map_column().get_data().resize_fill(rows, 
0);
-                bytes_usage += null_column.allocated_bytes();
-            } else {
-                to = make_nullable(from, false)->assert_mutable();
-            }
-        } else {
-            if (_keep_origin || !from->is_exclusive()) {
-                to->insert_range_from(*from, 0, rows);
-                bytes_usage += from->allocated_bytes();
-            } else {
-                to = from->assert_mutable();
-            }
+            RETURN_IF_ERROR(projections[i]->execute(&input_block, 
&result_column_ids[i]));
         }
-    };
+        input_block.shuffle_columns(result_column_ids);
+    }
 
+    DCHECK_EQ(rows, input_block.rows());

Review Comment:
   [P2] Keep a release-mode rejection for an empty intermediate stage
   
   `init()` still accepts an empty element in `intermediate_projections_list`; 
`resize(0)` followed by `shuffle_columns({})` creates a zero-column block whose 
`rows()` is zero. These replacement checks disappear in release builds, so 
final expressions execute with count zero and a nonempty input can return 
`Status::OK()` with no rows, whereas the deleted check returned 
`InternalError`. Please restore the empty-stage/runtime row-count invariant 
(the per-expression size checks can rely on `execute_column()`) and cover the 
release behavior with a malformed-plan/unit test.



##########
be/src/exec/operator/operator.cpp:
##########
@@ -325,77 +325,35 @@ Status OperatorXBase::do_projections(RuntimeState* state, 
Block* origin_block,
     }
     Block input_block = *origin_block;
 
-    size_t bytes_usage = 0;
-    ColumnsWithTypeAndName new_columns;
+    std::vector<int> result_column_ids;
     for (const auto& projections : local_state->_intermediate_projections) {
-        if (projections.empty()) {
-            return Status::InternalError("meet empty intermediate projection, 
node id: {}",
-                                         node_id());
-        }
-        new_columns.resize(projections.size());
+        result_column_ids.resize(projections.size());
         for (int i = 0; i < projections.size(); i++) {
-            RETURN_IF_ERROR(projections[i]->execute(&input_block, 
new_columns[i]));
-            if (new_columns[i].column->size() != rows) {
-                return Status::InternalError(
-                        "intermediate projection result column size {} not 
equal input rows {}, "
-                        "expr: {}",
-                        new_columns[i].column->size(), rows,
-                        projections[i]->root()->debug_string());
-            }
-        }
-        Block tmp_block {new_columns};
-        bytes_usage += tmp_block.allocated_bytes();
-        input_block.swap(tmp_block);
-    }
-
-    if (input_block.rows() != rows) {
-        return Status::InternalError(
-                "after intermediate projections input block rows {} not equal 
origin rows {}, "
-                "input_block: {}",
-                input_block.rows(), rows, input_block.dump_structure());
-    }
-    auto insert_column_datas = [&](auto& to, ColumnPtr& from, size_t rows) {
-        if (is_column_nullable(*to) && !is_column_nullable(*from)) {
-            if (_keep_origin || !from->is_exclusive()) {
-                auto& null_column = reinterpret_cast<ColumnNullable&>(*to);
-                null_column.get_nested_column().insert_range_from(*from, 0, 
rows);
-                null_column.get_null_map_column().get_data().resize_fill(rows, 
0);
-                bytes_usage += null_column.allocated_bytes();
-            } else {
-                to = make_nullable(from, false)->assert_mutable();
-            }
-        } else {
-            if (_keep_origin || !from->is_exclusive()) {
-                to->insert_range_from(*from, 0, rows);
-                bytes_usage += from->allocated_bytes();
-            } else {
-                to = from->assert_mutable();
-            }
+            RETURN_IF_ERROR(projections[i]->execute(&input_block, 
&result_column_ids[i]));
         }
-    };
+        input_block.shuffle_columns(result_column_ids);
+    }
 
+    DCHECK_EQ(rows, input_block.rows());
     auto scoped_mutable_block = 
VectorizedUtils::build_scoped_mutable_mem_reuse_block(
             output_block, *_output_row_descriptor);
     auto& mutable_block = scoped_mutable_block.mutable_block();
     auto& mutable_columns = mutable_block.mutable_columns();
-    if (rows != 0) {
-        DCHECK_EQ(mutable_columns.size(), local_state->_projections.size()) << 
debug_string();
-        for (int i = 0; i < mutable_columns.size(); ++i) {
-            ColumnPtr column_ptr;
-            
RETURN_IF_ERROR(local_state->_projections[i]->execute(&input_block, 
column_ptr));
-            if (column_ptr->size() != rows) {
-                return Status::InternalError(
-                        "projection result column size {} not equal input rows 
{}, expr: {}",
-                        column_ptr->size(), rows,
-                        local_state->_projections[i]->root()->debug_string());
-            }
-            column_ptr = column_ptr->convert_to_full_column_if_const();
-            bytes_usage += column_ptr->allocated_bytes();
-            insert_column_datas(mutable_columns[i], column_ptr, rows);
+    DCHECK_EQ(mutable_columns.size(), local_state->_projections.size()) << 
debug_string();
+
+    for (int i = 0; i < mutable_columns.size(); ++i) {
+        ColumnPtr column_ptr;
+        RETURN_IF_ERROR(local_state->_projections[i]->execute(&input_block, 
column_ptr));
+        column_ptr = column_ptr->convert_to_full_column_if_const();
+        if (mutable_columns[i]->is_nullable() != column_ptr->is_nullable()) {

Review Comment:
   [P1] Preserve the supported nullable widening here
   
   `VExpr::check_expr_output_type()` explicitly accepts `T -> Nullable(T)`, and 
`VExpr::execute_column()` also allows a declared nullable expression to return 
a physically non-nullable column when the evaluated rows contain no nulls. Such 
a plan passes prepare/expression execution but now throws here; the deleted 
branch attached an all-zero null map. Please preserve this one-way mismatch 
with `make_nullable(..., false)` (or equivalent) before transferring ownership, 
while still rejecting `Nullable(T) -> T`, and add a focused positive-row test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to