| 210 | } |
| 211 | |
| 212 | PJ::Expected<std::optional<std::string>> DataReader::latestStringAt( |
| 213 | const QueryPoint& point, std::size_t column_index) const { |
| 214 | auto lock = engine_.lockEngine(); |
| 215 | const TopicStorage* storage = engine_.getTopicStorage(point.topic_id); |
| 216 | if (storage == nullptr) { |
| 217 | return PJ::unexpected(fmt::format("Topic {} not found", point.topic_id)); |
| 218 | } |
| 219 | const std::optional<SampleRow> row = PJ::latestAt(storage->sealedChunks(), point.t, storage->retentionFloor()); |
| 220 | if (!row.has_value()) { |
| 221 | return std::optional<std::string>{}; |
| 222 | } |
| 223 | // Guard the column index (chunks before a mid-stream column add have fewer |
| 224 | // columns), the string-ness of the column (readString is UB on a numeric |
| 225 | // column), and nulls — then copy the value out WHILE the lock is held, since |
| 226 | // readString views chunk-internal dictionary memory that must not escape. |
| 227 | if (column_index >= row->chunk->columns.size()) { |
| 228 | return std::optional<std::string>{}; |
| 229 | } |
| 230 | const auto& descriptor = row->chunk->columns[column_index].descriptor; |
| 231 | if (descriptor == nullptr || descriptor->logical_type != PrimitiveType::kString || |
| 232 | row->chunk->isNull(column_index, row->row_index)) { |
| 233 | return std::optional<std::string>{}; |
| 234 | } |
| 235 | return std::optional<std::string>{std::string(row->chunk->readString(column_index, row->row_index))}; |
| 236 | } |
| 237 | |
| 238 | Expected<SeriesReader> DataReader::series(TopicId topic_id, std::size_t column_index) const { |
| 239 | auto lock = engine_.lockEngine(); |