| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * License); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| #include "single_device_tsblock_reader.h" |
| |
| #include <algorithm> |
| #include <iostream> |
| #include <set> |
| |
| #include "common/db_common.h" |
| |
| namespace storage { |
| |
| namespace { |
| const char* kTimeOnlyContextName = "__time_only_aligned_context__"; |
| } |
| |
| SingleDeviceTsBlockReader::SingleDeviceTsBlockReader( |
| DeviceQueryTask* device_query_task, uint32_t block_size, |
| IMetadataQuerier* metadata_querier, TsFileIOReader* tsfile_io_reader, |
| Filter* time_filter, Filter* field_filter) |
| : device_query_task_(device_query_task), |
| field_filter_(field_filter), |
| block_size_(block_size), |
| tuple_desc_(), |
| tsfile_io_reader_(tsfile_io_reader) {} |
| |
| int SingleDeviceTsBlockReader::init(DeviceQueryTask* device_query_task, |
| uint32_t block_size, Filter* time_filter, |
| Filter* field_filter) { |
| remaining_offset_ = 0; |
| remaining_limit_ = -1; |
| dense_row_count_ = -1; |
| return init_internal(device_query_task, block_size, time_filter, |
| field_filter); |
| } |
| |
| int SingleDeviceTsBlockReader::init(DeviceQueryTask* device_query_task, |
| uint32_t block_size, Filter* time_filter, |
| Filter* field_filter, int row_offset, |
| int row_limit) { |
| remaining_offset_ = row_offset; |
| remaining_limit_ = row_limit; |
| dense_row_count_ = -1; |
| return init_internal(device_query_task, block_size, time_filter, |
| field_filter); |
| } |
| |
| int32_t SingleDeviceTsBlockReader::compute_dense_row_count( |
| const std::vector<ITimeseriesIndex*>& ts_indexes) { |
| int64_t reference_time_count = -1; |
| // Single-chunk timeseries skip per-chunk statistic serialization |
| // (see TsFileIOWriter / TimeseriesIndex::deserialize_from); when the |
| // chunk-level statistic is null, fall back to the TimeseriesIndex's |
| // top-level statistic, which summarizes that lone chunk. |
| auto count_chunk_points = [](const common::SimpleList<ChunkMeta*>& list, |
| Statistic* fallback) -> int64_t { |
| int64_t total = 0; |
| int nchunks = 0; |
| for (auto it = list.begin(); it != list.end(); it++) { |
| nchunks++; |
| if (it.get()->statistic_) { |
| total += it.get()->statistic_->count_; |
| } |
| } |
| if (total == 0 && nchunks == 1 && fallback != nullptr) { |
| total = fallback->count_; |
| } |
| return total; |
| }; |
| for (const auto* ts_index : ts_indexes) { |
| if (ts_index == nullptr) { |
| continue; |
| } |
| |
| int64_t time_count = 0; |
| int64_t value_count = 0; |
| |
| if (ts_index->is_aligned()) { |
| auto* time_list = ts_index->get_time_chunk_meta_list(); |
| auto* value_list = ts_index->get_value_chunk_meta_list(); |
| if (time_list == nullptr || value_list == nullptr) { |
| return -1; |
| } |
| // Use the time-side and value-side top stats independently: |
| // the value-side count_ excludes nulls, so reusing it for the |
| // time chunk would misclassify sparse data as dense. |
| const auto* aligned_ti = |
| dynamic_cast<const AlignedTimeseriesIndex*>(ts_index); |
| if (aligned_ti == nullptr) { |
| return -1; |
| } |
| Statistic* time_top_stat = |
| aligned_ti->time_ts_idx_ != nullptr |
| ? aligned_ti->time_ts_idx_->get_statistic() |
| : nullptr; |
| Statistic* value_top_stat = |
| aligned_ti->value_ts_idx_ != nullptr |
| ? aligned_ti->value_ts_idx_->get_statistic() |
| : nullptr; |
| time_count = count_chunk_points(*time_list, time_top_stat); |
| value_count = count_chunk_points(*value_list, value_top_stat); |
| } else { |
| auto* list = ts_index->get_chunk_meta_list(); |
| if (list == nullptr) { |
| return -1; |
| } |
| time_count = count_chunk_points(*list, ts_index->get_statistic()); |
| value_count = time_count; |
| } |
| |
| if (time_count == 0 || value_count == 0) { |
| return -1; |
| } |
| if (reference_time_count < 0) { |
| reference_time_count = time_count; |
| } else if (time_count != reference_time_count) { |
| return -1; |
| } |
| if (value_count != reference_time_count) { |
| return -1; |
| } |
| } |
| |
| if (reference_time_count < 0) { |
| return -1; |
| } |
| return static_cast<int32_t>(reference_time_count); |
| } |
| |
| int SingleDeviceTsBlockReader::init_internal(DeviceQueryTask* device_query_task, |
| uint32_t block_size, |
| Filter* time_filter, |
| Filter* field_filter) { |
| int ret = common::E_OK; |
| pa_.init(512, common::AllocModID::MOD_TSFILE_READER); |
| tuple_desc_.reset(); |
| auto table_schema = device_query_task->get_table_schema(); |
| tuple_desc_.push_back(common::g_time_column_schema); |
| for (const auto& column_name : device_query_task_->get_column_names()) { |
| common::ColumnSchema column_schema( |
| table_schema->get_column_schema(column_name)); |
| if (column_schema.is_valid() && |
| column_schema.data_type_ != common::VECTOR) { |
| tuple_desc_.push_back(column_schema); |
| } |
| } |
| time_column_index_ = 0; |
| if (RET_FAIL(common::TsBlock::create_tsblock(&tuple_desc_, current_block_, |
| block_size))) { |
| return ret; |
| } |
| col_appenders_.resize(tuple_desc_.get_column_count()); |
| for (uint32_t i = 0; i < tuple_desc_.get_column_count(); i++) { |
| col_appenders_[i] = new common::ColAppender(i, current_block_); |
| } |
| row_appender_ = new common::RowAppender(current_block_); |
| std::vector<ITimeseriesIndex*> time_series_indexs( |
| device_query_task_->get_column_mapping() |
| ->get_measurement_columns() |
| .size()); |
| if (RET_FAIL(tsfile_io_reader_->get_timeseries_indexes( |
| device_query_task->get_device_id(), |
| device_query_task->get_column_mapping()->get_measurement_columns(), |
| time_series_indexs, pa_))) { |
| return ret; |
| } |
| dense_row_count_ = compute_dense_row_count(time_series_indexs); |
| // Early device-level time skip: if time_filter is set and ALL chunks of |
| // this device have statistics that fall outside the filter range, skip the |
| // entire device. Chunks without statistics are assumed to satisfy. |
| // |
| // Skip the entire shortcut when time_series_indexs is empty (e.g. a |
| // time-only query that selects no value column): there's nothing to |
| // prove outside the filter, and dropping out here would lose the |
| // time-only fallback path that runs below. |
| if (time_filter != nullptr && !time_series_indexs.empty()) { |
| bool examined_any = false; |
| bool all_outside = true; |
| for (const auto* ts_idx : time_series_indexs) { |
| if (ts_idx == nullptr) continue; |
| auto* chunk_list = ts_idx->is_aligned() |
| ? ts_idx->get_time_chunk_meta_list() |
| : ts_idx->get_chunk_meta_list(); |
| if (chunk_list == nullptr) { |
| // No chunk metadata for this column means we can't prove it |
| // lies outside the filter, so the device can't be safely |
| // skipped. The decision is final, so stop scanning. |
| all_outside = false; |
| break; |
| } |
| examined_any = true; |
| for (auto it = chunk_list->begin(); it != chunk_list->end(); it++) { |
| if (it.get()->statistic_ == nullptr || |
| time_filter->satisfy(it.get()->statistic_)) { |
| all_outside = false; |
| break; |
| } |
| } |
| if (!all_outside) break; |
| } |
| if (examined_any && all_outside) { |
| // No data in this device matches the time filter. |
| delete current_block_; |
| current_block_ = nullptr; |
| return common::E_OK; |
| } |
| } |
| // Try multi-value aligned path: one VectorMeasurementColumnContext (and |
| // the SSI it owns) reads all aligned value columns at once. This is the |
| // entry point for AlignedChunkReader's per-column parallel decode pool |
| // (created in TsFileSeriesScanIterator::init_chunk_reader_multi when |
| // num_cols > 1 && parallel_read_enabled_); per-column |
| // SingleMeasurementColumnContext siblings would each open their own |
| // single-column SSI and never reach it. Falls back to the per-column path |
| // if ctx->init() fails (e.g. the device mixes aligned and non-aligned |
| // chunks). |
| bool used_multi = false; |
| std::set<std::string> multi_names; |
| { |
| bool can_multi = !time_series_indexs.empty(); |
| auto& meas_cols = |
| device_query_task->get_column_mapping()->get_measurement_columns(); |
| for (const auto& ts_idx : time_series_indexs) { |
| if (ts_idx == nullptr || !ts_idx->is_aligned()) { |
| can_multi = false; |
| break; |
| } |
| } |
| if (can_multi) { |
| std::vector<std::string> meas_names(meas_cols.begin(), |
| meas_cols.end()); |
| // Stable order by first appearance in the result schema so the |
| // shared SSI's column buffers line up with the result columns. |
| std::sort( |
| meas_names.begin(), meas_names.end(), |
| [device_query_task](const std::string& lhs, |
| const std::string& rhs) { |
| const auto& lhs_pos = |
| device_query_task->get_column_mapping()->get_column_pos( |
| lhs); |
| const auto& rhs_pos = |
| device_query_task->get_column_mapping()->get_column_pos( |
| rhs); |
| const int lhs_first = |
| lhs_pos.empty() ? INT32_MAX : lhs_pos.front(); |
| const int rhs_first = |
| rhs_pos.empty() ? INT32_MAX : rhs_pos.front(); |
| if (lhs_first != rhs_first) { |
| return lhs_first < rhs_first; |
| } |
| return lhs < rhs; |
| }); |
| std::vector<std::vector<int32_t>> pos_list; |
| pos_list.reserve(meas_names.size()); |
| for (const auto& name : meas_names) { |
| const auto& pos = |
| device_query_task->get_column_mapping()->get_column_pos( |
| name); |
| pos_list.push_back( |
| std::vector<int32_t>(pos.begin(), pos.end())); |
| } |
| |
| auto* ctx = new VectorMeasurementColumnContext(tsfile_io_reader_); |
| if (common::E_OK == ctx->init(device_query_task_, meas_names, |
| time_filter, pos_list, pa_)) { |
| // The shared ctx is referenced from N map entries; close() |
| // and the merge loop dedupe by pointer (already in place). |
| for (const auto& name : meas_names) { |
| field_column_contexts_.insert(std::make_pair(name, ctx)); |
| multi_names.insert(name); |
| } |
| aligned_col_count_ = meas_names.size(); |
| used_multi = true; |
| } else { |
| delete ctx; |
| } |
| } |
| } |
| |
| // Per-column path for anything not absorbed by the multi-value ctx |
| // (e.g. fallback when init() failed, or a non-aligned column would have |
| // been added before we generalize this for mixed schemas). |
| for (const auto& time_series_index : time_series_indexs) { |
| if (time_series_index == nullptr) { |
| continue; |
| } |
| const std::string measurement_name = |
| time_series_index->get_measurement_name().to_std_string(); |
| if (used_multi && multi_names.count(measurement_name) > 0) { |
| continue; |
| } |
| construct_column_context(time_series_index, time_filter, 0, -1); |
| } |
| |
| if (field_column_contexts_.empty()) { |
| // If value columns were actually requested but none produced a |
| // context, every one of them read empty under the current filter |
| // (e.g. an empty/inverted time range, or a filter that matches no |
| // rows). The result is simply empty -- return it directly. The |
| // time-only fallback below is only for genuine time-only queries (no |
| // value columns); routing an all-empty value query through it would |
| // call alloc_multi_ssi(), which is aligned-only and returns |
| // E_NOT_SUPPORT on non-aligned devices. |
| bool any_value_column_requested = false; |
| for (const auto* ts_idx : time_series_indexs) { |
| if (ts_idx != nullptr) { |
| any_value_column_requested = true; |
| break; |
| } |
| } |
| if (any_value_column_requested) { |
| delete current_block_; |
| current_block_ = nullptr; |
| return common::E_OK; |
| } |
| |
| std::vector<std::string> empty_measurements; |
| std::vector<std::vector<int32_t>> empty_positions; |
| auto* time_only_ctx = |
| new VectorMeasurementColumnContext(tsfile_io_reader_); |
| int time_only_ret = |
| time_only_ctx->init(device_query_task_, empty_measurements, |
| time_filter, empty_positions, pa_); |
| if (common::E_OK == time_only_ret) { |
| field_column_contexts_.insert( |
| std::make_pair(kTimeOnlyContextName, time_only_ctx)); |
| } else { |
| delete time_only_ctx; |
| // Only treat "no data" as an acceptable empty result; I/O |
| // errors, OOM, and corruption from the time-only init must |
| // propagate so the caller sees the actual failure instead of |
| // an empty resultset wearing E_OK. |
| if (time_only_ret != common::E_NO_MORE_DATA) { |
| delete current_block_; |
| current_block_ = nullptr; |
| return time_only_ret; |
| } |
| } |
| } |
| |
| // Detect aligned fast path: every field column comes from an aligned chunk. |
| if (!field_column_contexts_.empty() && dense_row_count_ >= 0 && |
| aligned_col_count_ == field_column_contexts_.size()) { |
| all_aligned_ = true; |
| aligned_vec_.reserve(field_column_contexts_.size()); |
| if (used_multi) { |
| // Single shared VectorMeasurementColumnContext handles all |
| // columns — push it once, otherwise we'd schedule the same |
| // bulk_copy_into N times. |
| aligned_vec_.push_back(field_column_contexts_.begin()->second); |
| } else { |
| for (auto& kv : field_column_contexts_) { |
| aligned_vec_.push_back(kv.second); |
| } |
| } |
| } |
| |
| if (field_column_contexts_.empty()) { |
| delete current_block_; |
| current_block_ = nullptr; |
| return common::E_OK; |
| } |
| |
| for (const auto& id_column : |
| device_query_task->get_column_mapping()->get_id_columns()) { |
| const auto& column_pos_in_result = |
| device_query_task->get_column_mapping()->get_column_pos(id_column); |
| int column_pos_in_id = table_schema->find_id_column_order(id_column) + |
| (!table_schema->is_virtual_table()); |
| id_column_contexts_.insert(std::make_pair( |
| id_column, |
| IdColumnContext(column_pos_in_result, column_pos_in_id))); |
| } |
| return ret; |
| } |
| |
| int SingleDeviceTsBlockReader::has_next(bool& has_next) { |
| if (!last_block_returned_) { |
| has_next = true; |
| return common::E_OK; |
| } |
| |
| if (field_column_contexts_.empty()) { |
| has_next = false; |
| return common::E_OK; |
| } |
| |
| if (remaining_limit_ == 0) { |
| has_next = false; |
| return common::E_OK; |
| } |
| |
| for (auto col_appender : col_appenders_) { |
| col_appender->reset(); |
| } |
| |
| current_block_->reset(); |
| |
| if (all_aligned_) { |
| return has_next_aligned(has_next); |
| } |
| |
| bool next_time_set = false; |
| next_time_ = -1; |
| |
| std::vector<MeasurementColumnContext*> min_time_columns; |
| while (current_block_->get_row_count() < block_size_) { |
| if (remaining_limit_ > 0 && |
| current_block_->get_row_count() >= |
| static_cast<uint32_t>(remaining_limit_)) { |
| break; |
| } |
| std::set<MeasurementColumnContext*> visited_contexts; |
| for (auto& column_context : field_column_contexts_) { |
| if (!visited_contexts.insert(column_context.second).second) { |
| continue; |
| } |
| int64_t time; |
| if (IS_FAIL(column_context.second->get_current_time(time))) { |
| continue; |
| } |
| if (!next_time_set || time < next_time_) { |
| next_time_set = true; |
| next_time_ = time; |
| min_time_columns.clear(); |
| min_time_columns.push_back(column_context.second); |
| } else if (time == next_time_) { |
| min_time_columns.push_back(column_context.second); |
| } |
| } |
| |
| if (!next_time_set) { |
| break; |
| } |
| |
| if (remaining_offset_ > 0) { |
| for (auto* col_ctx : min_time_columns) { |
| if (IS_FAIL(advance_column(col_ctx))) { |
| break; |
| } |
| } |
| remaining_offset_--; |
| min_time_columns.clear(); |
| next_time_set = false; |
| next_time_ = -1; |
| if (field_column_contexts_.empty()) { |
| break; |
| } |
| continue; |
| } |
| |
| if (IS_FAIL(fill_measurements(min_time_columns))) { |
| has_next = false; |
| return common::E_OK; |
| } else { |
| next_time_set = false; |
| next_time_ = -1; |
| } |
| |
| if (field_column_contexts_.empty()) { |
| break; |
| } |
| } |
| if (remaining_limit_ > 0 && current_block_->get_row_count() > 0) { |
| remaining_limit_ -= current_block_->get_row_count(); |
| } |
| int ret = common::E_OK; |
| if (current_block_->get_row_count() > 0) { |
| if (RET_FAIL(fill_ids())) { |
| return ret; |
| } |
| current_block_->fill_trailling_nulls(); |
| last_block_returned_ = false; |
| has_next = true; |
| return ret; |
| } |
| has_next = false; |
| return ret; |
| } |
| |
| int SingleDeviceTsBlockReader::has_next_aligned(bool& result_has_next) { |
| int ret = common::E_OK; |
| int time_in_query_index = tuple_desc_.get_time_column_index(); |
| |
| while (current_block_->get_row_count() < block_size_) { |
| if (aligned_vec_.empty()) break; |
| |
| if (remaining_limit_ == 0) break; |
| |
| // Check if first column has data. |
| uint32_t avail = aligned_vec_[0]->available_rows(); |
| if (avail == 0) { |
| for (auto* ctx : aligned_vec_) { |
| ctx->remove_from(field_column_contexts_); |
| } |
| aligned_vec_.clear(); |
| break; |
| } |
| |
| // Find the batch size: min of output capacity and all SSI |
| // availabilities. |
| uint32_t batch = block_size_ - current_block_->get_row_count(); |
| for (auto* ctx : aligned_vec_) { |
| uint32_t ctx_avail = ctx->available_rows(); |
| if (ctx_avail == 0) { |
| batch = 0; |
| break; |
| } |
| if (ctx_avail < batch) batch = ctx_avail; |
| } |
| if (batch == 0) { |
| for (auto* ctx : aligned_vec_) { |
| ctx->remove_from(field_column_contexts_); |
| } |
| aligned_vec_.clear(); |
| break; |
| } |
| |
| // Handle offset: skip rows before copying. |
| if (remaining_offset_ > 0) { |
| uint32_t skip = std::min(batch, (uint32_t)remaining_offset_); |
| for (auto* ctx : aligned_vec_) { |
| int sr = ctx->skip_rows(skip); |
| if (sr != common::E_OK) return sr; |
| } |
| remaining_offset_ -= skip; |
| continue; |
| } |
| |
| // Handle limit: cap the batch size. |
| if (remaining_limit_ > 0) { |
| batch = std::min(batch, (uint32_t)remaining_limit_); |
| } |
| |
| // First SSI: bulk copy time + values + row_count. |
| int copy_ret = aligned_vec_[0]->bulk_copy_into( |
| col_appenders_, col_appenders_[time_column_index_], row_appender_, |
| batch); |
| // E_NO_MORE_DATA is the normal end-of-stream signal; any other |
| // error (I/O, decode, corruption) must propagate to the caller |
| // instead of silently truncating the result with E_OK. |
| if (copy_ret != common::E_OK && copy_ret != common::E_NO_MORE_DATA) { |
| return copy_ret; |
| } |
| |
| // Also copy time to explicit time column if requested. |
| if (time_in_query_index != -1) { |
| common::Vector* time_vec = |
| current_block_->get_vector(time_column_index_); |
| char* time_src = |
| time_vec->get_value_data().get_data() + |
| (current_block_->get_row_count() - batch) * sizeof(int64_t); |
| col_appenders_[time_in_query_index]->bulk_append_fixed( |
| time_src, batch, sizeof(int64_t)); |
| } |
| |
| // Other SSIs: bulk copy values only (no time, no row_count). Any |
| // hard error from these columns also has to propagate; otherwise a |
| // truncated/corrupt value column would silently emit nulls. |
| for (size_t i = 1; i < aligned_vec_.size(); i++) { |
| int other_ret = aligned_vec_[i]->bulk_copy_into( |
| col_appenders_, nullptr, nullptr, batch); |
| if (other_ret != common::E_OK && |
| other_ret != common::E_NO_MORE_DATA) { |
| return other_ret; |
| } |
| } |
| |
| // Decrement limit for data already copied. |
| if (remaining_limit_ > 0) { |
| remaining_limit_ -= batch; |
| } |
| |
| // If first SSI signaled no-more-data, stop after accounting. |
| if (copy_ret == common::E_NO_MORE_DATA) break; |
| } |
| |
| if (current_block_->get_row_count() > 0) { |
| if (RET_FAIL(fill_ids())) return ret; |
| current_block_->fill_trailling_nulls(); |
| last_block_returned_ = false; |
| result_has_next = true; |
| } else { |
| result_has_next = false; |
| } |
| return ret; |
| } |
| |
| int SingleDeviceTsBlockReader::fill_measurements( |
| std::vector<MeasurementColumnContext*>& column_contexts) { |
| int ret = common::E_OK; |
| if (field_filter_ == |
| nullptr /*TODO: || field_filter_->satisfy(column_contexts)*/) { |
| row_appender_->add_row(); |
| if (!col_appenders_[time_column_index_]->add_row()) { |
| assert(false); |
| } |
| col_appenders_[time_column_index_]->append((const char*)&next_time_, |
| sizeof(next_time_)); |
| int time_in_query_index = tuple_desc_.get_time_column_index(); |
| if (time_in_query_index != -1) { |
| if (!col_appenders_[time_in_query_index]->add_row()) { |
| assert(false); |
| } |
| col_appenders_[time_in_query_index]->append( |
| (const char*)&next_time_, sizeof(next_time_)); |
| } |
| for (auto& column_context : column_contexts) { |
| column_context->fill_into(col_appenders_); |
| if (RET_FAIL(advance_column(column_context))) { |
| break; |
| } |
| } |
| |
| // Align all columns, filling with nulls where data is missing. |
| uint32_t row_count = |
| col_appenders_[time_column_index_]->get_col_row_count(); |
| for (auto& col_appender : col_appenders_) { |
| if (tuple_desc_.get_column_category( |
| col_appender->get_column_index()) != |
| common::ColumnCategory::FIELD) { |
| continue; |
| } |
| while (col_appender->get_col_row_count() < row_count) { |
| col_appender->add_row(); |
| col_appender->append_null(); |
| } |
| } |
| } |
| return ret; |
| } |
| |
| int SingleDeviceTsBlockReader::advance_column( |
| MeasurementColumnContext* column_context) { |
| int ret = column_context->move_iter(); |
| if (ret == common::E_NO_MORE_DATA) { |
| column_context->remove_from(field_column_contexts_); |
| ret = common::E_OK; |
| } |
| return ret; |
| } |
| |
| void SingleMeasurementColumnContext::remove_from( |
| std::map<std::string, MeasurementColumnContext*>& column_context_map) { |
| auto iter = column_context_map.find(column_name_); |
| if (iter != column_context_map.end()) { |
| delete iter->second; |
| column_context_map.erase(iter); |
| } |
| } |
| |
| int SingleDeviceTsBlockReader::fill_ids() { |
| int ret = common::E_OK; |
| for (const auto& entry : id_column_contexts_) { |
| const auto& id_column_context = entry.second; |
| for (int32_t pos : id_column_context.pos_in_result_) { |
| std::string* device_tag = nullptr; |
| auto device_id = device_query_task_->get_device_id(); |
| int32_t pos_in_device_id = id_column_context.pos_in_device_id_; |
| if (pos_in_device_id >= 0 && static_cast<size_t>(pos_in_device_id) < |
| device_id->get_split_seg_num()) { |
| device_tag = device_id->get_split_segname_at(pos_in_device_id); |
| } |
| |
| if (device_tag == nullptr) { |
| ret = col_appenders_[pos + 1]->fill_null( |
| current_block_->get_row_count()); |
| if (ret != common::E_OK) { |
| return ret; |
| } |
| continue; |
| } |
| |
| if (RET_FAIL(col_appenders_[pos + 1]->fill( |
| device_tag->c_str(), device_tag->length(), |
| current_block_->get_row_count()))) { |
| return ret; |
| } |
| } |
| } |
| return ret; |
| } |
| |
| int SingleDeviceTsBlockReader::next(common::TsBlock*& ret_block) { |
| bool next = false; |
| has_next(next); |
| if (!next) { |
| return common::E_NO_MORE_DATA; |
| } |
| last_block_returned_ = true; |
| ret_block = current_block_; |
| return common::E_OK; |
| } |
| |
| void SingleDeviceTsBlockReader::close() { |
| aligned_vec_.clear(); // non-owning; owned by field_column_contexts_ |
| // De-duplicate pointers before deleting: VectorMeasurementColumnContext |
| // has multiple map entries pointing to the same object. |
| std::set<MeasurementColumnContext*> unique_contexts; |
| for (auto& column_context : field_column_contexts_) { |
| unique_contexts.insert(column_context.second); |
| } |
| for (auto* ctx : unique_contexts) { |
| delete ctx; |
| } |
| for (auto& col_appender : col_appenders_) { |
| if (col_appender) { |
| delete col_appender; |
| col_appender = nullptr; |
| } |
| } |
| if (row_appender_) { |
| delete row_appender_; |
| row_appender_ = nullptr; |
| } |
| device_query_task_ = nullptr; // owned by the task iterator arena |
| if (current_block_) { |
| delete current_block_; |
| current_block_ = nullptr; |
| } |
| } |
| |
| int SingleDeviceTsBlockReader::construct_column_context( |
| const ITimeseriesIndex* time_series_index, Filter* time_filter, |
| int ssi_offset, int ssi_limit) { |
| int ret = common::E_OK; |
| if (time_series_index == nullptr || |
| (!time_series_index->is_aligned() && |
| time_series_index->get_chunk_meta_list()->empty())) { |
| } else if (time_series_index->is_aligned()) { |
| const int effective_ssi_offset = dense_row_count_ >= 0 ? ssi_offset : 0; |
| const int effective_ssi_limit = dense_row_count_ >= 0 ? ssi_limit : -1; |
| const AlignedTimeseriesIndex* aligned_time_series_index = |
| dynamic_cast<const AlignedTimeseriesIndex*>(time_series_index); |
| if (aligned_time_series_index == nullptr) { |
| assert(false); |
| } |
| if (aligned_time_series_index->value_ts_idx_ != nullptr && |
| aligned_time_series_index->value_ts_idx_->get_statistic() != |
| nullptr && |
| aligned_time_series_index->value_ts_idx_->get_statistic()->count_ == |
| 0) { |
| return ret; |
| } |
| SingleMeasurementColumnContext* column_context = |
| new SingleMeasurementColumnContext(tsfile_io_reader_); |
| if (RET_FAIL(column_context->init( |
| device_query_task_, time_series_index, time_filter, |
| device_query_task_->get_column_mapping()->get_column_pos( |
| time_series_index->get_measurement_name().to_std_string()), |
| pa_, effective_ssi_offset, effective_ssi_limit))) { |
| delete column_context; |
| return ret; |
| } |
| field_column_contexts_.insert(std::make_pair( |
| time_series_index->get_measurement_name().to_std_string(), |
| column_context)); |
| aligned_col_count_++; |
| } else { |
| SingleMeasurementColumnContext* column_context = |
| new SingleMeasurementColumnContext(tsfile_io_reader_); |
| if (RET_FAIL(column_context->init( |
| device_query_task_, time_series_index, time_filter, |
| device_query_task_->get_column_mapping()->get_column_pos( |
| time_series_index->get_measurement_name().to_std_string()), |
| pa_, ssi_offset, ssi_limit))) { |
| delete column_context; |
| return ret; |
| } |
| |
| field_column_contexts_.insert(std::make_pair( |
| time_series_index->get_measurement_name().to_std_string(), |
| column_context)); |
| } |
| return ret; |
| } |
| |
| int SingleMeasurementColumnContext::init( |
| DeviceQueryTask* device_query_task, |
| const ITimeseriesIndex* time_series_index, Filter* time_filter, |
| const std::vector<int32_t>& pos_in_result, common::PageArena& pa, |
| int ssi_offset, int ssi_limit) { |
| int ret = common::E_OK; |
| pos_in_result_ = pos_in_result; |
| column_name_ = time_series_index->get_measurement_name().to_std_string(); |
| if (RET_FAIL(tsfile_io_reader_->alloc_ssi( |
| device_query_task->get_device_id(), |
| time_series_index->get_measurement_name().to_std_string(), ssi_, pa, |
| time_filter))) { |
| } else { |
| ssi_->set_row_range(ssi_offset, ssi_limit); |
| if (RET_FAIL(get_next_tsblock(true))) { |
| } |
| } |
| return ret; |
| } |
| |
| int SingleMeasurementColumnContext::get_next_tsblock(bool alloc_mem) { |
| int ret = common::E_OK; |
| if (tsblock_ != nullptr) { |
| if (time_iter_) { |
| delete time_iter_; |
| time_iter_ = nullptr; |
| } |
| if (value_iter_) { |
| delete value_iter_; |
| value_iter_ = nullptr; |
| } |
| tsblock_->reset(); |
| } |
| if (RET_FAIL(ssi_->get_next(tsblock_, alloc_mem))) { |
| if (time_iter_) { |
| delete time_iter_; |
| time_iter_ = nullptr; |
| } |
| if (value_iter_) { |
| delete value_iter_; |
| value_iter_ = nullptr; |
| } |
| if (tsblock_) { |
| ssi_->destroy(); |
| tsblock_ = nullptr; |
| } |
| } else { |
| time_iter_ = new common::ColIterator(0, tsblock_); |
| value_iter_ = new common::ColIterator(1, tsblock_); |
| } |
| return ret; |
| } |
| |
| int SingleMeasurementColumnContext::get_current_time(int64_t& time) { |
| if (time_iter_->end()) { |
| return common::E_NO_MORE_DATA; |
| } |
| uint32_t len = 0; |
| time = *(int64_t*)(time_iter_->read(&len)); |
| return common::E_OK; |
| } |
| |
| int SingleMeasurementColumnContext::get_current_value(char*& value, |
| uint32_t& len) { |
| if (value_iter_->end()) { |
| return common::E_NO_MORE_DATA; |
| } |
| bool is_null = false; |
| value = value_iter_->read(&len, &is_null); |
| return common::E_OK; |
| } |
| |
| int SingleMeasurementColumnContext::move_iter() { |
| int ret = common::E_OK; |
| time_iter_->next(); |
| value_iter_->next(); |
| if (time_iter_->end()) { |
| if (RET_FAIL(get_next_tsblock(false))) { |
| return ret; |
| } |
| } |
| return ret; |
| } |
| |
| void SingleMeasurementColumnContext::fill_into( |
| std::vector<common::ColAppender*>& col_appenders) { |
| char* val = nullptr; |
| uint32_t len = 0; |
| if (IS_FAIL(get_current_value(val, len))) { |
| return; |
| } |
| for (int32_t pos : pos_in_result_) { |
| col_appenders[pos + 1]->add_row(); |
| if (val == nullptr) { |
| col_appenders[pos + 1]->append_null(); |
| } else { |
| col_appenders[pos + 1]->append(val, len); |
| } |
| } |
| } |
| |
| uint32_t SingleMeasurementColumnContext::available_rows() const { |
| if (!time_iter_ || time_iter_->end()) return 0; |
| return time_iter_->remaining(); |
| } |
| |
| int SingleMeasurementColumnContext::bulk_copy_into( |
| std::vector<common::ColAppender*>& col_appenders, |
| common::ColAppender* time_appender, common::RowAppender* row_appender, |
| uint32_t count) { |
| int ret = common::E_OK; |
| const uint32_t time_elem_size = sizeof(int64_t); |
| auto dt = value_iter_->get_data_type(); |
| bool is_varlen = |
| (dt == common::STRING || dt == common::TEXT || dt == common::BLOB); |
| |
| // Bulk copy time column (only first SSI does this). |
| if (time_appender) { |
| time_appender->bulk_append_fixed(time_iter_->data_ptr(), count, |
| time_elem_size); |
| } |
| |
| // Advance output row count (only first SSI does this). |
| if (row_appender) { |
| row_appender->add_rows(count); |
| } |
| |
| if (is_varlen || value_iter_->has_null()) { |
| for (uint32_t r = 0; r < count; r++) { |
| uint32_t len = 0; |
| bool is_null = false; |
| char* val = value_iter_->read(&len, &is_null); |
| for (int32_t pos : pos_in_result_) { |
| auto* appender = col_appenders[pos + 1]; |
| appender->add_row(); |
| if (is_null) { |
| appender->append_null(); |
| } else { |
| appender->append(val, len); |
| } |
| } |
| value_iter_->next(); |
| } |
| } else { |
| const uint32_t val_elem_size = common::get_data_type_size(dt); |
| char* val_ptr = value_iter_->data_ptr(); |
| for (int32_t pos : pos_in_result_) { |
| col_appenders[pos + 1]->bulk_append_fixed(val_ptr, count, |
| val_elem_size); |
| } |
| value_iter_->advance(count, val_elem_size); |
| } |
| |
| // Advance source iterators. |
| time_iter_->advance(count, time_elem_size); |
| |
| // If source TsBlock exhausted, load next. |
| if (time_iter_->end()) { |
| if (RET_FAIL(get_next_tsblock(false))) { |
| return ret; |
| } |
| } |
| return ret; |
| } |
| |
| int SingleMeasurementColumnContext::skip_rows(uint32_t count) { |
| if (!time_iter_ || time_iter_->end()) return common::E_OK; |
| const uint32_t time_elem_size = sizeof(int64_t); |
| auto dt = value_iter_->get_data_type(); |
| bool is_varlen = |
| (dt == common::STRING || dt == common::TEXT || dt == common::BLOB); |
| uint32_t to_skip = std::min(count, time_iter_->remaining()); |
| time_iter_->advance(to_skip, time_elem_size); |
| if (is_varlen || value_iter_->has_null()) { |
| for (uint32_t r = 0; r < to_skip; r++) { |
| value_iter_->next(); |
| } |
| } else { |
| const uint32_t val_elem_size = common::get_data_type_size(dt); |
| value_iter_->advance(to_skip, val_elem_size); |
| } |
| if (time_iter_->end()) { |
| // Propagate hard errors from the next-tsblock load; E_NO_MORE_DATA |
| // is the legitimate end-of-stream signal and gets squashed back to |
| // E_OK so the caller's outer loop notices via available_rows()==0. |
| int r = get_next_tsblock(false); |
| if (r != common::E_OK && r != common::E_NO_MORE_DATA) return r; |
| } |
| return common::E_OK; |
| } |
| |
| // ── VectorMeasurementColumnContext implementation ─────────────────────── |
| |
| VectorMeasurementColumnContext::~VectorMeasurementColumnContext() { |
| if (time_iter_) { |
| delete time_iter_; |
| time_iter_ = nullptr; |
| } |
| for (auto* vi : value_iters_) { |
| if (vi) delete vi; |
| } |
| value_iters_.clear(); |
| if (ssi_) { |
| ssi_->revert_tsblock(); |
| } |
| tsfile_io_reader_->revert_ssi(ssi_); |
| ssi_ = nullptr; |
| } |
| |
| int VectorMeasurementColumnContext::init( |
| DeviceQueryTask* device_query_task, |
| const std::vector<std::string>& measurement_names, Filter* time_filter, |
| std::vector<std::vector<int32_t>>& pos_in_result, common::PageArena& pa) { |
| int ret = common::E_OK; |
| pos_in_result_ = pos_in_result; |
| column_names_ = measurement_names; |
| if (RET_FAIL(tsfile_io_reader_->alloc_multi_ssi( |
| device_query_task->get_device_id(), measurement_names, ssi_, pa, |
| time_filter))) { |
| return ret; |
| } |
| if (RET_FAIL(get_next_tsblock(true))) { |
| return ret; |
| } |
| return ret; |
| } |
| |
| int VectorMeasurementColumnContext::get_next_tsblock(bool alloc_mem) { |
| int ret = common::E_OK; |
| if (tsblock_ != nullptr) { |
| if (time_iter_) { |
| delete time_iter_; |
| time_iter_ = nullptr; |
| } |
| for (auto* vi : value_iters_) { |
| if (vi) delete vi; |
| } |
| value_iters_.clear(); |
| tsblock_->reset(); |
| } |
| if (RET_FAIL(ssi_->get_next(tsblock_, alloc_mem))) { |
| if (time_iter_) { |
| delete time_iter_; |
| time_iter_ = nullptr; |
| } |
| for (auto* vi : value_iters_) { |
| if (vi) delete vi; |
| } |
| value_iters_.clear(); |
| if (tsblock_) { |
| ssi_->destroy(); |
| tsblock_ = nullptr; |
| } |
| } else { |
| time_iter_ = new common::ColIterator(0, tsblock_); |
| uint32_t num_value_cols = tsblock_->get_column_count() - 1; |
| value_iters_.reserve(num_value_cols); |
| for (uint32_t c = 0; c < num_value_cols; c++) { |
| value_iters_.push_back(new common::ColIterator(c + 1, tsblock_)); |
| } |
| } |
| return ret; |
| } |
| |
| int VectorMeasurementColumnContext::get_current_time(int64_t& time) { |
| if (!time_iter_ || time_iter_->end()) return common::E_NO_MORE_DATA; |
| uint32_t len = 0; |
| time = *(int64_t*)(time_iter_->read(&len)); |
| return common::E_OK; |
| } |
| |
| int VectorMeasurementColumnContext::get_current_value(char*& value, |
| uint32_t& len) { |
| if (value_iters_.empty() || value_iters_[0]->end()) |
| return common::E_NO_MORE_DATA; |
| bool is_null = false; |
| value = value_iters_[0]->read(&len, &is_null); |
| return common::E_OK; |
| } |
| |
| int VectorMeasurementColumnContext::move_iter() { |
| int ret = common::E_OK; |
| time_iter_->next(); |
| for (auto* vi : value_iters_) vi->next(); |
| if (time_iter_->end()) { |
| if (RET_FAIL(get_next_tsblock(false))) return ret; |
| } |
| return ret; |
| } |
| |
| void VectorMeasurementColumnContext::fill_into( |
| std::vector<common::ColAppender*>& col_appenders) { |
| for (uint32_t c = 0; c < value_iters_.size() && c < pos_in_result_.size(); |
| c++) { |
| uint32_t len = 0; |
| bool is_null = false; |
| char* val = value_iters_[c]->read(&len, &is_null); |
| for (int32_t pos : pos_in_result_[c]) { |
| col_appenders[pos + 1]->add_row(); |
| if (is_null) { |
| col_appenders[pos + 1]->append_null(); |
| } else { |
| col_appenders[pos + 1]->append(val, len); |
| } |
| } |
| } |
| } |
| |
| void VectorMeasurementColumnContext::remove_from( |
| std::map<std::string, MeasurementColumnContext*>& column_context_map) { |
| if (column_names_.empty()) { |
| for (auto it = column_context_map.begin(); |
| it != column_context_map.end();) { |
| if (it->second == this) { |
| it = column_context_map.erase(it); |
| } else { |
| ++it; |
| } |
| } |
| delete this; |
| return; |
| } |
| for (const auto& name : column_names_) { |
| column_context_map.erase(name); |
| } |
| delete this; |
| } |
| |
| uint32_t VectorMeasurementColumnContext::available_rows() const { |
| if (!time_iter_ || time_iter_->end()) return 0; |
| return time_iter_->remaining(); |
| } |
| |
| int VectorMeasurementColumnContext::bulk_copy_into( |
| std::vector<common::ColAppender*>& col_appenders, |
| common::ColAppender* time_appender, common::RowAppender* row_appender, |
| uint32_t count) { |
| int ret = common::E_OK; |
| const uint32_t time_elem_size = sizeof(int64_t); |
| |
| // Bulk copy time column (only when time_appender is provided). |
| if (time_appender) { |
| time_appender->bulk_append_fixed(time_iter_->data_ptr(), count, |
| time_elem_size); |
| } |
| |
| // Advance output row count. |
| if (row_appender) { |
| row_appender->add_rows(count); |
| } |
| |
| // Bulk copy each value column to its output positions, propagating nulls. |
| for (uint32_t c = 0; c < value_iters_.size() && c < pos_in_result_.size(); |
| c++) { |
| auto dt = value_iters_[c]->get_data_type(); |
| bool is_varlen = |
| (dt == common::STRING || dt == common::TEXT || dt == common::BLOB); |
| bool src_has_null = value_iters_[c]->has_null(); |
| |
| if (is_varlen || src_has_null) { |
| // Row-by-row copy for variable-length columns using the |
| // ColIterator next()/read() which properly tracks offsets. Fixed |
| // length columns with nulls also need this path because their |
| // payload buffer only stores non-null values. |
| auto* iter = value_iters_[c]; |
| for (uint32_t r = 0; r < count; r++) { |
| uint32_t len = 0; |
| bool is_null = false; |
| char* val = iter->read(&len, &is_null); |
| for (int32_t pos : pos_in_result_[c]) { |
| auto* appender = col_appenders[pos + 1]; |
| appender->add_row(); |
| if (is_null) { |
| appender->append_null(); |
| } else { |
| appender->append(val, len); |
| } |
| } |
| iter->next(); |
| } |
| } else { |
| // Bulk copy for fixed-length columns |
| uint32_t val_elem_size = common::get_data_type_size(dt); |
| char* val_ptr = value_iters_[c]->data_ptr(); |
| for (int32_t pos : pos_in_result_[c]) { |
| col_appenders[pos + 1]->bulk_append_fixed(val_ptr, count, |
| val_elem_size); |
| } |
| } |
| } |
| |
| // Advance all source iterators. |
| time_iter_->advance(count, time_elem_size); |
| for (uint32_t c = 0; c < value_iters_.size(); c++) { |
| auto dt = value_iters_[c]->get_data_type(); |
| bool is_varlen = |
| (dt == common::STRING || dt == common::TEXT || dt == common::BLOB); |
| if (!is_varlen && !value_iters_[c]->has_null()) { |
| uint32_t val_elem_size = common::get_data_type_size(dt); |
| value_iters_[c]->advance(count, val_elem_size); |
| } |
| // Variable-length iterators and fixed-length iterators with nulls were |
| // already advanced in the copy loop above. |
| } |
| |
| // If source TsBlock exhausted, load next. |
| if (time_iter_->end()) { |
| if (RET_FAIL(get_next_tsblock(false))) return ret; |
| } |
| return ret; |
| } |
| |
| int VectorMeasurementColumnContext::skip_rows(uint32_t count) { |
| if (!time_iter_ || time_iter_->end()) return common::E_OK; |
| const uint32_t time_elem_size = sizeof(int64_t); |
| uint32_t to_skip = std::min(count, time_iter_->remaining()); |
| time_iter_->advance(to_skip, time_elem_size); |
| for (uint32_t c = 0; c < value_iters_.size(); c++) { |
| auto dt = value_iters_[c]->get_data_type(); |
| bool is_varlen = |
| (dt == common::STRING || dt == common::TEXT || dt == common::BLOB); |
| if (!is_varlen && !value_iters_[c]->has_null()) { |
| uint32_t val_elem_size = common::get_data_type_size(dt); |
| value_iters_[c]->advance(to_skip, val_elem_size); |
| } else { |
| // Variable-length and fixed-length-with-null vectors need next() |
| // to keep the payload offset aligned with non-null rows. |
| for (uint32_t r = 0; r < to_skip; r++) { |
| value_iters_[c]->next(); |
| } |
| } |
| } |
| if (time_iter_->end()) { |
| int r = get_next_tsblock(false); |
| if (r != common::E_OK && r != common::E_NO_MORE_DATA) return r; |
| } |
| return common::E_OK; |
| } |
| |
| } // namespace storage |