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


##########
be/src/common/config.cpp:
##########
@@ -75,6 +75,15 @@ DEFINE_Int32(arrow_flight_sql_port, "8050");
 // Validate Arrow input buffers in opted-in Arrow readers before converting 
them to Doris columns.
 DEFINE_Bool(enable_arrow_input_validation, "true");
 
+// Max bytes of a single utf8/binary (int32-offset) column allowed in one 
Arrow Flight result
+// RecordBatch. Arrow utf8/binary offsets are int32, so a column's data in one 
batch must stay
+// below 2^31 (= INT32_MAX + 1). When a block would exceed this, the Arrow 
Flight readers split
+// it by rows into multiple batches so each batch stays valid. Default is the 
hard 2^31 limit;
+// only lower it (e.g. in tests) to exercise the split path without 
materializing 2GB of data.
+DEFINE_mInt64(arrow_flight_result_max_utf8_bytes, "2147483648");

Review Comment:
   [P1] Do not expose this validator through the broken mutable path
   
   `DEFINE_mInt64` exposes this value through `/api/update_config`, but 
`UPDATE_FIELD` invokes the registered zero-argument validator before assigning 
`new_value`, so it validates the old default and then accepts `0`, `-1`, or a 
value above 2^31. Zero makes every nonempty result hit the row-limit error, 
while a negative value becomes a huge `size_t` and disables splitting; after 
that, a valid recovery update is rejected because the invalid old value is what 
gets validated. With `persist=true`, the unchecked value is also written to 
`be_custom.conf` and can prevent the next startup. Since the comment says this 
is lowered only in tests, please make it non-mutable, or fix the update path to 
validate the candidate before committing it, and add `set_config` coverage.
   



##########
be/src/format/arrow/arrow_block_convertor.cpp:
##########
@@ -270,6 +277,109 @@ Status convert_to_arrow_batch(const Block& block, const 
std::shared_ptr<arrow::S
     return converter.convert(result);
 }
 
+namespace {
+
+// Only utf8/binary use int32 offsets and can overflow at 2^31; fixed-size and 
large_* cannot.
+bool is_int32_offset_binary_type(const std::shared_ptr<arrow::DataType>& type) 
{
+    return type->id() == arrow::Type::STRING || type->id() == 
arrow::Type::BINARY;
+}
+
+// Byte length written into the arrow string/binary array for `row` (0 for 
null), mirroring
+// DataTypeStringSerDeBase::write_column_to_arrow which appends 
get_data_at(row).size bytes.
+size_t arrow_value_byte_size_at(const IColumn& column, size_t row) {
+    const IColumn* data = &column;
+    if (const auto* nullable = check_and_get_column<ColumnNullable>(column)) {
+        if (nullable->is_null_at(row)) {
+            return 0;
+        }
+        data = &nullable->get_nested_column();
+    }
+    return data->get_data_at(row).size;
+}
+
+} // namespace
+
+Status convert_to_arrow_batches(const Block& block, const 
std::shared_ptr<arrow::Schema>& schema,
+                                arrow::MemoryPool* pool,
+                                
std::vector<std::shared_ptr<arrow::RecordBatch>>* results,
+                                const cctz::time_zone& timezone_obj) {
+    results->clear();
+    const int num_fields = schema->num_fields();
+    if (block.columns() != static_cast<size_t>(num_fields)) {
+        return Status::InvalidArgument("number fields not match");
+    }
+    const size_t num_rows = block.rows();
+    const auto max_bytes = 
static_cast<size_t>(config::arrow_flight_result_max_utf8_bytes);
+
+    // Fast path: O(#string cols), each byte_size() is O(1) and >= the actual 
arrow data size.
+    // If no int32-offset string/binary column reaches the limit, emit the 
whole block as one
+    // batch -- zero added cost on the normal path, and this never misses an 
overflow.
+    std::vector<int> big_fields;
+    for (int idx = 0; idx < num_fields; ++idx) {
+        if (is_int32_offset_binary_type(schema->field(idx)->type()) &&
+            block.get_by_position(idx).column->byte_size() >= max_bytes) {
+            big_fields.push_back(idx);
+        }
+    }
+    if (big_fields.empty() || num_rows == 0) {
+        std::shared_ptr<arrow::RecordBatch> batch;
+        RETURN_IF_ERROR(convert_to_arrow_batch(block, schema, pool, &batch, 
timezone_obj));
+        results->push_back(std::move(batch));
+        return Status::OK();
+    }
+
+    // Materialize the (rarely) const columns once so per-row size probing is 
cheap and correct.
+    std::vector<ColumnPtr> big_cols;
+    big_cols.reserve(big_fields.size());
+    for (int idx : big_fields) {
+        
big_cols.push_back(block.get_by_position(idx).column->convert_to_full_column_if_const());
+    }
+
+    // Walk rows, cutting a batch right before any tracked column would reach 
the int32 limit;
+    // the tightest-growing column decides each cut.
+    std::vector<size_t> running(big_cols.size());
+    std::vector<size_t> row_bytes(big_cols.size());
+    size_t start = 0;
+    while (start < num_rows) {
+        running.assign(running.size(), 0);
+        size_t end = start;
+        while (end < num_rows) {
+            bool fits = true;
+            for (size_t c = 0; c < big_cols.size(); ++c) {
+                const size_t rb = arrow_value_byte_size_at(*big_cols[c], end);
+                if (rb >= max_bytes) {
+                    return Status::InternalError(
+                            "Arrow Flight cannot send column '{}': a single 
value at row {} is {} "
+                            "bytes, exceeding the Arrow utf8/binary int32 
offset limit of {} "
+                            "bytes.",
+                            schema->field(big_fields[c])->name(), end, rb, 
max_bytes);
+                }
+                row_bytes[c] = rb;
+                if (running[c] + rb >= max_bytes) {

Review Comment:
   [P1] Bound the complete Flight IPC body, not only value bytes
   
   The exact per-buffer threshold is covered separately; even after that is 
corrected, Arrow 17's `FlightPayload::Validate()` rejects an 
`ipc_message.body_length` above `INT32_MAX`, while this cut accounts only for 
one tracked value buffer. Offsets, validity buffers, padding, fixed-width 
columns, and other individually sub-limit string columns all contribute to the 
same body. The existing BigString test is already a counterexample: its 
2,147,483,640 value bytes pass this `< 2^31` check by 8 bytes, but the offset 
buffer makes the Flight payload too large even though `RecordBatch::Validate()` 
succeeds. Please size/cut for the complete IPC body as well and add a 
Flight-payload-level 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