Coverage Report

Created: 2026-08-06 22:56

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