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 |