blob: 0d463641cba4558c641f116ed471d8fbecd81c55 [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 "format_v2/table/adbc_reader.h"
#include <arrow-adbc/adbc.h>
#include <arrow/api.h>
#include <gtest/gtest.h>
#include <unistd.h>
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <map>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "common/object_pool.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_number.h"
#include "core/data_type/data_type_string.h"
#include "format_v2/file_reader.h"
#include "gen_cpp/PlanNodes_types.h"
#include "io/file_factory.h"
#include "io/io_common.h"
#include "runtime/runtime_profile.h"
#include "runtime/runtime_state.h"
#include "testutil/adbc_sqlite_driver.h"
#include "testutil/desc_tbl_builder.h"
#include "util/adbc_driver_registry.h"
namespace doris::format::adbc {
namespace {
// Skipping is only correct when thirdparty predates arrow-adbc; it must say so out loud rather
// than surface as an unrelated-looking dlopen failure.
#define SKIP_WITHOUT_SQLITE_DRIVER() \
do { \
if (!adbc_sqlite_driver_available()) { \
GTEST_SKIP() << "ADBC SQLite driver not found at " << adbc_sqlite_driver_path() \
<< "; run 'cd thirdparty && ./build-thirdparty.sh arrow_adbc' to build " \
"it. End-to-end ADBC coverage is NOT being exercised."; \
} \
} while (0)
class BatchAdbcStream final : public AdbcStream {
public:
BatchAdbcStream(std::vector<std::shared_ptr<arrow::RecordBatch>> batches,
std::shared_ptr<int> close_count)
: _batches(std::move(batches)), _close_count(std::move(close_count)) {}
Status next(std::shared_ptr<arrow::RecordBatch>* batch) override {
DORIS_CHECK(batch != nullptr);
if (_next_batch >= _batches.size()) {
*batch = nullptr;
return Status::OK();
}
*batch = _batches[_next_batch++];
return Status::OK();
}
Status close() override {
++(*_close_count);
return Status::OK();
}
private:
std::vector<std::shared_ptr<arrow::RecordBatch>> _batches;
std::shared_ptr<int> _close_count;
size_t _next_batch = 0;
};
TFileRangeDesc adbc_range(std::map<std::string, std::string> params) {
TTableFormatFileDesc table_desc;
table_desc.__set_table_format_type("adbc");
table_desc.__set_adbc_params(std::move(params));
TFileRangeDesc range;
range.__set_format_type(TFileFormatType::FORMAT_ARROW);
range.__set_path("/dummyPath");
range.__set_table_format_params(std::move(table_desc));
return range;
}
// A range that passes validation but is never actually connected to; group A injects a fake stream.
TFileRangeDesc fake_adbc_range() {
return adbc_range({{"driver_path", "/dummy/libadbc_driver_fake.so"},
{"uri", "file:/dummy.db"},
{"query_sql", "SELECT 1"}});
}
std::vector<SlotDescriptor*> string_slot(ObjectPool* pool, DescriptorTbl** desc_tbl) {
DescriptorTblBuilder builder(pool);
builder.declare_tuple() << std::make_tuple(std::make_shared<DataTypeString>(),
std::string("c_str"));
*desc_tbl = builder.build();
return (*desc_tbl)->get_tuple_descriptor(0)->slots();
}
std::vector<SlotDescriptor*> sqlite_slots(ObjectPool* pool, DescriptorTbl** desc_tbl) {
DescriptorTblBuilder builder(pool);
// SQLite stores integers as 64-bit and reals as doubles; the ADBC driver reports them as such.
builder.declare_tuple()
<< std::make_tuple(std::make_shared<DataTypeInt64>(), std::string("id"))
<< std::make_tuple(std::make_shared<DataTypeInt64>(), std::string("v_int"))
<< std::make_tuple(std::make_shared<DataTypeFloat64>(), std::string("v_dbl"))
<< std::make_tuple(std::make_shared<DataTypeString>(), std::string("v_txt"));
*desc_tbl = builder.build();
return (*desc_tbl)->get_tuple_descriptor(0)->slots();
}
// large_utf8 is what Go-based drivers emit and what the string serde refuses; feeding it through
// proves the reader normalizes before materializing.
std::shared_ptr<arrow::RecordBatch> make_named_large_string_batch(const std::string& column_name) {
arrow::LargeStringBuilder b;
EXPECT_TRUE(b.Append("doris").ok());
EXPECT_TRUE(b.AppendNull().ok());
std::shared_ptr<arrow::Array> arr;
EXPECT_TRUE(b.Finish(&arr).ok());
auto schema = arrow::schema({arrow::field(column_name, arrow::large_utf8())});
return arrow::RecordBatch::Make(schema, 2, {arr});
}
std::shared_ptr<arrow::RecordBatch> make_large_string_batch() {
return make_named_large_string_batch("c_str");
}
std::unique_ptr<AdbcFileReader> create_reader(RuntimeProfile* profile, const TFileRangeDesc& range,
const std::vector<SlotDescriptor*>& slots,
AdbcStreamFactory factory) {
auto system_properties = std::make_shared<io::FileSystemProperties>();
auto file_description = std::make_unique<io::FileDescription>();
file_description->path = "/dummyPath";
return std::make_unique<AdbcFileReader>(system_properties, file_description, nullptr, profile,
range, slots, std::move(factory));
}
Block make_request_block(const std::vector<ColumnDefinition>& schema,
const std::vector<int32_t>& local_ids) {
Block block;
for (const auto local_id : local_ids) {
const auto it = std::find_if(schema.begin(), schema.end(), [&](const auto& column) {
return column.local_id == local_id;
});
DORIS_CHECK(it != schema.end());
block.insert({it->type->create_column(), it->type, it->name});
}
return block;
}
std::string nullable_string_at(const IColumn& column, size_t row) {
const auto& nullable = assert_cast<const ColumnNullable&>(column);
const auto& nested = assert_cast<const ColumnString&>(nullable.get_nested_column());
return nested.get_data_at(row).to_string();
}
int64_t nullable_int64_at(const IColumn& column, size_t row) {
const auto& nullable = assert_cast<const ColumnNullable&>(column);
return assert_cast<const ColumnInt64&>(nullable.get_nested_column()).get_data()[row];
}
double nullable_double_at(const IColumn& column, size_t row) {
const auto& nullable = assert_cast<const ColumnNullable&>(column);
return assert_cast<const ColumnFloat64&>(nullable.get_nested_column()).get_data()[row];
}
bool is_null_at(const IColumn& column, size_t row) {
return assert_cast<const ColumnNullable&>(column).is_null_at(row);
}
// Runs DDL/DML through ADBC so the fixture needs neither the sqlite3 CLI nor a sqlite dev package.
::testing::AssertionResult run_sqlite_ddl(const std::string& uri, const std::string& sql) {
const AdbcDriver* driver = nullptr;
Status st = AdbcDriverRegistry::instance().get_or_load(adbc_sqlite_driver_path(), "", &driver);
if (!st.ok()) {
return ::testing::AssertionFailure() << "load driver: " << st.to_string();
}
AdbcError error = ADBC_ERROR_INIT;
AdbcDatabase database {};
AdbcConnection connection {};
AdbcStatement statement {};
auto fail = [&](const char* what, AdbcStatusCode code) {
std::string message = error.message != nullptr ? error.message : "";
if (error.release != nullptr) {
error.release(&error);
}
return ::testing::AssertionFailure() << what << " failed (" << code << "): " << message;
};
if (auto code = driver->DatabaseNew(&database, &error); code != ADBC_STATUS_OK) {
return fail("DatabaseNew", code);
}
if (auto code = driver->DatabaseSetOption(&database, ADBC_OPTION_URI, uri.c_str(), &error);
code != ADBC_STATUS_OK) {
return fail("DatabaseSetOption", code);
}
if (auto code = driver->DatabaseInit(&database, &error); code != ADBC_STATUS_OK) {
return fail("DatabaseInit", code);
}
if (auto code = driver->ConnectionNew(&connection, &error); code != ADBC_STATUS_OK) {
return fail("ConnectionNew", code);
}
if (auto code = driver->ConnectionInit(&connection, &database, &error);
code != ADBC_STATUS_OK) {
return fail("ConnectionInit", code);
}
if (auto code = driver->StatementNew(&connection, &statement, &error); code != ADBC_STATUS_OK) {
return fail("StatementNew", code);
}
if (auto code = driver->StatementSetSqlQuery(&statement, sql.c_str(), &error);
code != ADBC_STATUS_OK) {
return fail("StatementSetSqlQuery", code);
}
int64_t rows_affected = -1;
if (auto code = driver->StatementExecuteQuery(&statement, nullptr, &rows_affected, &error);
code != ADBC_STATUS_OK) {
return fail("StatementExecuteQuery", code);
}
if (error.release != nullptr) {
error.release(&error);
}
static_cast<void>(driver->StatementRelease(&statement, &error));
static_cast<void>(driver->ConnectionRelease(&connection, &error));
static_cast<void>(driver->DatabaseRelease(&database, &error));
if (error.release != nullptr) {
error.release(&error);
}
return ::testing::AssertionSuccess();
}
} // namespace
// Group A: the materialization path, driven from a RecordBatch so no database is involved.
// The key case: without normalize_arrow_array the serde hits its unsupported-type branch and this
// fails. Everything else in the reader can be right and the data still would not land.
TEST(AdbcReaderTest, NormalizesLargeStringBeforeMaterializing) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = string_slot(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_normalize_test");
auto close_count = std::make_shared<int>(0);
auto reader =
create_reader(&profile, fake_adbc_range(), slots,
[close_count](const TFileRangeDesc&, std::unique_ptr<AdbcStream>* out) {
*out = std::make_unique<BatchAdbcStream>(
std::vector<std::shared_ptr<arrow::RecordBatch>> {
make_large_string_batch()},
close_count);
return Status::OK();
});
ASSERT_TRUE(reader->init(&state).ok());
std::vector<ColumnDefinition> schema;
ASSERT_TRUE(reader->get_schema(&schema).ok());
ASSERT_EQ(schema.size(), 1);
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
ASSERT_TRUE(reader->open(request).ok());
auto block = make_request_block(schema, {0});
size_t rows = 0;
bool eof = false;
const auto status = reader->get_block(&block, &rows, &eof);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_EQ(rows, 2);
EXPECT_FALSE(eof);
EXPECT_EQ(nullable_string_at(*block.get_by_position(0).column, 0), "doris");
EXPECT_TRUE(is_null_at(*block.get_by_position(0).column, 1));
ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
EXPECT_EQ(rows, 0);
EXPECT_TRUE(eof);
ASSERT_TRUE(reader->close().ok());
EXPECT_EQ(*close_count, 1);
}
// An all-null int64 array standing in for a TEXT column, which is what a source that infers Arrow
// types from the values it returns sends when a filter leaves only nulls.
std::shared_ptr<arrow::RecordBatch> make_all_null_int64_batch(const std::string& column_name) {
arrow::Int64Builder b;
EXPECT_TRUE(b.AppendNull().ok());
EXPECT_TRUE(b.AppendNull().ok());
std::shared_ptr<arrow::Array> arr;
EXPECT_TRUE(b.Finish(&arr).ok());
auto schema = arrow::schema({arrow::field(column_name, arrow::int64())});
return arrow::RecordBatch::Make(schema, 2, {arr});
}
// A column whose values are ALL null arrives with a type that says nothing about the column: a
// source inferring Arrow types from values has nothing to infer from. Measured on the SQLite driver,
// the same TEXT column is utf8 for `SELECT id, name FROM t1` and int64 for the same query plus
// `WHERE name IS NULL`. Without the all-null branch this reaches the string serde and fails with
// "Unsupported arrow type for string column: 9", and FE cannot prevent it -- it cannot know which
// rows a filter will leave.
TEST(AdbcReaderTest, MaterializesAnAllNullColumnWhateverTypeTheSourceClaims) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = string_slot(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_all_null_test");
auto close_count = std::make_shared<int>(0);
auto reader =
create_reader(&profile, fake_adbc_range(), slots,
[close_count](const TFileRangeDesc&, std::unique_ptr<AdbcStream>* out) {
*out = std::make_unique<BatchAdbcStream>(
std::vector<std::shared_ptr<arrow::RecordBatch>> {
make_all_null_int64_batch("c_str")},
close_count);
return Status::OK();
});
ASSERT_TRUE(reader->init(&state).ok());
std::vector<ColumnDefinition> schema;
ASSERT_TRUE(reader->get_schema(&schema).ok());
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
ASSERT_TRUE(reader->open(request).ok());
auto block = make_request_block(schema, {0});
size_t rows = 0;
bool eof = false;
const auto status = reader->get_block(&block, &rows, &eof);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_EQ(rows, 2);
EXPECT_TRUE(is_null_at(*block.get_by_position(0).column, 0));
EXPECT_TRUE(is_null_at(*block.get_by_position(0).column, 1));
}
// The other side of that branch: a column carrying real values keeps its real type, so a genuine
// FE/source schema disagreement still fails rather than being papered over as nulls.
TEST(AdbcReaderTest, StillRejectsATypeMismatchOnAColumnThatHasValues) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = string_slot(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_type_mismatch_test");
auto close_count = std::make_shared<int>(0);
arrow::Int64Builder values;
EXPECT_TRUE(values.Append(7).ok());
EXPECT_TRUE(values.AppendNull().ok());
std::shared_ptr<arrow::Array> arr;
EXPECT_TRUE(values.Finish(&arr).ok());
auto batch = arrow::RecordBatch::Make(arrow::schema({arrow::field("c_str", arrow::int64())}), 2,
{arr});
auto reader = create_reader(
&profile, fake_adbc_range(), slots,
[close_count, batch](const TFileRangeDesc&, std::unique_ptr<AdbcStream>* out) {
*out = std::make_unique<BatchAdbcStream>(
std::vector<std::shared_ptr<arrow::RecordBatch>> {batch}, close_count);
return Status::OK();
});
ASSERT_TRUE(reader->init(&state).ok());
std::vector<ColumnDefinition> schema;
ASSERT_TRUE(reader->get_schema(&schema).ok());
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
ASSERT_TRUE(reader->open(request).ok());
auto block = make_request_block(schema, {0});
size_t rows = 0;
bool eof = false;
EXPECT_FALSE(reader->get_block(&block, &rows, &eof).ok());
}
// A pushed-down COUNT(*) projects nothing, so every column the source returns is unrequested by
// definition. Without the empty-projection branch the unknown-column check below rejects the first
// one and a query asking for nothing but a number fails.
TEST(AdbcReaderTest, CountsRowsWhenTheScanProjectsNoColumns) {
RuntimeState state;
RuntimeProfile profile("adbc_reader_count_only_test");
auto close_count = std::make_shared<int>(0);
const std::vector<SlotDescriptor*> no_slots;
auto reader =
create_reader(&profile, fake_adbc_range(), no_slots,
[close_count](const TFileRangeDesc&, std::unique_ptr<AdbcStream>* out) {
*out = std::make_unique<BatchAdbcStream>(
std::vector<std::shared_ptr<arrow::RecordBatch>> {
make_named_large_string_batch("1")},
close_count);
return Status::OK();
});
ASSERT_TRUE(reader->init(&state).ok());
std::vector<ColumnDefinition> schema;
ASSERT_TRUE(reader->get_schema(&schema).ok());
ASSERT_TRUE(schema.empty());
auto request = std::make_shared<FileScanRequest>();
ASSERT_TRUE(reader->open(request).ok());
Block block;
size_t rows = 0;
bool eof = false;
const auto status = reader->get_block(&block, &rows, &eof);
ASSERT_TRUE(status.ok()) << status.to_string();
EXPECT_EQ(rows, 2);
EXPECT_FALSE(eof);
EXPECT_EQ(block.columns(), 0);
ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
EXPECT_EQ(rows, 0);
EXPECT_TRUE(eof);
ASSERT_TRUE(reader->close().ok());
}
// The other half of the branch above: an unrequested column arriving ALONGSIDE requested ones still
// fails. That state means FE and this reader disagree about the projection, and this check is the
// only signal the disagreement exists -- relaxing it to tolerate the count case would remove it.
TEST(AdbcReaderTest, RejectsAColumnTheScanDidNotRequest) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = string_slot(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_unknown_column_test");
auto close_count = std::make_shared<int>(0);
auto reader =
create_reader(&profile, fake_adbc_range(), slots,
[close_count](const TFileRangeDesc&, std::unique_ptr<AdbcStream>* out) {
*out = std::make_unique<BatchAdbcStream>(
std::vector<std::shared_ptr<arrow::RecordBatch>> {
make_named_large_string_batch("not_requested")},
close_count);
return Status::OK();
});
ASSERT_TRUE(reader->init(&state).ok());
std::vector<ColumnDefinition> schema;
ASSERT_TRUE(reader->get_schema(&schema).ok());
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
ASSERT_TRUE(reader->open(request).ok());
auto block = make_request_block(schema, {0});
size_t rows = 0;
bool eof = false;
const auto status = reader->get_block(&block, &rows, &eof);
EXPECT_FALSE(status.ok());
EXPECT_NE(status.to_string().find("not_requested"), std::string::npos) << status.to_string();
}
// A range missing a required parameter must be rejected up front, not at connect time where the
// error would come back as an opaque driver message.
TEST(AdbcReaderTest, RejectsIncompleteAdbcParams) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = string_slot(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_bad_range_test");
for (const auto* missing : {"driver_path", "uri", "query_sql"}) {
auto range = fake_adbc_range();
range.table_format_params.adbc_params.erase(missing);
auto reader = create_reader(&profile, range, slots,
[](const TFileRangeDesc&, std::unique_ptr<AdbcStream>* out) {
*out = nullptr;
return Status::OK();
});
EXPECT_FALSE(reader->init(&state).ok()) << "missing " << missing << " was accepted";
}
auto range = fake_adbc_range();
range.table_format_params.__isset.adbc_params = false;
auto reader = create_reader(&profile, range, slots, {});
EXPECT_FALSE(reader->init(&state).ok());
}
// A range says either "run this statement" or "read this partition of a statement already run".
// Accepting one that says both would let this reader execute a query the source has already
// executed, depending only on which branch the code happened to take first.
TEST(AdbcReaderTest, RejectsARangeThatSaysBothOrNeitherKindOfWork) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = string_slot(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_exclusive_work_test");
auto both = fake_adbc_range();
both.table_format_params.adbc_params["partition_descriptor"] = "Zm9vYmFy";
auto both_reader = create_reader(&profile, both, slots, {});
const auto both_status = both_reader->init(&state);
ASSERT_FALSE(both_status.ok());
EXPECT_NE(both_status.to_string().find("both"), std::string::npos) << both_status.to_string();
auto neither = fake_adbc_range();
neither.table_format_params.adbc_params.erase("query_sql");
auto neither_reader = create_reader(&profile, neither, slots, {});
const auto neither_status = neither_reader->init(&state);
ASSERT_FALSE(neither_status.ok());
EXPECT_NE(neither_status.to_string().find("neither"), std::string::npos)
<< neither_status.to_string();
}
// Group B: the real ADBC C API call sequence, against the SQLite driver thirdparty builds.
class AdbcSqliteReaderTest : public ::testing::Test {
protected:
void SetUp() override {
SKIP_WITHOUT_SQLITE_DRIVER();
_db_path = std::filesystem::temp_directory_path() /
("doris_adbc_reader_test_" + std::to_string(::getpid()) + ".db");
std::filesystem::remove(_db_path);
const std::string uri = "file:" + _db_path.string();
ASSERT_TRUE(run_sqlite_ddl(
uri, "CREATE TABLE t (id INTEGER, v_int INTEGER, v_dbl REAL, v_txt TEXT)"));
ASSERT_TRUE(run_sqlite_ddl(uri,
"INSERT INTO t VALUES (1, 10, 1.5, 'alpha'), "
"(2, NULL, NULL, NULL), (3, 30, 3.5, 'gamma')"));
}
void TearDown() override {
if (!_db_path.empty()) {
std::filesystem::remove(_db_path);
}
}
std::string uri() const { return "file:" + _db_path.string(); }
std::filesystem::path _db_path;
};
// Group A cannot prove the ADBC call sequence is right, nor that the driver manager is usable at
// run time. SQLite makes that testable without a server or a container.
TEST_F(AdbcSqliteReaderTest, ReadsFromRealSqliteDriverEndToEnd) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = sqlite_slots(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_sqlite_e2e_test");
auto range = adbc_range({
{"driver_path", adbc_sqlite_driver_path()},
{"uri", uri()},
{"query_sql", "SELECT id, v_int, v_dbl, v_txt FROM t ORDER BY id"},
});
// No injected factory: this goes through the real ADBC stream.
auto reader = create_reader(&profile, range, slots, {});
ASSERT_TRUE(reader->init(&state).ok());
std::vector<ColumnDefinition> schema;
ASSERT_TRUE(reader->get_schema(&schema).ok());
ASSERT_EQ(schema.size(), 4);
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
for (int32_t id = 0; id < 4; ++id) {
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(id)).ok());
}
const auto open_status = reader->open(request);
ASSERT_TRUE(open_status.ok()) << open_status.to_string();
auto block = make_request_block(schema, {0, 1, 2, 3});
size_t rows = 0;
bool eof = false;
const auto status = reader->get_block(&block, &rows, &eof);
ASSERT_TRUE(status.ok()) << status.to_string();
ASSERT_EQ(rows, 3);
const auto& id_col = *block.get_by_position(0).column;
const auto& int_col = *block.get_by_position(1).column;
const auto& dbl_col = *block.get_by_position(2).column;
const auto& txt_col = *block.get_by_position(3).column;
EXPECT_EQ(nullable_int64_at(id_col, 0), 1);
EXPECT_EQ(nullable_int64_at(id_col, 2), 3);
EXPECT_EQ(nullable_int64_at(int_col, 0), 10);
EXPECT_TRUE(is_null_at(int_col, 1));
EXPECT_EQ(nullable_int64_at(int_col, 2), 30);
EXPECT_DOUBLE_EQ(nullable_double_at(dbl_col, 0), 1.5);
EXPECT_TRUE(is_null_at(dbl_col, 1));
EXPECT_DOUBLE_EQ(nullable_double_at(dbl_col, 2), 3.5);
EXPECT_EQ(nullable_string_at(txt_col, 0), "alpha");
EXPECT_TRUE(is_null_at(txt_col, 1));
EXPECT_EQ(nullable_string_at(txt_col, 2), "gamma");
ASSERT_TRUE(reader->close().ok());
}
// A driver path that is not there is the most likely user error, so it must not look like an
// internal failure.
TEST_F(AdbcSqliteReaderTest, MissingDriverFailsWithThePathInTheMessage) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = sqlite_slots(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_missing_driver_test");
auto range = adbc_range({
{"driver_path", "/nonexistent/libadbc_driver_nope.so"},
{"uri", uri()},
{"query_sql", "SELECT 1"},
});
auto reader = create_reader(&profile, range, slots, {});
ASSERT_TRUE(reader->init(&state).ok());
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
const auto status = reader->open(request);
ASSERT_FALSE(status.ok());
EXPECT_NE(status.to_string().find("/nonexistent/libadbc_driver_nope.so"), std::string::npos);
}
// A bad query has to surface the driver's own message, otherwise SQL problems are undiagnosable.
TEST_F(AdbcSqliteReaderTest, InvalidQuerySurfacesTheDriverMessage) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = sqlite_slots(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_bad_query_test");
auto range = adbc_range({
{"driver_path", adbc_sqlite_driver_path()},
{"uri", uri()},
{"query_sql", "SELECT * FROM no_such_table"},
});
auto reader = create_reader(&profile, range, slots, {});
ASSERT_TRUE(reader->init(&state).ok());
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
const auto status = reader->open(request);
ASSERT_FALSE(status.ok());
EXPECT_NE(status.to_string().find("no_such_table"), std::string::npos)
<< "driver message was lost: " << status.to_string();
}
// A partition descriptor is FE's base64 of driver-private bytes. Garbage there has to be named as
// such: handed to the driver undecoded it would come back as an opaque parse failure from inside a
// protobuf, with nothing pointing at the parameter that was wrong.
TEST_F(AdbcSqliteReaderTest, RejectsAPartitionDescriptorThatIsNotBase64) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = sqlite_slots(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_bad_partition_test");
auto range = adbc_range({
{"driver_path", adbc_sqlite_driver_path()},
{"uri", uri()},
{"partition_descriptor", "not base64 at all!!"},
});
auto reader = create_reader(&profile, range, slots, {});
ASSERT_TRUE(reader->init(&state).ok());
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
const auto status = reader->open(request);
ASSERT_FALSE(status.ok());
EXPECT_NE(status.to_string().find("partition_descriptor"), std::string::npos)
<< status.to_string();
}
// The reader must actually take the partition branch, not fall through to running a statement it
// was not given. SQLite has no partitioned execution, so the proof that the call was made is the
// driver's own refusal of it -- naming ConnectionReadPartition, the entry point only this branch
// reaches. Reading a partition successfully needs a source that produces one, which is the Flight
// SQL regression suite, not a unit test.
TEST_F(AdbcSqliteReaderTest, ReadsAPartitionThroughTheDriverInsteadOfRunningAStatement) {
ObjectPool pool;
DescriptorTbl* desc_tbl = nullptr;
const auto slots = sqlite_slots(&pool, &desc_tbl);
RuntimeState state;
RuntimeProfile profile("adbc_reader_partition_branch_test");
auto range = adbc_range({
{"driver_path", adbc_sqlite_driver_path()},
{"uri", uri()},
// Valid base64; the bytes are meaningless to the driver, which never gets to look at
// them because it has no partition support at all.
{"partition_descriptor", "Zm9vYmFy"},
});
auto reader = create_reader(&profile, range, slots, {});
ASSERT_TRUE(reader->init(&state).ok());
auto request = std::make_shared<FileScanRequest>();
FileScanRequestBuilder builder(request.get());
ASSERT_TRUE(builder.add_non_predicate_column(LocalColumnId(0)).ok());
const auto status = reader->open(request);
ASSERT_FALSE(status.ok());
EXPECT_NE(status.to_string().find("ConnectionReadPartition"), std::string::npos)
<< "the partition branch was not taken: " << status.to_string();
}
// ---- the release contract Arrow aborts the process over ----
namespace {
// A stream shaped like the one the Flight SQL driver hands out: its release callback runs, but
// leaves `release` set. The Arrow C data interface forbids that, and Arrow C++ does not merely
// complain -- ArrowArrayStreamRelease calls abort(), taking the whole BE with it.
struct MisbehavingDriverStream {
int release_calls = 0;
int get_next_calls = 0;
};
MisbehavingDriverStream& state_of(ArrowArrayStream* self) {
return *static_cast<MisbehavingDriverStream*>(self->private_data);
}
int misbehaving_get_schema(ArrowArrayStream* /*self*/, ArrowSchema* /*out*/) {
return 0;
}
int misbehaving_get_next(ArrowArrayStream* self, ArrowArray* /*out*/) {
state_of(self).get_next_calls++;
return 0;
}
const char* misbehaving_get_last_error(ArrowArrayStream* /*self*/) {
return "driver said so";
}
void misbehaving_release(ArrowArrayStream* self) {
state_of(self).release_calls++;
// Deliberately does NOT clear self->release. This is the bug being defended against.
}
ArrowArrayStream misbehaving_stream(MisbehavingDriverStream* state) {
ArrowArrayStream stream {};
stream.get_schema = misbehaving_get_schema;
stream.get_next = misbehaving_get_next;
stream.get_last_error = misbehaving_get_last_error;
stream.release = misbehaving_release;
stream.private_data = state;
return stream;
}
} // namespace
TEST(AdbcStreamReleaseContractTest, ClearsReleaseEvenWhenTheDriverDoesNot) {
MisbehavingDriverStream state;
ArrowArrayStream stream = misbehaving_stream(&state);
enforce_stream_release_contract(&stream);
ASSERT_NE(stream.release, nullptr);
stream.release(&stream);
// The invariant Arrow asserts on, and the one a scan against Flight SQL used to break.
EXPECT_EQ(stream.release, nullptr);
// The driver still gets released, exactly once: the wrapper must not leak the real stream.
EXPECT_EQ(state.release_calls, 1);
}
TEST(AdbcStreamReleaseContractTest, StillDelegatesEveryCallbackToTheDriver) {
// A wrapper that swallowed calls would turn a crash into silently empty results, which is
// worse: the scan would report success on rows it never read.
MisbehavingDriverStream state;
ArrowArrayStream stream = misbehaving_stream(&state);
enforce_stream_release_contract(&stream);
ArrowArray array {};
EXPECT_EQ(stream.get_next(&stream, &array), 0);
EXPECT_EQ(state.get_next_calls, 1);
EXPECT_STREQ(stream.get_last_error(&stream), "driver said so");
stream.release(&stream);
}
TEST(AdbcStreamReleaseContractTest, LeavesAnAlreadyReleasedStreamAlone) {
// Wrapping one would hand Arrow callbacks that dereference a delegate with nothing behind it.
ArrowArrayStream stream {};
enforce_stream_release_contract(&stream);
EXPECT_EQ(stream.release, nullptr);
EXPECT_EQ(stream.private_data, nullptr);
}
} // namespace doris::format::adbc