blob: b2fcbbac23667b27f4a5aeba432bafb972506aef [file]
// 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 "exec/rowid_fetcher.h"
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "runtime/descriptor_helper.h"
#include "runtime/descriptors.h"
namespace doris {
// source_column_key() decides which projected slots collapse onto a single scanned
// column. Two slots that name the same physical column must produce the same key, and
// any two slots that do not must produce different ones -- a false merge drops a column
// the result block still expects, which is the crash this keying was introduced to fix.
class RowIdStorageReaderTest : public testing::Test {
public:
static std::string key_of(const SlotDescriptor& slot, uint32_t column_idx) {
return RowIdStorageReader::source_column_key(slot, column_idx);
}
protected:
struct SlotSpec {
std::string col_name = "c";
int32_t col_unique_id = 1;
std::vector<std::string> column_paths = {};
TColumnAccessPaths access_paths = {};
};
static SlotDescriptor make_slot(const SlotSpec& spec) {
TSlotDescriptor tdesc = TSlotDescriptorBuilder()
.type(TYPE_INT)
.nullable(true)
.column_name(spec.col_name)
.column_pos(0)
.build();
tdesc.__set_col_unique_id(spec.col_unique_id);
tdesc.__set_column_paths(spec.column_paths);
if (!spec.access_paths.empty()) {
tdesc.__set_all_access_paths(spec.access_paths);
}
return SlotDescriptor(tdesc);
}
static TColumnAccessPath data_path(const std::vector<std::string>& path) {
TColumnAccessPath access_path;
access_path.type = TAccessPathType::DATA;
TDataAccessPath data;
data.__set_path(path);
access_path.__set_data_access_path(data);
return access_path;
}
static TColumnAccessPath bare_path() {
TColumnAccessPath access_path;
access_path.type = TAccessPathType::DATA;
return access_path;
}
};
TEST_F(RowIdStorageReaderTest, SameSourceColumnSharesKey) {
// The bug case: one physical column projected twice must dedup onto one scan column.
const SlotDescriptor first = make_slot({});
const SlotDescriptor second = make_slot({});
EXPECT_EQ(key_of(first, 3), key_of(second, 3));
}
TEST_F(RowIdStorageReaderTest, ColumnIndexSeparatesKeys) {
const SlotDescriptor slot = make_slot({});
EXPECT_NE(key_of(slot, 3), key_of(slot, 4));
}
TEST_F(RowIdStorageReaderTest, ColumnNameSeparatesKeys) {
EXPECT_NE(key_of(make_slot({.col_name = "a"}), 0), key_of(make_slot({.col_name = "b"}), 0));
}
TEST_F(RowIdStorageReaderTest, UniqueIdSeparatesKeys) {
EXPECT_NE(key_of(make_slot({.col_unique_id = 1}), 0),
key_of(make_slot({.col_unique_id = 2}), 0));
}
TEST_F(RowIdStorageReaderTest, NameAndIndexBoundaryIsNotAmbiguous) {
// Without length prefixes, "a" + idx 12 and "a1" + idx 2 both flatten to "a12".
EXPECT_NE(key_of(make_slot({.col_name = "a"}), 12), key_of(make_slot({.col_name = "a1"}), 2));
}
TEST_F(RowIdStorageReaderTest, PathComponentBoundaryIsNotAmbiguous) {
// The concatenation hazard the length prefix exists for: ["a", "b"] and ["a:b"] are
// different nested columns but share the naive ':'-joined spelling.
EXPECT_NE(key_of(make_slot({.column_paths = {"a", "b"}}), 0),
key_of(make_slot({.column_paths = {"a:b"}}), 0));
}
TEST_F(RowIdStorageReaderTest, EmptyPathIsNotTheSameAsNoPath) {
EXPECT_NE(key_of(make_slot({.column_paths = {}}), 0),
key_of(make_slot({.column_paths = {""}}), 0));
}
TEST_F(RowIdStorageReaderTest, PathOrderMatters) {
EXPECT_NE(key_of(make_slot({.column_paths = {"a", "b"}}), 0),
key_of(make_slot({.column_paths = {"b", "a"}}), 0));
}
TEST_F(RowIdStorageReaderTest, EqualPathsShareKey) {
EXPECT_EQ(key_of(make_slot({.column_paths = {"a", "b"}}), 0),
key_of(make_slot({.column_paths = {"a", "b"}}), 0));
}
TEST_F(RowIdStorageReaderTest, AccessPathSeparatesKeys) {
EXPECT_NE(key_of(make_slot({.access_paths = {data_path({"a"})}}), 0),
key_of(make_slot({.access_paths = {data_path({"b"})}}), 0));
}
TEST_F(RowIdStorageReaderTest, AbsentAccessPathIsNotAnEmptyOne) {
// The presence bit: an unset data_access_path must not collide with one that is set
// but carries no components.
EXPECT_NE(key_of(make_slot({.access_paths = {bare_path()}}), 0),
key_of(make_slot({.access_paths = {data_path({})}}), 0));
}
TEST_F(RowIdStorageReaderTest, AccessPathCountSeparatesKeys) {
EXPECT_NE(key_of(make_slot({.access_paths = {data_path({"a"})}}), 0),
key_of(make_slot({.access_paths = {data_path({"a"}), data_path({"b"})}}), 0));
}
// Runs every submitted task on the submitting thread. The point of these cases is which
// status reaches the caller, not the threading, and inline execution keeps them
// deterministic.
class InlineScanScheduler : public ScannerScheduler {
public:
Status start(int, int, int, int) override { return Status::OK(); }
void stop() override {}
Status submit_scan_task(SimplifiedScanTask scan_task) override {
scan_task.scan_func();
return Status::OK();
}
Status submit_scan_task(SimplifiedScanTask scan_task, const std::string&) override {
scan_task.scan_func();
return Status::OK();
}
void reset_thread_num(int, int, int) override {}
int get_queue_size() override { return 0; }
int get_active_threads() override { return 0; }
std::vector<int> thread_debug_info() override { return {}; }
Status schedule_scan_task(std::shared_ptr<ScannerContext>, std::shared_ptr<ScanTask>,
std::unique_lock<std::mutex>&) override {
return Status::OK();
}
};
// submit_external_scan_tasks() signals completion from a Defer, so a worker that leaves
// without publishing its status would still wake the waiter and the caller would report
// success over a partially filled result block.
class SubmitExternalScanTasksTest : public RowIdStorageReaderTest {
protected:
static constexpr size_t kTaskCount = 3;
static Status run_tasks(const std::function<Status(size_t)>& run_task) {
InlineScanScheduler scheduler;
std::counting_semaphore<> semaphore {kTaskCount};
return RowIdStorageReader::submit_external_scan_tasks(
&scheduler, semaphore, kTaskCount,
[](size_t idx) { return fmt::format("task-{}", idx); }, run_task);
}
};
TEST_F(SubmitExternalScanTasksTest, AllTasksSucceedingReturnsOk) {
size_t ran = 0;
EXPECT_TRUE(run_tasks([&](size_t) -> Status {
++ran;
return Status::OK();
}).ok());
EXPECT_EQ(ran, kTaskCount);
}
TEST_F(SubmitExternalScanTasksTest, ReturnedErrorReachesTheCaller) {
const Status result = run_tasks([](size_t idx) -> Status {
return idx == kTaskCount - 1 ? Status::InternalError("scanner returned an error")
: Status::OK();
});
EXPECT_FALSE(result.ok());
EXPECT_NE(result.to_string().find("scanner returned an error"), std::string::npos);
}
TEST_F(SubmitExternalScanTasksTest, ThrownExceptionReachesTheCaller) {
// The last task is the interesting one: it is the completion that releases the
// waiter, so a status lost here is a status the caller never sees.
const Status result = run_tasks([](size_t idx) -> Status {
if (idx == kTaskCount - 1) {
throw Exception(ErrorCode::INTERNAL_ERROR, "scanner threw");
}
return Status::OK();
});
EXPECT_FALSE(result.ok());
EXPECT_NE(result.to_string().find("scanner threw"), std::string::npos);
}
} // namespace doris