Coverage Report

Created: 2026-10-12 17:34

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/table/adbc_reader.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "format_v2/table/adbc_reader.h"
19
20
#include <arrow-adbc/adbc.h>
21
#include <arrow-adbc/adbc_driver_manager.h>
22
#include <arrow/array/array_base.h>
23
#include <arrow/c/bridge.h>
24
#include <arrow/record_batch.h>
25
26
#include <cstdint>
27
#include <map>
28
#include <memory>
29
#include <string>
30
#include <string_view>
31
#include <utility>
32
#include <vector>
33
34
#include "common/cast_set.h"
35
#include "common/check.h"
36
#include "core/assert_cast.h"
37
#include "core/block/block.h"
38
#include "core/data_type/data_type.h"
39
#include "core/data_type/data_type_array.h"
40
#include "core/data_type/data_type_map.h"
41
#include "core/data_type/data_type_nullable.h"
42
#include "core/data_type/data_type_struct.h"
43
#include "core/data_type/primitive_type.h"
44
#include "core/data_type_serde/data_type_serde.h"
45
#include "core/data_type_serde/data_type_variant_v2_serde.h"
46
#include "format/arrow/arrow_array_normalizer.h"
47
#include "format/parquet/arrow_memory_pool.h"
48
#include "format_v2/column_mapper.h"
49
#include "format_v2/materialized_reader_util.h"
50
#include "runtime/descriptors.h"
51
#include "runtime/exec_env.h"
52
#include "runtime/file_scan_profile.h"
53
#include "runtime/runtime_state.h"
54
#include "util/adbc_driver_registry.h"
55
#include "util/string_util.h"
56
#include "util/timezone_utils.h"
57
#include "util/url_coding.h"
58
59
namespace doris::format::adbc {
60
namespace {
61
62
// Keys of TTableFormatFileDesc.adbc_params. Kept next to the code that reads them so the FE-side
63
// producer and this consumer stay diffable.
64
constexpr const char* kParamDriverPath = "driver_path";
65
constexpr const char* kParamDriverEntrypoint = "driver_entrypoint";
66
constexpr const char* kParamUri = "uri";
67
constexpr const char* kParamUsername = "username";
68
constexpr const char* kParamPassword = "password";
69
constexpr const char* kParamQuerySql = "query_sql";
70
// Base64 of one opaque partition descriptor the driver produced on FE. Mutually exclusive with
71
// kParamQuerySql: a range either runs a statement here or reads one partition of a statement the
72
// source has already run.
73
constexpr const char* kParamPartitionDescriptor = "partition_descriptor";
74
// Anything under this prefix is an ADBC option name in full (the prefix is part of the option name,
75
// e.g. "adbc.connection.autocommit") and is handed to the driver untouched.
76
constexpr std::string_view kAdbcOptionPrefix = "adbc.";
77
78
const std::string* find_param(const std::map<std::string, std::string>& params,
79
141
                              const std::string& key) {
80
141
    const auto it = params.find(key);
81
141
    return it == params.end() ? nullptr : &it->second;
82
141
}
83
84
30
Status validate_adbc_range(const TFileRangeDesc& range) {
85
30
    if (!range.__isset.table_format_params ||
86
30
        range.table_format_params.table_format_type != "adbc") {
87
0
        return Status::InvalidArgument("ADBC reader requires the adbc table format");
88
0
    }
89
30
    if (!range.table_format_params.__isset.adbc_params) {
90
1
        return Status::InvalidArgument("ADBC reader requires adbc_params");
91
1
    }
92
29
    const auto& params = range.table_format_params.adbc_params;
93
57
    for (const auto* key : {kParamDriverPath, kParamUri}) {
94
57
        const auto* value = find_param(params, key);
95
57
        if (value == nullptr || value->empty()) {
96
2
            return Status::InvalidArgument("ADBC reader requires a non-empty '{}' parameter", key);
97
2
        }
98
57
    }
99
27
    const auto* query_sql = find_param(params, kParamQuerySql);
100
27
    const auto* partition = find_param(params, kParamPartitionDescriptor);
101
27
    const bool has_query = query_sql != nullptr && !query_sql->empty();
102
27
    const bool has_partition = partition != nullptr && !partition->empty();
103
    // Not a defensive nicety: reading a partition means the source has ALREADY run the statement, so
104
    // a range carrying both would let this reader run it a second time depending on which branch it
105
    // happened to take. FE refuses to build such a range; this refuses to act on one.
106
27
    if (has_query == has_partition) {
107
3
        return Status::InvalidArgument(
108
3
                "ADBC reader requires exactly one of '{}' and '{}', but the range carries {}",
109
3
                kParamQuerySql, kParamPartitionDescriptor, has_query ? "both" : "neither");
110
3
    }
111
24
    return Status::OK();
112
27
}
113
114
Status arrow_type_mismatch(const std::string& path, const arrow::DataType& arrow_type,
115
3
                           const DataTypePtr& doris_type) {
116
3
    return Status::InvalidArgument(
117
3
            "ADBC Arrow type mismatch at '{}': runtime type '{}' does not match cached Doris "
118
3
            "type '{}'",
119
3
            path, arrow_type.ToString(), doris_type->get_name());
120
3
}
121
122
0
int timestamp_scale(arrow::TimeUnit::type unit) {
123
0
    switch (unit) {
124
0
    case arrow::TimeUnit::SECOND:
125
0
        return 0;
126
0
    case arrow::TimeUnit::MILLI:
127
0
        return 3;
128
0
    case arrow::TimeUnit::MICRO:
129
0
    case arrow::TimeUnit::NANO:
130
0
        return 6;
131
0
    }
132
0
    return -1;
133
0
}
134
135
Status validate_arrow_type(const arrow::DataType& arrow_type, const DataTypePtr& doris_type,
136
19
                           const std::string& path) {
137
19
    DORIS_CHECK(doris_type != nullptr);
138
19
    const auto nested_doris_type = remove_nullable(doris_type);
139
19
    const auto primitive_type = nested_doris_type->get_primitive_type();
140
19
    const auto mismatch = [&]() { return arrow_type_mismatch(path, arrow_type, doris_type); };
141
142
    // The FE maps the Variant extension to the V2 carrier, not to its binary storage fields.
143
19
    if (primitive_type == TYPE_VARIANT) {
144
1
        return DataTypeVariantV2SerDe::is_native_arrow_type(arrow_type) ? Status::OK() : mismatch();
145
1
    }
146
147
18
    switch (arrow_type.id()) {
148
0
    case arrow::Type::BOOL:
149
0
        return primitive_type == TYPE_BOOLEAN ? Status::OK() : mismatch();
150
0
    case arrow::Type::INT8:
151
0
        return primitive_type == TYPE_TINYINT ? Status::OK() : mismatch();
152
0
    case arrow::Type::UINT8:
153
0
        return primitive_type == TYPE_SMALLINT ? Status::OK() : mismatch();
154
0
    case arrow::Type::INT16:
155
0
        return primitive_type == TYPE_SMALLINT ? Status::OK() : mismatch();
156
0
    case arrow::Type::UINT16:
157
0
        return primitive_type == TYPE_INT ? Status::OK() : mismatch();
158
3
    case arrow::Type::INT32:
159
3
        return primitive_type == TYPE_INT ? Status::OK() : mismatch();
160
0
    case arrow::Type::UINT32:
161
0
        return primitive_type == TYPE_BIGINT ? Status::OK() : mismatch();
162
3
    case arrow::Type::INT64:
163
3
        return primitive_type == TYPE_BIGINT ? Status::OK() : mismatch();
164
0
    case arrow::Type::UINT64:
165
0
        return primitive_type == TYPE_LARGEINT ? Status::OK() : mismatch();
166
0
    case arrow::Type::HALF_FLOAT:
167
2
    case arrow::Type::FLOAT:
168
2
        return primitive_type == TYPE_FLOAT ? Status::OK() : mismatch();
169
1
    case arrow::Type::DOUBLE:
170
1
        return primitive_type == TYPE_DOUBLE ? Status::OK() : mismatch();
171
2
    case arrow::Type::STRING:
172
2
    case arrow::Type::BINARY:
173
2
    case arrow::Type::FIXED_SIZE_BINARY:
174
2
        return primitive_type == TYPE_STRING ? Status::OK() : mismatch();
175
0
    case arrow::Type::DATE32:
176
0
        return primitive_type == TYPE_DATEV2 ? Status::OK() : mismatch();
177
0
    case arrow::Type::DATE64:
178
0
        return primitive_type == TYPE_DATETIMEV2 && nested_doris_type->get_scale() == 3
179
0
                       ? Status::OK()
180
0
                       : mismatch();
181
0
    case arrow::Type::TIMESTAMP: {
182
0
        const auto& timestamp = static_cast<const arrow::TimestampType&>(arrow_type);
183
0
        const bool zoned = !timestamp.timezone().empty();
184
0
        const auto expected = zoned ? TYPE_TIMESTAMPTZ : TYPE_DATETIMEV2;
185
0
        return primitive_type == expected &&
186
0
                               nested_doris_type->get_scale() == timestamp_scale(timestamp.unit())
187
0
                       ? Status::OK()
188
0
                       : mismatch();
189
2
    }
190
0
    case arrow::Type::DECIMAL128:
191
0
    case arrow::Type::DECIMAL256: {
192
0
        const auto& decimal = static_cast<const arrow::DecimalType&>(arrow_type);
193
0
        return is_decimal(primitive_type) &&
194
0
                               nested_doris_type->get_precision() == decimal.precision() &&
195
0
                               nested_doris_type->get_scale() == decimal.scale()
196
0
                       ? Status::OK()
197
0
                       : mismatch();
198
0
    }
199
4
    case arrow::Type::LIST:
200
5
    case arrow::Type::LARGE_LIST:
201
5
    case arrow::Type::FIXED_SIZE_LIST: {
202
5
        if (primitive_type != TYPE_ARRAY) {
203
0
            return mismatch();
204
0
        }
205
5
        const auto& list = static_cast<const arrow::BaseListType&>(arrow_type);
206
5
        const auto& array = assert_cast<const DataTypeArray&>(*nested_doris_type);
207
5
        return validate_arrow_type(*list.value_type(), array.get_nested_type(), path + ".element");
208
5
    }
209
2
    case arrow::Type::STRUCT: {
210
2
        if (primitive_type != TYPE_STRUCT) {
211
0
            return mismatch();
212
0
        }
213
2
        const auto& arrow_struct = static_cast<const arrow::StructType&>(arrow_type);
214
2
        const auto& doris_struct = assert_cast<const DataTypeStruct&>(*nested_doris_type);
215
2
        if (cast_set<size_t>(arrow_struct.num_fields()) != doris_struct.get_elements().size()) {
216
0
            return mismatch();
217
0
        }
218
4
        for (int field_idx = 0; field_idx < arrow_struct.num_fields(); ++field_idx) {
219
3
            const auto& arrow_field = arrow_struct.field(field_idx);
220
3
            const auto& doris_name = doris_struct.get_element_name(field_idx);
221
3
            if (to_lower(arrow_field->name()) != to_lower(doris_name)) {
222
1
                return Status::InvalidArgument(
223
1
                        "ADBC Arrow field mismatch at '{}': runtime field '{}' does not match "
224
1
                        "cached field '{}' at ordinal {}",
225
1
                        path, arrow_field->name(), doris_name, field_idx);
226
1
            }
227
2
            RETURN_IF_ERROR(validate_arrow_type(*arrow_field->type(),
228
2
                                                doris_struct.get_element(field_idx),
229
2
                                                path + "." + doris_name));
230
2
        }
231
1
        return Status::OK();
232
2
    }
233
0
    case arrow::Type::MAP: {
234
0
        if (primitive_type != TYPE_MAP) {
235
0
            return mismatch();
236
0
        }
237
0
        const auto& arrow_map = static_cast<const arrow::MapType&>(arrow_type);
238
0
        const auto& doris_map = assert_cast<const DataTypeMap&>(*nested_doris_type);
239
0
        RETURN_IF_ERROR(validate_arrow_type(*arrow_map.key_type(), doris_map.get_key_type(),
240
0
                                            path + ".key"));
241
0
        return validate_arrow_type(*arrow_map.item_type(), doris_map.get_value_type(),
242
0
                                   path + ".value");
243
0
    }
244
0
    default:
245
0
        return mismatch();
246
18
    }
247
18
}
248
249
// Drivers allocate the strings inside AdbcError, so every populated error has to be released.
250
class AdbcErrorGuard {
251
public:
252
10
    AdbcErrorGuard() = default;
253
10
    ~AdbcErrorGuard() { reset(); }
254
    AdbcErrorGuard(const AdbcErrorGuard&) = delete;
255
    AdbcErrorGuard& operator=(const AdbcErrorGuard&) = delete;
256
257
37
    AdbcError* get() { return &_error; }
258
259
2
    std::string take_message() {
260
2
        std::string message = _error.message != nullptr ? _error.message : "";
261
2
        reset();
262
2
        return message;
263
2
    }
264
265
47
    void reset() {
266
47
        if (_error.release != nullptr) {
267
1
            _error.release(&_error);
268
1
        }
269
47
        _error = ADBC_ERROR_INIT;
270
47
    }
271
272
private:
273
    AdbcError _error = ADBC_ERROR_INIT;
274
};
275
276
2
Status adbc_call_status(const char* what, AdbcStatusCode code, AdbcErrorGuard& error) {
277
2
    const std::string message = error.take_message();
278
    // AdbcStatusCode is a uint8_t, so spell out the name as well as the number.
279
2
    return Status::InternalError("ADBC: {} failed ({}, code {}): {}", what,
280
2
                                 AdbcStatusCodeMessage(code), static_cast<int>(code),
281
2
                                 message.empty() ? "driver reported no message" : message);
282
2
}
283
284
#define RETURN_IF_ADBC_ERROR(expr, what, error)                       \
285
27
    do {                                                              \
286
27
        const AdbcStatusCode adbc_call_code = (expr);                 \
287
27
        if (adbc_call_code != ADBC_STATUS_OK) {                       \
288
2
            return adbc_call_status((what), adbc_call_code, (error)); \
289
2
        }                                                             \
290
27
        (error).reset();                                              \
291
25
    } while (0)
292
293
// The production stream: one ADBC database/connection/statement per scan range.
294
//
295
// P0 keeps them un-pooled on purpose. Reusing databases across ranges is a throughput optimization
296
// that only pays off once multiple partitions run concurrently, and an unverifiable caching layer
297
// added now would only obscure the functional path.
298
class RealAdbcStream final : public AdbcStream {
299
public:
300
5
    explicit RealAdbcStream(const TFileRangeDesc& range) : _range(range) {}
301
5
    ~RealAdbcStream() override { static_cast<void>(close()); }
302
303
5
    Status open() {
304
5
        RETURN_IF_ERROR(validate_adbc_range(_range));
305
5
        const auto& params = _range.table_format_params.adbc_params;
306
5
        const std::string& driver_path = *find_param(params, kParamDriverPath);
307
5
        const std::string& uri = *find_param(params, kParamUri);
308
5
        const auto* entrypoint = find_param(params, kParamDriverEntrypoint);
309
        // validate_adbc_range has established that exactly one of these is present.
310
5
        const auto* partition = find_param(params, kParamPartitionDescriptor);
311
312
5
        RETURN_IF_ERROR(AdbcDriverRegistry::instance().get_or_load(
313
5
                driver_path, entrypoint != nullptr ? *entrypoint : std::string(), &_driver));
314
4
        DORIS_CHECK(_driver != nullptr);
315
316
4
        AdbcErrorGuard error;
317
4
        RETURN_IF_ADBC_ERROR(_driver->DatabaseNew(&_database, error.get()), "DatabaseNew", error);
318
4
        _database_created = true;
319
4
        RETURN_IF_ERROR(_set_database_options(params, uri, error));
320
4
        RETURN_IF_ADBC_ERROR(_driver->DatabaseInit(&_database, error.get()), "DatabaseInit", error);
321
322
4
        RETURN_IF_ADBC_ERROR(_driver->ConnectionNew(&_connection, error.get()), "ConnectionNew",
323
4
                             error);
324
4
        _connection_created = true;
325
4
        RETURN_IF_ADBC_ERROR(_driver->ConnectionInit(&_connection, &_database, error.get()),
326
4
                             "ConnectionInit", error);
327
328
4
        if (partition != nullptr && !partition->empty()) {
329
2
            RETURN_IF_ERROR(_read_partition(*partition, error));
330
2
        } else {
331
2
            RETURN_IF_ERROR(_execute_query(*find_param(params, kParamQuerySql), error));
332
2
        }
333
334
        // Before Arrow ever sees it: the driver's stream may not clear its release callback, and
335
        // Arrow aborts the process when that happens. Both branches need it -- the Flight SQL
336
        // driver's ReadPartition stream breaks the contract exactly like its ExecuteQuery one.
337
1
        enforce_stream_release_contract(&_c_stream);
338
339
1
        auto reader = arrow::ImportRecordBatchReader(&_c_stream);
340
1
        if (!reader.ok()) {
341
0
            return Status::InternalError("ADBC: failed to import the result stream: {}",
342
0
                                         reader.status().ToString());
343
0
        }
344
        // ImportRecordBatchReader moves the stream's contents; the reader owns it from here.
345
1
        _reader = reader.MoveValueUnsafe();
346
1
        return Status::OK();
347
1
    }
348
349
1
    Status next(std::shared_ptr<arrow::RecordBatch>* batch) override {
350
1
        DORIS_CHECK(batch != nullptr);
351
1
        if (_reader == nullptr) {
352
0
            return Status::InternalError("ADBC: result stream is not open");
353
0
        }
354
1
        std::shared_ptr<arrow::RecordBatch> next_batch;
355
1
        const auto status = _reader->ReadNext(&next_batch);
356
1
        if (!status.ok()) {
357
0
            return Status::InternalError("ADBC: failed to read the next batch: {}",
358
0
                                         status.ToString());
359
0
        }
360
1
        *batch = std::move(next_batch);
361
1
        return Status::OK();
362
1
    }
363
364
6
    Status close() override {
365
6
        Status result = Status::OK();
366
        // Release in reverse order of creation. The reader owns the imported stream, so it has to
367
        // go before the statement that produced it.
368
6
        _reader.reset();
369
6
        if (_c_stream.release != nullptr) {
370
            // Only reachable when the import itself failed; nothing else owns the stream then.
371
0
            _c_stream.release(&_c_stream);
372
0
            _c_stream = {};
373
0
        }
374
6
        AdbcErrorGuard error;
375
6
        if (_statement_created) {
376
2
            const auto code = _driver->StatementRelease(&_statement, error.get());
377
2
            if (code != ADBC_STATUS_OK && result.ok()) {
378
0
                result = adbc_call_status("StatementRelease", code, error);
379
0
            }
380
2
            error.reset();
381
2
            _statement_created = false;
382
2
        }
383
6
        if (_connection_created) {
384
4
            const auto code = _driver->ConnectionRelease(&_connection, error.get());
385
4
            if (code != ADBC_STATUS_OK && result.ok()) {
386
0
                result = adbc_call_status("ConnectionRelease", code, error);
387
0
            }
388
4
            error.reset();
389
4
            _connection_created = false;
390
4
        }
391
6
        if (_database_created) {
392
4
            const auto code = _driver->DatabaseRelease(&_database, error.get());
393
4
            if (code != ADBC_STATUS_OK && result.ok()) {
394
0
                result = adbc_call_status("DatabaseRelease", code, error);
395
0
            }
396
4
            error.reset();
397
4
            _database_created = false;
398
4
        }
399
        // _driver itself is owned by AdbcDriverRegistry and is never released.
400
6
        return result;
401
6
    }
402
403
private:
404
    // Runs the statement FE generated. One statement per range, so this range is the whole query.
405
2
    Status _execute_query(const std::string& query_sql, AdbcErrorGuard& error) {
406
2
        RETURN_IF_ADBC_ERROR(_driver->StatementNew(&_connection, &_statement, error.get()),
407
2
                             "StatementNew", error);
408
2
        _statement_created = true;
409
2
        RETURN_IF_ADBC_ERROR(
410
2
                _driver->StatementSetSqlQuery(&_statement, query_sql.c_str(), error.get()),
411
2
                "StatementSetSqlQuery", error);
412
2
        int64_t rows_affected = -1;
413
2
        RETURN_IF_ADBC_ERROR(_driver->StatementExecuteQuery(&_statement, &_c_stream, &rows_affected,
414
2
                                                            error.get()),
415
2
                             "StatementExecuteQuery", error);
416
1
        return Status::OK();
417
2
    }
418
419
    // Reads one partition of a query FE already had the source execute. No statement is created:
420
    // ADBC reads a partition off a connection, and the whole point is that this can happen on a
421
    // different machine from the one that planned it.
422
2
    Status _read_partition(const std::string& base64_descriptor, AdbcErrorGuard& error) {
423
2
        std::string descriptor;
424
2
        if (!base64_decode(base64_descriptor, &descriptor)) {
425
1
            return Status::InvalidArgument("ADBC: the '{}' parameter is not valid base64",
426
1
                                           kParamPartitionDescriptor);
427
1
        }
428
1
        RETURN_IF_ADBC_ERROR(
429
1
                _driver->ConnectionReadPartition(
430
1
                        &_connection, reinterpret_cast<const uint8_t*>(descriptor.data()),
431
1
                        descriptor.size(), &_c_stream, error.get()),
432
1
                "ConnectionReadPartition", error);
433
0
        return Status::OK();
434
1
    }
435
436
    Status _set_database_options(const std::map<std::string, std::string>& params,
437
4
                                 const std::string& uri, AdbcErrorGuard& error) {
438
4
        RETURN_IF_ADBC_ERROR(
439
4
                _driver->DatabaseSetOption(&_database, ADBC_OPTION_URI, uri.c_str(), error.get()),
440
4
                "DatabaseSetOption(uri)", error);
441
8
        for (const auto* key : {kParamUsername, kParamPassword}) {
442
8
            const auto* value = find_param(params, key);
443
8
            if (value == nullptr || value->empty()) {
444
8
                continue;
445
8
            }
446
0
            RETURN_IF_ADBC_ERROR(
447
0
                    _driver->DatabaseSetOption(&_database, key, value->c_str(), error.get()),
448
0
                    "DatabaseSetOption(credentials)", error);
449
0
        }
450
12
        for (const auto& [key, value] : params) {
451
12
            if (!key.starts_with(kAdbcOptionPrefix)) {
452
12
                continue;
453
12
            }
454
0
            RETURN_IF_ADBC_ERROR(
455
0
                    _driver->DatabaseSetOption(&_database, key.c_str(), value.c_str(), error.get()),
456
0
                    "DatabaseSetOption(passthrough)", error);
457
0
        }
458
4
        return Status::OK();
459
4
    }
460
461
    const TFileRangeDesc _range;
462
    const AdbcDriver* _driver = nullptr;
463
    AdbcDatabase _database {};
464
    AdbcConnection _connection {};
465
    AdbcStatement _statement {};
466
    ArrowArrayStream _c_stream {};
467
    std::shared_ptr<arrow::RecordBatchReader> _reader;
468
    bool _database_created = false;
469
    bool _connection_created = false;
470
    bool _statement_created = false;
471
};
472
473
5
Status create_real_adbc_stream(const TFileRangeDesc& range, std::unique_ptr<AdbcStream>* out) {
474
5
    DORIS_CHECK(out != nullptr);
475
5
    auto stream = std::make_unique<RealAdbcStream>(range);
476
5
    RETURN_IF_ERROR(stream->open());
477
1
    *out = std::move(stream);
478
1
    return Status::OK();
479
5
}
480
481
ColumnDefinition adbc_child_definition(const std::string& name, DataTypePtr type, int32_t local_id);
482
483
// Mirrors synthesize_remote_doris_children in remote_doris_reader.cpp. Both readers expose table
484
// slots as file columns, so complex columns still need structural children for TableColumnMapper.
485
// Kept separate rather than shared to avoid reshaping the already-shipped remote_doris reader.
486
27
std::vector<ColumnDefinition> synthesize_adbc_children(const DataTypePtr& type) {
487
27
    std::vector<ColumnDefinition> children;
488
27
    DORIS_CHECK(type != nullptr);
489
27
    const auto nested_type = remove_nullable(type);
490
27
    switch (nested_type->get_primitive_type()) {
491
7
    case TYPE_ARRAY: {
492
7
        const auto* array_type = assert_cast<const DataTypeArray*>(nested_type.get());
493
7
        children.push_back(adbc_child_definition("element", array_type->get_nested_type(), 0));
494
7
        break;
495
0
    }
496
0
    case TYPE_MAP: {
497
0
        const auto* map_type = assert_cast<const DataTypeMap*>(nested_type.get());
498
0
        children.push_back(adbc_child_definition("key", map_type->get_key_type(), 0));
499
0
        children.push_back(adbc_child_definition("value", map_type->get_value_type(), 1));
500
0
        break;
501
0
    }
502
2
    case TYPE_STRUCT: {
503
2
        const auto* struct_type = assert_cast<const DataTypeStruct*>(nested_type.get());
504
2
        children.reserve(struct_type->get_elements().size());
505
6
        for (size_t idx = 0; idx < struct_type->get_elements().size(); ++idx) {
506
4
            children.push_back(adbc_child_definition(struct_type->get_element_name(idx),
507
4
                                                     struct_type->get_element(idx),
508
4
                                                     cast_set<int32_t>(idx)));
509
4
        }
510
2
        break;
511
0
    }
512
18
    default:
513
18
        break;
514
27
    }
515
27
    return children;
516
27
}
517
518
ColumnDefinition adbc_child_definition(const std::string& name, DataTypePtr type,
519
11
                                       int32_t local_id) {
520
11
    ColumnDefinition child;
521
11
    child.identifier = Field::create_field<TYPE_STRING>(name);
522
11
    child.local_id = local_id;
523
11
    child.name = name;
524
11
    child.type = std::move(type);
525
11
    child.children = synthesize_adbc_children(child.type);
526
11
    return child;
527
11
}
528
529
// A stream that forwards everything to the driver's and, on release, does the one thing some
530
// drivers forget: clear its own release callback. Heap-allocated because Arrow keeps only the
531
// ArrowArrayStream it was handed, and the delegate has to outlive this function.
532
struct DelegatingStream {
533
    ArrowArrayStream inner;
534
};
535
536
1
int delegating_get_schema(ArrowArrayStream* self, ArrowSchema* out) {
537
1
    auto& inner = static_cast<DelegatingStream*>(self->private_data)->inner;
538
1
    return inner.get_schema(&inner, out);
539
1
}
540
541
2
int delegating_get_next(ArrowArrayStream* self, ArrowArray* out) {
542
2
    auto& inner = static_cast<DelegatingStream*>(self->private_data)->inner;
543
2
    return inner.get_next(&inner, out);
544
2
}
545
546
1
const char* delegating_get_last_error(ArrowArrayStream* self) {
547
1
    auto& inner = static_cast<DelegatingStream*>(self->private_data)->inner;
548
1
    return inner.get_last_error != nullptr ? inner.get_last_error(&inner) : nullptr;
549
1
}
550
551
3
void delegating_release(ArrowArrayStream* self) {
552
3
    auto* delegate = static_cast<DelegatingStream*>(self->private_data);
553
3
    if (delegate->inner.release != nullptr) {
554
3
        delegate->inner.release(&delegate->inner);
555
3
    }
556
3
    delete delegate;
557
3
    self->private_data = nullptr;
558
    // What the driver failed to do, and what Arrow aborts the process over.
559
3
    self->release = nullptr;
560
3
}
561
562
} // namespace
563
564
4
void enforce_stream_release_contract(ArrowArrayStream* stream) {
565
4
    DORIS_CHECK(stream != nullptr);
566
4
    if (stream->release == nullptr) {
567
        // Already released; nothing to delegate to, and wrapping it would hand Arrow a stream
568
        // whose callbacks dereference a released delegate.
569
1
        return;
570
1
    }
571
3
    auto* delegate = new DelegatingStream {.inner = *stream};
572
3
    *stream = ArrowArrayStream {.get_schema = delegating_get_schema,
573
3
                                .get_next = delegating_get_next,
574
3
                                .get_last_error = delegating_get_last_error,
575
3
                                .release = delegating_release,
576
3
                                .private_data = delegate};
577
3
}
578
579
AdbcFileReader::AdbcFileReader(std::shared_ptr<io::FileSystemProperties>& system_properties,
580
                               std::unique_ptr<io::FileDescription>& file_description,
581
                               std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile* profile,
582
                               const TFileRangeDesc& range,
583
                               const std::vector<SlotDescriptor*>& file_slot_descs,
584
                               AdbcStreamFactory stream_factory)
585
25
        : FileReader(system_properties, file_description, std::move(io_ctx), profile),
586
25
          _range(range),
587
25
          _file_slot_descs(file_slot_descs),
588
25
          _stream_factory(std::move(stream_factory)) {
589
25
    TimezoneUtils::find_cctz_time_zone(TimezoneUtils::default_time_zone, _ctz);
590
25
}
591
592
25
AdbcFileReader::~AdbcFileReader() {
593
25
    static_cast<void>(close());
594
25
}
595
596
24
void AdbcFileReader::_init_profile() {
597
24
    if (_profile == nullptr) {
598
0
        return;
599
0
    }
600
24
    const auto hierarchy = file_scan_profile::ensure_hierarchy(_profile);
601
24
    _io_time = hierarchy.io;
602
24
    static const char* adbc_profile = "AdbcFileReader";
603
24
    _total_time =
604
24
            ADD_CHILD_TIMER_WITH_LEVEL(_profile, adbc_profile, file_scan_profile::FILE_READER, 1);
605
24
    _open_stream_time = ADD_CHILD_TIMER_WITH_LEVEL(_profile, "AdbcOpenStreamTime", adbc_profile, 1);
606
24
    _next_batch_time = ADD_CHILD_TIMER_WITH_LEVEL(_profile, "AdbcNextBatchTime", adbc_profile, 1);
607
24
    _normalize_time = ADD_CHILD_TIMER_WITH_LEVEL(_profile, "AdbcNormalizeTime", adbc_profile, 1);
608
24
    _materialize_time =
609
24
            ADD_CHILD_TIMER_WITH_LEVEL(_profile, "AdbcMaterializeTime", adbc_profile, 1);
610
24
    _filter_time = ADD_CHILD_TIMER_WITH_LEVEL(_profile, "AdbcFilterTime", adbc_profile, 1);
611
24
}
612
613
24
Status AdbcFileReader::init(RuntimeState* state) {
614
24
    _init_profile();
615
24
    SCOPED_TIMER(_total_time);
616
24
    _runtime_state = state;
617
24
    RETURN_IF_ERROR(validate_adbc_range(_range));
618
18
    RETURN_IF_ERROR(_build_col_name_to_file_id());
619
18
    _eof = false;
620
18
    return Status::OK();
621
18
}
622
623
14
Status AdbcFileReader::get_schema(std::vector<ColumnDefinition>* file_schema) const {
624
14
    SCOPED_TIMER(_total_time);
625
14
    DORIS_CHECK(file_schema != nullptr);
626
14
    file_schema->clear();
627
14
    file_schema->reserve(_file_slot_descs.size());
628
30
    for (size_t idx = 0; idx < _file_slot_descs.size(); ++idx) {
629
16
        const auto* slot = _file_slot_descs[idx];
630
16
        DORIS_CHECK(slot != nullptr);
631
16
        file_schema->push_back({
632
16
                .identifier = Field::create_field<TYPE_INT>(cast_set<int32_t>(idx)),
633
16
                .local_id = cast_set<int32_t>(idx),
634
16
                .name = slot->col_name(),
635
16
                .type = slot->type(),
636
16
                .children = synthesize_adbc_children(slot->type()),
637
16
        });
638
16
    }
639
14
    return Status::OK();
640
14
}
641
642
std::unique_ptr<TableColumnMapper> AdbcFileReader::create_column_mapper(
643
2
        TableColumnMapperOptions options) const {
644
    // ADBC streams return complete Arrow roots, so TableReader must own every nested projection and
645
    // reorder after the full source shape has been materialized.
646
2
    return std::make_unique<MaterializedColumnMapper>(std::move(options));
647
2
}
648
649
18
Status AdbcFileReader::open(std::shared_ptr<FileScanRequest> request) {
650
18
    SCOPED_TIMER(_total_time);
651
18
    SCOPED_TIMER(_open_stream_time);
652
18
    RETURN_IF_ERROR(FileReader::open(std::move(request)));
653
18
    RETURN_IF_ERROR(_open_stream());
654
14
    _eof = false;
655
14
    return Status::OK();
656
18
}
657
658
16
Status AdbcFileReader::get_block(Block* file_block, size_t* rows, bool* eof) {
659
16
    SCOPED_TIMER(_total_time);
660
16
    DORIS_CHECK(file_block != nullptr);
661
16
    DORIS_CHECK(rows != nullptr);
662
16
    DORIS_CHECK(eof != nullptr);
663
16
    if (_stream == nullptr) {
664
0
        return Status::InternalError("ADBC reader is not open");
665
0
    }
666
16
    if (_io_ctx != nullptr && _io_ctx->should_stop) {
667
        // Observe cancellation before entering a potentially blocking driver read.
668
0
        RETURN_IF_ERROR(close());
669
0
        *rows = 0;
670
0
        *eof = true;
671
0
        return Status::OK();
672
0
    }
673
674
16
    *rows = 0;
675
16
    *eof = false;
676
16
    std::shared_ptr<arrow::RecordBatch> batch;
677
16
    {
678
16
        SCOPED_TIMER(_io_time);
679
16
        SCOPED_TIMER(_next_batch_time);
680
16
        RETURN_IF_ERROR(_stream->next(&batch));
681
16
    }
682
16
    if (batch == nullptr) {
683
2
        *eof = true;
684
2
        _eof = true;
685
2
        return Status::OK();
686
2
    }
687
688
14
    {
689
14
        SCOPED_TIMER(_materialize_time);
690
14
        RETURN_IF_ERROR(_materialize_record_batch(*batch, file_block, rows));
691
14
    }
692
7
    _record_scan_rows(cast_set<int64_t>(*rows));
693
7
    {
694
7
        SCOPED_TIMER(_filter_time);
695
7
        RETURN_IF_ERROR(
696
7
                apply_materialized_reader_filters(_request.get(), _io_ctx.get(), file_block, rows));
697
7
    }
698
7
    return Status::OK();
699
7
}
700
701
36
Status AdbcFileReader::close() {
702
36
    SCOPED_TIMER(_total_time);
703
36
    if (_stream != nullptr) {
704
14
        RETURN_IF_ERROR(_stream->close());
705
14
        _stream.reset();
706
14
    }
707
36
    _request.reset();
708
36
    _eof = true;
709
36
    return Status::OK();
710
36
}
711
712
18
Status AdbcFileReader::_open_stream() {
713
18
    DORIS_CHECK(_stream == nullptr);
714
18
    if (_stream_factory) {
715
13
        RETURN_IF_ERROR(_stream_factory(_range, &_stream));
716
13
    } else {
717
5
        RETURN_IF_ERROR(create_real_adbc_stream(_range, &_stream));
718
5
    }
719
14
    DORIS_CHECK(_stream != nullptr);
720
14
    return Status::OK();
721
18
}
722
723
Status AdbcFileReader::_materialize_record_batch(const arrow::RecordBatch& batch, Block* file_block,
724
14
                                                 size_t* rows) const {
725
14
    DORIS_CHECK(file_block != nullptr);
726
14
    DORIS_CHECK(rows != nullptr);
727
14
    if (_request == nullptr) {
728
0
        return Status::InternalError("ADBC reader is not open");
729
0
    }
730
731
14
    if (_col_name_to_file_id.empty()) {
732
        // A pushed-down COUNT(*) projects no columns at all: the scan wants rows counted, no values.
733
        // Counting here rather than falling into the loop below is not an optimization -- every column
734
        // the source returns is unrequested by definition in this state, so the loop's unknown-column
735
        // check would reject the first one and fail a query that asked for nothing but a number.
736
        // FE sends a one-constant-column statement for this case, so the batch is narrow.
737
        //
738
        // Only the empty case is special-cased. An unrequested column arriving alongside requested ones
739
        // still fails: that means FE and this reader disagree about the projection, and it is the one
740
        // signal that the disagreement exists.
741
1
        *rows = cast_set<size_t>(batch.num_rows());
742
1
        return Status::OK();
743
1
    }
744
745
13
    ArrowMemoryPool<> local_arrow_pool;
746
13
    arrow::MemoryPool* arrow_pool = ExecEnv::GetInstance()->arrow_memory_pool();
747
13
    if (arrow_pool == nullptr) {
748
        // Embedded and unit-test runtimes may omit ExecEnv memory initialization; keep conversions
749
        // on Doris' tracked allocator instead of falling back to Arrow's untracked default pool.
750
13
        arrow_pool = &local_arrow_pool;
751
13
    }
752
13
    std::vector<bool> materialized_columns(file_block->columns(), false);
753
22
    for (int arrow_idx = 0; arrow_idx < batch.num_columns(); ++arrow_idx) {
754
16
        const std::string& column_name = batch.schema()->field(arrow_idx)->name();
755
16
        const auto file_id_it = _col_name_to_file_id.find(column_name);
756
16
        if (file_id_it == _col_name_to_file_id.end()) {
757
1
            return Status::InternalError("ADBC source returned unknown column {}", column_name);
758
1
        }
759
15
        const auto block_position_it = _request->local_positions.find(file_id_it->second);
760
15
        if (block_position_it == _request->local_positions.end()) {
761
0
            continue;
762
0
        }
763
15
        std::shared_ptr<arrow::Array> array;
764
15
        {
765
15
            SCOPED_TIMER(_normalize_time);
766
15
            RETURN_IF_ERROR(normalize_arrow_array(batch.column(arrow_idx), arrow_pool, &array));
767
15
        }
768
15
        RETURN_IF_ERROR(_materialize_arrow_column(column_name, array, batch.num_rows(),
769
15
                                                  file_id_it->second, block_position_it->second,
770
15
                                                  file_block));
771
9
        materialized_columns[block_position_it->second.value()] = true;
772
9
    }
773
774
9
    for (const auto& [file_column_id, block_position] : _request->local_positions) {
775
9
        if (block_position.value() >= materialized_columns.size()) {
776
0
            return Status::InternalError(
777
0
                    "ADBC requested block position {} out of range, block columns {}",
778
0
                    block_position.value(), materialized_columns.size());
779
0
        }
780
9
        if (!materialized_columns[block_position.value()]) {
781
0
            return Status::InternalError("ADBC source did not return requested file column id {}",
782
0
                                         file_column_id.value());
783
0
        }
784
9
    }
785
786
6
    *rows = cast_set<size_t>(batch.num_rows());
787
6
    return Status::OK();
788
6
}
789
790
Status AdbcFileReader::_materialize_arrow_column(const std::string& column_name,
791
                                                 const std::shared_ptr<arrow::Array>& array,
792
                                                 int64_t num_rows, LocalColumnId file_column_id,
793
                                                 const LocalIndex& block_position,
794
15
                                                 Block* file_block) const {
795
15
    DORIS_CHECK(file_block != nullptr);
796
15
    DORIS_CHECK(array != nullptr);
797
15
    if (block_position.value() >= file_block->columns()) {
798
0
        return Status::InternalError("ADBC block position {} out of range, block columns {}",
799
0
                                     block_position.value(), file_block->columns());
800
0
    }
801
15
    const auto& target_type = file_block->get_by_position(block_position.value()).type;
802
803
    // The cached FE target remains authoritative when a source schema changes mid-cache-window;
804
    // otherwise SerDes without a null map can silently turn source nulls into default values.
805
15
    if (array->null_count() > 0 && !target_type->is_nullable()) {
806
2
        return Status::InternalError(
807
2
                "ADBC Arrow column '{}' contains {} null rows for non-nullable Doris type {}",
808
2
                column_name, array->null_count(), target_type->get_name());
809
2
    }
810
811
    // An all-null column arrives with a type that says nothing about the column.
812
    //
813
    // A source that infers Arrow types from the VALUES it returns -- rather than from the declared
814
    // column type -- has nothing to infer from when every value in the result is null, and picks
815
    // whatever its default is. Measured against the SQLite driver: the same TEXT column comes back
816
    // as utf8 for `SELECT id, name FROM t1` and as int64 for
817
    // `SELECT id, name FROM t1 WHERE name IS NULL`, purely because the filter left only nulls.
818
    // Handing that to the serde fails with "Unsupported arrow type for string column: 9", and no
819
    // amount of care on the FE side avoids it: FE cannot know in advance which rows will survive.
820
    //
821
    // N nulls are what this array means whatever type it claims, so materialize them directly. The
822
    // check is narrow on purpose -- a column with even one non-null value keeps its real type and
823
    // still fails loudly on a genuine mismatch, which is the signal that FE and the source disagree
824
    // about the schema rather than about one result set.
825
    //
826
    // Only for a nullable target: substituting defaults into a NOT NULL column would turn a source
827
    // that wrongly sent nulls into silently wrong data, so that keeps failing in the serde.
828
13
    if (array->null_count() == array->length() && target_type->is_nullable()) {
829
1
        auto columns_guard = file_block->mutate_columns_scoped();
830
1
        auto& columns = columns_guard.mutable_columns();
831
1
        columns[block_position.value()]->insert_many_defaults(cast_set<size_t>(num_rows));
832
1
        return Status::OK();
833
1
    }
834
835
    // Validate the whole logical shape before acquiring mutable block columns. Struct SerDes map
836
    // children by ordinal, so a stale cache entry can otherwise produce plausible but wrong values.
837
12
    RETURN_IF_ERROR(validate_arrow_type(*array->type(), target_type, column_name));
838
839
8
    auto columns_guard = file_block->mutate_columns_scoped();
840
8
    auto& columns = columns_guard.mutable_columns();
841
8
    try {
842
8
        RETURN_IF_ERROR(target_type->get_serde()->read_column_from_arrow(
843
8
                *columns[block_position.value()], array.get(), 0, num_rows, _ctz));
844
8
    } catch (const Exception& e) {
845
0
        return Status::InternalError(
846
0
                "Failed to convert ADBC Arrow column '{}' (file_column_id={}, arrow type={}) to "
847
0
                "Doris block: {}",
848
0
                column_name, file_column_id.value(), array->type()->ToString(), e.what());
849
0
    }
850
8
    return Status::OK();
851
8
}
852
853
18
Status AdbcFileReader::_build_col_name_to_file_id() {
854
18
    _col_name_to_file_id.clear();
855
18
    _col_name_to_file_id.reserve(_file_slot_descs.size());
856
50
    for (size_t idx = 0; idx < _file_slot_descs.size(); ++idx) {
857
32
        const auto* slot = _file_slot_descs[idx];
858
32
        DORIS_CHECK(slot != nullptr);
859
32
        _col_name_to_file_id.emplace(slot->col_name(), LocalColumnId(cast_set<int32_t>(idx)));
860
32
    }
861
18
    return Status::OK();
862
18
}
863
864
AdbcReader::AdbcReader(AdbcStreamFactory stream_factory)
865
1
        : _stream_factory(std::move(stream_factory)) {}
866
867
1
Status AdbcReader::init(TableReadOptions&& options) {
868
1
    if (options.file_slot_descs == nullptr) {
869
0
        return Status::InvalidArgument("ADBC reader requires file slot descriptors");
870
0
    }
871
1
    return TableReader::init(std::move(options));
872
1
}
873
874
1
Status AdbcReader::prepare_split(const SplitReadOptions& options) {
875
1
    {
876
        // Keep protocol validation visible while avoiding overlap with TableReader's own scopes.
877
1
        SCOPED_TIMER(_profile.total_timer);
878
1
        SCOPED_TIMER(_profile.prepare_split_timer);
879
1
        RETURN_IF_ERROR(validate_adbc_range(options.current_range));
880
1
    }
881
1
    return TableReader::prepare_split(options);
882
1
}
883
884
1
Status AdbcReader::create_file_reader(std::unique_ptr<FileReader>* reader) {
885
1
    DORIS_CHECK(reader != nullptr);
886
1
    DORIS_CHECK(_file_slot_descs != nullptr);
887
1
    *reader = std::make_unique<AdbcFileReader>(_system_properties, _current_task->data_file,
888
1
                                               _io_ctx, _scanner_profile, _current_file_range_desc,
889
1
                                               *_file_slot_descs, _stream_factory);
890
1
    return Status::OK();
891
1
}
892
893
} // namespace doris::format::adbc