github-actions[bot] commented on code in PR #69040:
URL: https://github.com/apache/doris/pull/69040#discussion_r4374374677
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java:
##########
@@ -184,6 +185,27 @@ private static boolean isUnsafeDateArithmetic(Expression
expression) {
return false;
}
+ /** Reject FILE values in SQL keys that require comparison, ordering, or
hashing. */
+ public static void checkFileKey(Expression value, String operation) {
+ if (value.getDataType().typeContainsFile()) {
+ throw new AnalysisException(operation + " does not support FILE or
a type containing FILE: "
+ + value.getDataType().toSql());
+ }
+ }
+
+ /** Called by the existing recursive expression type check, without a
second plan traversal. */
+ public static void checkFileExpression(Expression expression) {
Review Comment:
[P2] Check FILE keys inside aggregate ORDER BY expressions
This reduced plan is admitted without a LogicalSort or WindowExpression:
```text
Aggregate(output=[GROUP_CONCAT(CAST(number AS STRING) ORDER BY f)])
Project(number, CAST(NAMED_STRUCT('uri', CONCAT('urn:',number),
'offset',NULL,'size',NULL,'content_type',NULL,
'checksum',NULL,'inline',NULL) AS FILE) AS f)
Numbers(number=2)
```
`GroupConcat` accepts ANY for its OrderExpression, while this recursive
check ignores that expression shape. `translateAggregateFunction` preserves the
FILE order argument; BE installs `AggregateFunctionSort`, and
`AggregateFunctionSortData::sort` calls `sort_block` ->
`ColumnFile::get_permutation`, which throws `FILE sorting is not supported`.
Apply the recursive `checkFileKey` restriction to OrderExpression children and
cover nested FILE keys too. This is a separate ordering path from the existing
aggregate-DISTINCT report: the trigger has no DISTINCT or deduplication key.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateFunctionCommand.java:
##########
@@ -445,6 +439,9 @@ private void checkUdfSupportedType(Type type, String
typePosition) throws Analys
for (StructField field : ((StructType) type).getFields()) {
checkUdfSupportedType(field.getType(), typePosition + " field
" + field.getName());
}
+ } else if (type.isObjectStored()) {
Review Comment:
[P2] Provide transport for the admitted Python FILE signatures
This accepts a Python function declared as `py(FILE) RETURNS INT` (the new
CREATE tests also assert acceptance of nested FILE arguments/returns). Calling
`py(f)` with a valid nonconstant six-field STRUCT-to-FILE value cannot execute:
`PythonFunctionCall::open` passes FILE into `PythonUDFMeta::serialize_to_json`,
whose `convert_types_to_schema` calls
`DorisArrowSchemaConvertor::convert_to_arrow_type`. That switch has no FILE
case and returns `Unknown primitive type(File) convert to Arrow type`;
ARRAY/MAP/STRUCT recurse into the same failure. Both FILE Arrow serde
directions explicitly return NotSupported as well. The scalar INT result passes
the FILE result-sink fence, so it does not prevent this path. Implement the
Python FILE schema/serde bridge, or recursively reject unsupported signatures
at CREATE, and add a CREATE-and-invoke case instead of validating catalog
acceptance alone.
##########
be/src/core/column/column_file.cpp:
##########
@@ -0,0 +1,470 @@
+// 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 "core/column/column_file.h"
+
+#include <crc32c/crc32c.h>
+
+#include <algorithm>
+#include <limits>
+
+#include "core/arena.h"
+#include "core/assert_cast.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_varbinary.h"
+#include "core/column/column_vector.h"
+#include "core/column/columns_common.h"
+#include "core/custom_allocator.h"
+#include "core/data_type/primitive_type.h"
+#include "core/field.h"
+#include "exec/common/sip_hash.h"
+#include "util/hash_util.hpp"
+
+namespace doris {
+namespace {
+
+MutableColumns make_file_children() {
+ MutableColumns children;
+ children.reserve(ColumnFile::NUM_CHILDREN);
+ children.push_back(ColumnNullable::create(ColumnString::create(),
ColumnUInt8::create()));
+ children.push_back(ColumnNullable::create(ColumnInt64::create(),
ColumnUInt8::create()));
+ children.push_back(ColumnNullable::create(ColumnInt64::create(),
ColumnUInt8::create()));
+ children.push_back(ColumnNullable::create(ColumnString::create(),
ColumnUInt8::create()));
+ children.push_back(ColumnNullable::create(ColumnString::create(),
ColumnUInt8::create()));
+ children.push_back(ColumnNullable::create(ColumnVarbinary::create(),
ColumnUInt8::create()));
+ return children;
+}
+
+// VARBINARY has no column hash implementation. Hash the lossless row encoding,
+// which includes every child null flag and length, without exposing SQL
hashing.
+template <typename Update>
+void hash_file_rows(const ColumnFile& column, size_t start, size_t end, const
uint8_t* null_map,
+ Update&& update) {
+ DorisVector<char> buffer;
+ for (size_t row = start; row < end; ++row) {
+ if (null_map != nullptr && null_map[row] != 0) {
+ continue;
+ }
+ buffer.resize(column.serialize_size_at(row));
+ column.serialize_impl(buffer.data(), row);
+ update(row, buffer.data(), buffer.size());
+ }
+}
+
+uint32_t file_crc(const char* data, size_t size, uint32_t hash) {
+ // A complete row can exceed the uint32 length accepted by zlib_crc_hash.
+ while (size != 0) {
+ const auto length =
+ static_cast<uint32_t>(std::min<size_t>(size,
std::numeric_limits<uint32_t>::max()));
+ hash = HashUtil::zlib_crc_hash(data, length, hash);
+ data += length;
+ size -= length;
+ }
+ return hash;
+}
+
+} // namespace
+
+ColumnFile::ColumnFile() : ColumnFile(make_file_children()) {}
+
+ColumnFile::ColumnFile(MutableColumns&& columns) {
+ if (columns.size() != NUM_CHILDREN) {
+ throw Exception(ErrorCode::INTERNAL_ERROR, "ColumnFile requires six
children, got {}",
+ columns.size());
+ }
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ _columns[i] = std::move(columns[i]);
+ }
+ sanity_check();
+ check_const_only_in_top_level();
+}
+
+void ColumnFile::sanity_check() const {
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ const auto* child = dynamic_cast<const
ColumnNullable*>(_columns[i].get());
+ if (child == nullptr) {
+ throw Exception(ErrorCode::INTERNAL_ERROR, "ColumnFile child {}
must be nullable", i);
+ }
+ const auto& nested = child->get_nested_column();
+ bool expected_type;
+ if (i == 1 || i == 2) {
+ expected_type = dynamic_cast<const ColumnInt64*>(&nested) !=
nullptr;
+ } else if (i == 5) {
+ expected_type = dynamic_cast<const ColumnVarbinary*>(&nested) !=
nullptr;
+ } else {
+ // Both string offset widths represent the same physical child
type.
+ expected_type = nested.is_column_string();
+ }
+ if (!expected_type) {
+ throw Exception(ErrorCode::INTERNAL_ERROR, "Invalid ColumnFile
child {} type {}", i,
+ nested.get_name());
+ }
+ child->sanity_check();
+ if (i != 0 && child->size() != size()) {
+ throw Exception(ErrorCode::INTERNAL_ERROR,
+ "ColumnFile child {} has {} rows, expected {}", i,
child->size(),
+ size());
+ }
+ }
+}
+
+MutableColumnPtr ColumnFile::clone_resized(size_t size) const {
+ MutableColumns children;
+ children.reserve(NUM_CHILDREN);
+ for (const auto& column : _columns) {
+ children.push_back(column->clone_resized(size));
+ }
+ return create(std::move(children));
+}
+
+Field ColumnFile::operator[](size_t n) const {
+ Field result;
+ get(n, result);
+ return result;
+}
+
+void ColumnFile::get(size_t n, Field& res) const {
+ File value;
+ value.reserve(NUM_CHILDREN);
+ for (const auto& column : _columns) {
+ value.push_back((*column)[n]);
+ }
+ res = Field::create_field<TYPE_FILE>(std::move(value));
+}
+
+void ColumnFile::insert(const Field& value) {
+ DCHECK_EQ(value.get_type(), TYPE_FILE);
+ const auto& fields = value.get<TYPE_FILE>();
+ if (fields.size() != NUM_CHILDREN) {
+ throw Exception(ErrorCode::INTERNAL_ERROR, "FILE Field requires six
children, got {}",
+ fields.size());
+ }
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ _columns[i]->insert(fields[i]);
+ }
+}
+
+void ColumnFile::insert_from(const IColumn& src, size_t n) {
+ const auto& file = assert_cast<const ColumnFile&>(src);
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ _columns[i]->insert_from(file.get_column(i), n);
+ }
+}
+
+void ColumnFile::insert_range_from(const IColumn& src, size_t start, size_t
length) {
+ const auto& file = assert_cast<const ColumnFile&>(src);
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ _columns[i]->insert_range_from(file.get_column(i), start, length);
+ }
+}
+
+void ColumnFile::insert_range_from_ignore_overflow(const IColumn& src, size_t
start,
+ size_t length) {
+ const auto& file = assert_cast<const ColumnFile&>(src);
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ _columns[i]->insert_range_from_ignore_overflow(file.get_column(i),
start, length);
+ }
+}
+
+void ColumnFile::insert_indices_from(const IColumn& src, const uint32_t* begin,
+ const uint32_t* end) {
+ const auto& file = assert_cast<const ColumnFile&>(src);
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ _columns[i]->insert_indices_from(file.get_column(i), begin, end);
+ }
+}
+
+void ColumnFile::insert_many_from(const IColumn& src, size_t position, size_t
length) {
+ const auto& file = assert_cast<const ColumnFile&>(src);
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ _columns[i]->insert_many_from(file.get_column(i), position, length);
+ }
+}
+
+void ColumnFile::replace_column_data(const IColumn&, size_t, size_t) {
+ // Like ColumnString/ColumnStruct, variable-length rows are replaced by
rebuilding columns.
+ throw Exception(ErrorCode::INTERNAL_ERROR,
+ "Method replace_column_data is not supported for
ColumnFile");
+}
+
+void ColumnFile::insert_default() {
+ for (auto& column : _columns) {
+ column->insert_default();
+ }
+}
+
+void ColumnFile::insert_many_defaults(size_t length) {
+ for (auto& column : _columns) {
+ column->insert_many_defaults(length);
+ }
+}
+
+void ColumnFile::pop_back(size_t n) {
+ for (auto& column : _columns) {
+ column->pop_back(n);
+ }
+}
+
+void ColumnFile::reserve(size_t n) {
+ for (auto& column : _columns) {
+ column->reserve(n);
+ }
+}
+
+void ColumnFile::resize(size_t n) {
+ if (n > size()) {
+ // ColumnNullable::resize alone does not initialize new null-map
entries.
+ insert_many_defaults(n - size());
+ } else {
+ for (auto& column : _columns) {
+ column->resize(n);
+ }
+ }
+}
+
+void ColumnFile::clear() {
+ for (auto& column : _columns) {
+ column->clear();
+ }
+}
+
+void ColumnFile::erase(size_t start, size_t length) {
+ DCHECK_LE(start, size());
+ DCHECK_LE(length, size() - start);
+ // Filtering also works for the arena-backed VARBINARY child, which has no
erase().
+ Filter keep(size(), 1);
+ std::fill(keep.begin() + start, keep.begin() + start + length, 0);
+ filter(keep);
+}
+
+ColumnPtr ColumnFile::filter(const Filter& filter, ssize_t result_size_hint)
const {
+ MutableColumns children;
+ children.reserve(NUM_CHILDREN);
+ for (const auto& column : _columns) {
+ children.push_back(IColumn::mutate(column->filter(filter,
result_size_hint)));
+ }
+ return create(std::move(children));
+}
+
+size_t ColumnFile::filter(const Filter& filter) {
+ column_match_filter_size(size(), filter.size());
+ const size_t result_size = _columns[0]->filter(filter);
+ for (size_t i = 1; i < NUM_CHILDREN - 1; ++i) {
+ const size_t child_size = _columns[i]->filter(filter);
+ DCHECK_EQ(child_size, result_size);
+ }
+ auto& inline_column = assert_cast<ColumnNullable&>(*_columns[NUM_CHILDREN
- 1]);
+ if (result_size == 0) {
+ inline_column.clear();
+ return 0;
+ }
+ // The arena remains owned by this column. Compact views without copying
large payloads.
+ auto& data =
assert_cast<ColumnVarbinary&>(inline_column.get_nested_column()).get_data();
+ size_t output_row = 0;
+ for (size_t row = 0; row < filter.size(); ++row) {
+ if (filter[row]) {
+ data[output_row++] = data[row];
+ }
+ }
+ data.resize(output_row);
+ const size_t null_map_size =
inline_column.get_null_map_column().filter(filter);
+ DCHECK_EQ(output_row, result_size);
+ DCHECK_EQ(null_map_size, result_size);
+ return result_size;
+}
+
+Status ColumnFile::filter_by_selector(const uint16_t* selector, size_t count,
+ IColumn* destination) const {
+ auto& output = assert_cast<ColumnFile&>(*destination);
+ DCHECK(output.empty());
+ output.reserve(count);
+ // Row copies preserve selection order, duplicates and the binary child's
owned bytes.
+ for (size_t i = 0; i < count; ++i) {
+ output.insert_from(*this, selector[i]);
+ }
+ return Status::OK();
+}
+
+MutableColumnPtr ColumnFile::permute(const Permutation& permutation, size_t
limit) const {
+ MutableColumns children;
+ children.reserve(NUM_CHILDREN);
+ for (const auto& column : _columns) {
+ children.push_back(column->permute(permutation, limit));
+ }
+ return create(std::move(children));
+}
+
+ColumnPtr ColumnFile::convert_column_if_overflow() {
+ for (auto& column : _columns) {
+ column = column->convert_column_if_overflow();
+ }
+ return IColumn::convert_column_if_overflow();
+}
+
+size_t ColumnFile::byte_size() const {
+ size_t size = 0;
+ for (const auto& column : _columns) {
+ size += column->byte_size();
+ }
+ return size;
+}
+
+size_t ColumnFile::allocated_bytes() const {
+ size_t size = 0;
+ for (const auto& column : _columns) {
+ size += column->allocated_bytes();
+ }
+ return size;
+}
+
+bool ColumnFile::has_enough_capacity(const IColumn& src) const {
+ const auto& file = assert_cast<const ColumnFile&>(src);
+ for (size_t i = 0; i < NUM_CHILDREN; ++i) {
+ if (!_columns[i]->has_enough_capacity(file.get_column(i))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void ColumnFile::for_each_subcolumn(ColumnCallback callback) {
+ for (auto& column : _columns) {
+ callback(column);
+ }
+}
+
+StringRef ColumnFile::serialize_value_into_arena(size_t n, Arena& arena, const
char*& begin) const {
+ char* pos = arena.alloc_continue(serialize_size_at(n), begin);
+ return {pos, serialize_impl(pos, n)};
+}
+
+const char* ColumnFile::deserialize_and_insert_from_arena(const char* pos) {
+ return pos + deserialize_impl(pos);
+}
+
+size_t ColumnFile::serialize_size_at(size_t row) const {
+ size_t size = 0;
+ for (const auto& column : _columns) {
+ size += column->serialize_size_at(row);
+ }
+ return size;
+}
+
+size_t ColumnFile::serialize_impl(char* pos, size_t row) const {
+ size_t size = 0;
+ for (const auto& column : _columns) {
+ size += column->serialize_impl(pos + size, row);
+ }
+ DCHECK_EQ(size, serialize_size_at(row));
+ return size;
+}
+
+size_t ColumnFile::deserialize_impl(const char* pos) {
+ size_t size = 0;
+ for (auto& column : _columns) {
+ size += column->deserialize_impl(pos + size);
+ }
+ return size;
+}
+
+void ColumnFile::serialize_vec(StringRef* keys, size_t num_rows) const {
+ for (size_t i = 0; i < num_rows; ++i) {
+ // The caller owns these mutable row buffers, as in other column
serializers.
+ keys[i].size += serialize_impl(const_cast<char*>(keys[i].data +
keys[i].size), i);
+ }
+}
+
+void ColumnFile::deserialize_vec(StringRef* keys, size_t num_rows) {
+ for (size_t i = 0; i < num_rows; ++i) {
+ const auto size = deserialize_impl(keys[i].data);
+ keys[i].data += size;
+ keys[i].size -= size;
+ }
+}
+
+size_t ColumnFile::get_max_row_byte_size() const {
+ size_t size = 0;
+ for (const auto& column : _columns) {
+ size += column->get_max_row_byte_size();
+ }
+ return size;
+}
+
+void ColumnFile::update_hash_with_value(size_t n, SipHash& hash) const {
+ hash_file_rows(*this, n, n + 1, nullptr,
+ [&](size_t, const char* data, size_t size) {
hash.update(data, size); });
+}
+
+void ColumnFile::update_hashes_with_value(uint64_t* __restrict hashes,
+ const uint8_t* __restrict null_data)
const {
+ hash_file_rows(*this, 0, size(), null_data, [&](size_t row, const char*
data, size_t size) {
+ hashes[row] = HashUtil::xxHash64WithSeed(data, size, hashes[row]);
+ });
+}
+
+void ColumnFile::update_xxHash_with_value(size_t start, size_t end, uint64_t&
hash,
Review Comment:
[P2] Fence direct distinct aggregates from FILE integrity hashes
The new physical hash is also reachable through this directly callable SQL
aggregate:
```text
Aggregate(output=[MULTI_DISTINCT_COUNT(ARRAY(f))])
Project(CAST(NAMED_STRUCT('uri',CONCAT('urn:',number),
'offset',NULL,'size',NULL,'content_type',NULL,
'checksum',NULL,'inline',NULL) AS FILE) AS f)
Numbers(number=2)
```
`MultiDistinctCount` is registered as a builtin, accepts ANY, forces its
distinct flag to false, and checks only legacy VARIANT. BE's uniq factory
supports TYPE_ARRAY and calls `ColumnArray::update_xxHash_with_value` -> this
method, so FILE's complete serialized bytes become a SQL deduplication key
despite the declared integrity-only hash contract. Recursively reject FILE in
this direct aggregate's own legality check. This is distinct from the existing
ARRAY_AGG(DISTINCT f) report: there is no DISTINCT modifier or
optimizer-created grouping key, and checking `getDistinctArguments()` would
leave this instance admitted.
##########
fe/fe-core/src/main/java/org/apache/doris/qe/runtime/ThriftPlansBuilder.java:
##########
@@ -239,6 +240,10 @@ static Map<String, TAIResource>
collectAiResources(ConnectContext connectContext
return aiResourceMap;
}
+ public static Map<String, TS3Resource> collectS3Resources(ConnectContext
context) {
+ return context == null ? Maps.newLinkedHashMap() :
context.getS3Resources();
Review Comment:
[P2] Preserve job-owned resources across Broker Load workers
A Broker LOAD mapping such as `SET(sz=ELEMENT_AT(TO_FILE('r',uri),'size'))`
targets BIGINT and passes the FILE storage/output restrictions. Its mapping is
translated by `NereidsLoadingTaskPlanner`/`NereidsLoadPlanInfoCollector` on the
pending-task worker, but the resource is registered only in that worker's
ConnectContext. `LoadLoadingTask.executeOnce` later runs on a loading worker:
the default NereidsCoordinator captures that worker's context through
`CoordinatorContext.buildForLoad`, so this helper sends its empty resource map;
the legacy Coordinator constructor instead leaves context null. Neither
transfers the planning snapshot, and TO_FILE cannot resolve `r` in BE. A fresh
planning worker also lacks the job's currentUserIdentity
(`BrokerLoadJob.setComputeGroup` restores only compute selection), so the new
USAGE check can fail earlier on a null identity. Restore the submitting
identity for planning and carry an immutable task-owned snapshot into
execution, with a Broker LOAD
mapping test. This first-run asynchronous Broker Load boundary is separate
from the existing routine-load refresh and external opaque-plan reports.
--
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]