Coverage Report

Created: 2026-11-07 17:03

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