| 189 | } |
| 190 | |
| 191 | PJ::Expected<std::optional<double>> DataReader::latestNumericAt( |
| 192 | const QueryPoint& point, std::size_t column_index) const { |
| 193 | auto lock = engine_.lockEngine(); |
| 194 | const TopicStorage* storage = engine_.getTopicStorage(point.topic_id); |
| 195 | if (storage == nullptr) { |
| 196 | return PJ::unexpected(fmt::format("Topic {} not found", point.topic_id)); |
| 197 | } |
| 198 | const std::optional<SampleRow> row = PJ::latestAt(storage->sealedChunks(), point.t, storage->retentionFloor()); |
| 199 | if (!row.has_value()) { |
| 200 | return std::optional<double>{}; |
| 201 | } |
| 202 | // Guard the column index (a chunk committed before a mid-stream column add has |
| 203 | // fewer columns) and null cells — readNumericAsDouble reads a null as 0.0, so |
| 204 | // an explicit isNull check is what lets the Value column show "-" rather than a |
| 205 | // fabricated "0.000" for a sparsely-populated field. |
| 206 | if (column_index >= row->chunk->columns.size() || row->chunk->isNull(column_index, row->row_index)) { |
| 207 | return std::optional<double>{}; |
| 208 | } |
| 209 | return std::optional<double>{row->chunk->readNumericAsDouble(column_index, row->row_index)}; |
| 210 | } |
| 211 | |
| 212 | PJ::Expected<std::optional<std::string>> DataReader::latestStringAt( |
| 213 | const QueryPoint& point, std::size_t column_index) const { |