feat(promql): merge ordered raw series inputs (#9653)

I hereby agree to the terms of the [GreptimeDB
CLA](https://github.com/GreptimeTeam/.github/blob/main/CLA.md).

## Refer to a related PR or issue link (optional)

**Depends on #9650** (labels decoder). Includes `materialize_raw_labels`
— if the source-only PR merges first, this rebases cleanly.

## What's changed and what's your intention?

Add `promql::source::merge_ordered_raw_series`: merges multiple ordered
raw-series streams into one globally ordered stream by series key and
time.

All inputs must share the same schema and label binding, be
`KeyThenTime` ordered, and have undecoded labels. The merge recomputes
per-series `is_last` globally. Errors are terminal; already-emitted
prefixes are not rolled back.

**8 files, +4118/-0 lines (622 production + 3489 tests + 7 config).**

### Verification

- Compiled and tested with #9650 merged: `cargo check --all-targets` ✅,
`cargo nextest run -p promql` ✅, `cargo clippy -- -D warnings` ✅
- Cannot compile standalone until #9650 merges.

## PR Checklist

- [x] I have written the necessary rustdoc comments.
- [x] I have added the necessary unit tests and integration tests.
- [ ] This PR requires documentation updates.
- [x] API changes are backward compatible.
- [x] Schema or data changes are backward compatible.
- [ ] This PR needs to be backported to release branches, and I have
added the `backport-<target>` labels (e.g. `backport-v1.3` targets
`release/v1.3`).
This commit is contained in:
discord9
2026-10-18 14:46:57 +08:00
committed by GitHub
4 changed files with 2902 additions and 0 deletions
+3
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod merge;
#[cfg(test)]
mod tests;
@@ -28,6 +29,8 @@ use futures::Stream;
use mito_codec::labels::{DecodedLabels, LabelProjection, PrimaryKeyLabelDecoder};
use store_api::region_engine::{RawLabelSource, RawSeriesStream, SendableRawSeriesStream};
pub use merge::merge_ordered_raw_series;
/// Materializes complete metric labels from a scan-bound raw-series source.
///
/// The caller supplies an already-bound source. Adjacent-chunk pack/unpack validation remains
+450
View File
@@ -0,0 +1,450 @@
// Copyright 2023 Greptime Team
//
// Licensed 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.
#[cfg(test)]
mod tests;
use std::cmp::Ordering;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use common_error::ext::BoxedError;
use datatypes::error::InvalidSeriesSnafu;
use datatypes::series::SeriesOrdering;
use datatypes::series::raw::RawSeriesChunk;
use datatypes::series::samples::{SeriesFields, SeriesSchema};
use futures::Stream;
use mito_codec::labels::{LabelProjection, PrimaryKeyLabelDecoder};
use store_api::region_engine::{RawLabelSource, RawSeriesStream, SendableRawSeriesStream};
/// Merge compatible, explicitly supplied raw streams by complete read key and time.
///
/// Callers provide authorized bindings and own snapshot/storage policy. This does not select an
/// MVCC view, resolve overlapping physical copies, or roll back emitted prefixes after errors.
/// `is_last` is global only over these inputs; there is no fallback, replay, or input sorting.
pub fn merge_ordered_raw_series(
inputs: Vec<SendableRawSeriesStream>,
) -> Result<SendableRawSeriesStream, BoxedError> {
let mut inputs = inputs.into_iter();
let first = inputs
.next()
.ok_or_else(|| invalid("raw merge requires at least one input"))?;
let schema = Arc::clone(first.schema());
let binding = first.label_source().clone();
let mut streams = vec![Input::new(first)];
for stream in inputs {
streams.push(Input::new(stream));
}
for input in &streams {
let stream = input
.stream
.as_ref()
.ok_or_else(|| invalid("raw merge input disappeared"))?;
validate_binding(stream.label_source())?;
validate_input(stream.as_ref().get_ref(), &schema, &binding)?;
}
Ok(Box::pin(OrderedRawMerge {
streams,
schema,
binding,
candidate: None,
}))
}
fn invalid(reason: &'static str) -> BoxedError {
BoxedError::new(InvalidSeriesSnafu { reason }.build())
}
fn validate_binding(binding: &RawLabelSource) -> Result<(), BoxedError> {
PrimaryKeyLabelDecoder::new(binding, LabelProjection::All)
.map(|_| ())
.map_err(BoxedError::new)
}
fn validate_input(
stream: &dyn RawSeriesStream,
schema: &SeriesSchema,
binding: &RawLabelSource,
) -> Result<(), BoxedError> {
let other = stream.schema();
if stream.ordering() != SeriesOrdering::KeyThenTime
|| other.eval_grid.is_some()
|| other.fields.is_empty()
|| other.fields.len() != schema.fields.len()
|| other
.fields
.iter()
.zip(schema.fields.iter())
.any(|(a, b)| a.name != b.name)
|| !bindings_compatible(stream.label_source(), binding)
{
return Err(invalid(
"raw merge requires equal raw schemas, primary-key bindings, and ReadKeyThenTime ordering",
));
}
Ok(())
}
fn bindings_compatible(left: &RawLabelSource, right: &RawLabelSource) -> bool {
let (
RawLabelSource::PrimaryKey {
metric_name: lm,
physical_metadata: lmeta,
logical_tags: lt,
},
RawLabelSource::PrimaryKey {
metric_name: rm,
physical_metadata: rmeta,
logical_tags: rt,
},
) = (left, right)
else {
return false;
};
if lm != rm
|| lt.len() != rt.len()
|| lt.iter().any(|tag| {
!rt.iter()
.any(|other| tag.column_id == other.column_id && tag.name == other.name)
})
{
return false;
}
let lm = lmeta.as_ref();
let rm = rmeta.as_ref();
lm.primary_key == rm.primary_key
&& lm.primary_key_encoding == rm.primary_key_encoding
&& lm
.primary_key
.iter()
.all(|id| match (lm.column_by_id(*id), rm.column_by_id(*id)) {
(Some(a), Some(b)) => {
a.column_id == b.column_id
&& a.column_schema.name == b.column_schema.name
&& a.column_schema.data_type == b.column_schema.data_type
&& a.semantic_type == b.semantic_type
}
_ => false,
})
}
struct Input {
stream: Option<SendableRawSeriesStream>,
head: Option<RawSeriesChunk>,
previous_key: Option<Vec<u8>>,
previous_time: Option<i64>,
ended_series: bool,
}
impl Input {
fn new(stream: SendableRawSeriesStream) -> Self {
Self {
stream: Some(stream),
head: None,
previous_key: None,
previous_time: None,
ended_series: false,
}
}
fn validate(
&mut self,
chunk: &RawSeriesChunk,
schema: &SeriesSchema,
) -> Result<(), BoxedError> {
if chunk.metric.is_some() {
return Err(invalid("raw merge requires undecoded primary-key chunks"));
}
RawSeriesChunk::try_new(
schema,
chunk.primary_key.clone(),
None,
chunk.time.clone(),
chunk.fields.clone(),
chunk.is_last,
)
.map_err(BoxedError::new)?;
let times = chunk.time.as_buffer();
if let Some(previous_key) = &self.previous_key {
match previous_key.as_slice().cmp(chunk.primary_key.as_slice()) {
Ordering::Greater => {
return Err(invalid("raw merge input read keys are not ordered"));
}
Ordering::Equal
if self.ended_series
|| self.previous_time.is_some_and(|time| times[0] <= time) =>
{
return Err(invalid(
"raw merge input has repeated, non-increasing timestamps or invalid is_last",
));
}
Ordering::Less if !self.ended_series => {
return Err(invalid("raw merge input changed read keys before is_last"));
}
_ => {}
}
}
self.previous_key = Some(chunk.primary_key.as_slice().to_vec());
self.previous_time = times.last().copied();
self.ended_series = chunk.is_last;
Ok(())
}
/// Returns whether this input can no longer contribute chunks for `key`.
///
/// A held head beyond `key` proves completion. Without a head, only an ended input or `key`'s
/// own final chunk proves completion: an earlier completed key never rules out unknown later
/// or intervening keys.
fn finished_at(&self, key: &[u8]) -> bool {
match self.head.as_ref() {
Some(head) => head.primary_key.as_slice() > key,
None => {
self.stream.is_none()
|| self.previous_key.as_deref().is_some_and(|previous| {
previous > key || (previous == key && self.ended_series)
})
}
}
}
}
struct OrderedRawMerge {
streams: Vec<Input>,
schema: Arc<SeriesSchema>,
binding: RawLabelSource,
candidate: Option<RawSeriesChunk>,
}
impl OrderedRawMerge {
fn fail(&mut self, error: BoxedError) -> Poll<Option<Result<RawSeriesChunk, BoxedError>>> {
self.streams.clear();
self.candidate = None;
Poll::Ready(Some(Err(error)))
}
/// Polls every input that has no head once, returning upstream errors before progress.
fn poll_inputs(&mut self, cx: &mut Context<'_>) -> Result<(), BoxedError> {
for input in &mut self.streams {
if input.stream.is_none() || input.head.is_some() {
continue;
}
match input
.stream
.as_mut()
.map(|stream| stream.as_mut().poll_next(cx))
{
Some(Poll::Pending) => {}
Some(Poll::Ready(Some(Err(error)))) => return Err(error),
Some(Poll::Ready(Some(Ok(chunk)))) => {
input.validate(&chunk, &self.schema)?;
input.head = Some(chunk);
}
Some(Poll::Ready(None)) => {
if input.previous_key.is_some() && !input.ended_series {
return Err(invalid("raw merge input ended before its final chunk"));
}
input.stream = None;
input.previous_key = None;
input.previous_time = None;
}
None => {}
}
}
Ok(())
}
/// Returns the index of the smallest available head, by read key then first timestamp.
fn smallest_head_index(&self) -> Option<usize> {
self.streams
.iter()
.enumerate()
.filter_map(|(index, input)| input.head.as_ref().map(|head| (index, head)))
.min_by(|(_, a), (_, b)| {
a.primary_key
.as_slice()
.cmp(b.primary_key.as_slice())
.then_with(|| a.time.timestamp(0).cmp(&b.time.timestamp(0)))
})
.map(|(index, _)| index)
}
/// Returns whether every input accounts for `key`: it holds a head, or completion is proven.
///
/// A headless input that cannot prove completion may still add chunks to `key`, so the merge
/// must wait instead of deciding `key`.
fn all_inputs_ready_at(&self, key: &[u8]) -> bool {
self.streams
.iter()
.all(|input| input.head.is_some() || input.finished_at(key))
}
fn take_next(&mut self) -> Result<Option<RawSeriesChunk>, BoxedError> {
let Some(index) = self.smallest_head_index() else {
return Ok(None);
};
let chunk = self.streams[index]
.head
.take()
.ok_or_else(|| invalid("raw merge lost selected head"))?;
let key = chunk.primary_key.as_slice();
let start = chunk
.time
.timestamp(0)
.ok_or_else(|| invalid("raw merge chunk has no timestamp"))?;
let mut end = chunk.time.len();
for input in &self.streams {
if let Some(head) = &input.head
&& head.primary_key.as_slice() == key
{
let next = head
.time
.timestamp(0)
.ok_or_else(|| invalid("raw merge chunk has no timestamp"))?;
if next == start {
return Err(invalid(
"raw merge inputs contain duplicate timestamps for a complete read key",
));
}
if next > start {
end = end.min(
chunk
.time
.as_buffer()
.partition_point(|timestamp| *timestamp < next),
);
}
}
}
if end == chunk.time.len() {
return Ok(Some(RawSeriesChunk {
primary_key: chunk.primary_key,
metric: None,
time: chunk.time,
fields: chunk.fields,
is_last: false,
}));
}
let fields = SeriesFields::try_from_vec(
chunk
.fields
.iter()
.map(|samples| samples.slice(0, end).map_err(BoxedError::new))
.collect::<Result<Vec<_>, _>>()?,
)
.map_err(BoxedError::new)?;
let time = chunk.time.slice(0, end).map_err(BoxedError::new)?;
if end < chunk.time.len() {
let remainder = SeriesFields::try_from_vec(
chunk
.fields
.iter()
.map(|samples| {
samples
.slice(end, chunk.time.len() - end)
.map_err(BoxedError::new)
})
.collect::<Result<Vec<_>, _>>()?,
)
.map_err(BoxedError::new)?;
self.streams[index].head = Some(RawSeriesChunk {
primary_key: chunk.primary_key.clone(),
metric: None,
time: chunk
.time
.slice(end, chunk.time.len() - end)
.map_err(BoxedError::new)?,
fields: remainder,
is_last: chunk.is_last,
});
}
Ok(Some(RawSeriesChunk {
primary_key: chunk.primary_key,
metric: None,
time,
fields,
is_last: false,
}))
}
}
impl Stream for OrderedRawMerge {
type Item = Result<RawSeriesChunk, BoxedError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.streams.is_empty() {
return Poll::Ready(None);
}
if this.candidate.is_none() {
if let Err(error) = this.poll_inputs(cx) {
return this.fail(error);
}
let Some(head) = this
.smallest_head_index()
.and_then(|index| this.streams[index].head.as_ref())
else {
// Only a fully drained input set is EOF: a live input may still reveal future keys
// or a late error, so its state stays owned instead of being cleared.
if this.streams.iter().all(|input| input.stream.is_none()) {
this.streams.clear();
return Poll::Ready(None);
}
return Poll::Pending;
};
// A headless input that cannot prove completion at the smallest available key may
// still add chunks to it, so the merge waits before consuming that head.
if !this.all_inputs_ready_at(head.primary_key.as_slice()) {
return Poll::Pending;
}
match this.take_next() {
Ok(candidate) => this.candidate = candidate,
Err(error) => return this.fail(error),
}
}
if let Err(error) = this.poll_inputs(cx) {
return this.fail(error);
}
let Some(candidate) = this.candidate.take() else {
this.streams.clear();
return Poll::Ready(None);
};
if !this.all_inputs_ready_at(candidate.primary_key.as_slice()) {
// The candidate's key may still be extended or interleaved by a headless input: an
// already-decided prefix is held back instead of being emitted as if it were final.
this.candidate = Some(candidate);
return Poll::Pending;
}
let is_last = this
.streams
.iter()
.all(|input| input.finished_at(candidate.primary_key.as_slice()));
Poll::Ready(Some(Ok(RawSeriesChunk {
is_last,
..candidate
})))
}
}
impl RawSeriesStream for OrderedRawMerge {
fn schema(&self) -> &Arc<SeriesSchema> {
&self.schema
}
fn label_source(&self) -> &RawLabelSource {
&self.binding
}
fn ordering(&self) -> SeriesOrdering {
SeriesOrdering::KeyThenTime
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,627 @@
// Copyright 2023 Greptime Team
//
// Licensed 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.
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::{Arc, Weak};
use std::task::{Context, Poll};
use common_error::ext::BoxedError;
use common_query::series_frame::{pack_raw_series, unpack_raw_series};
use datatypes::arrow::array::{Array, Float64Array, ListArray, UInt32Array};
use datatypes::arrow::buffer::{Buffer, ScalarBuffer};
use datatypes::series::SeriesOrdering;
use datatypes::series::histogram::{
BucketSideView, CounterResetHint, HistogramBatch, HistogramBatchBuilder, HistogramView,
};
use datatypes::series::raw::{RawSeriesChunk, RawTimestamps};
use datatypes::series::samples::{
DenseSamplesRef, FieldSpec, HistogramSamples, SampleRef, Samples, SeriesFields, SeriesSchema,
};
use futures::{Stream, StreamExt, TryStreamExt};
use store_api::region_engine::{RawLabelSource, RawSeriesStream, SendableRawSeriesStream};
use super::{label_source, read_key};
use crate::source::{materialize_raw_labels, merge_ordered_raw_series};
fn payload_schema() -> Arc<SeriesSchema> {
Arc::new(SeriesSchema {
fields: vec!["temperature", "latency", "observations"]
.into_iter()
.map(|name| FieldSpec {
name: Arc::from(name),
})
.collect::<Vec<_>>()
.into(),
eval_grid: None,
})
}
fn histograms(sums: &[f64]) -> Arc<HistogramBatch> {
let mut builder = HistogramBatchBuilder::new();
for &sum in sums {
builder
.push(HistogramView {
schema: -19,
zero_threshold: 0.125,
sum,
count: 23.5,
zero_count: 2.25,
reset_hint: CounterResetHint::Unrecognized(37),
start_timestamp: Some(-456),
custom_values: &[1.5, 4.5],
positive: BucketSideView {
span_offsets: &[1],
span_lengths: &[2],
buckets: &[3.25, 7.5],
},
negative: BucketSideView {
span_offsets: &[-2],
span_lengths: &[1],
buckets: &[0.75],
},
})
.unwrap();
}
builder.finish().unwrap()
}
fn payload_chunk(
key: Buffer,
times: &[i64],
floats: &[Option<f64>],
latency: &[Option<f64>],
mixed_sums: &[Option<f64>],
histogram_sums: &[Option<f64>],
last: bool,
) -> RawSeriesChunk {
assert_eq!(times.len(), floats.len());
assert_eq!(times.len(), latency.len());
assert_eq!(times.len(), mixed_sums.len());
assert_eq!(times.len(), histogram_sums.len());
let mixed_floats = Float64Array::from(latency.to_vec());
let mut mixed_index = 0;
let mixed_indices = UInt32Array::from(
mixed_sums
.iter()
.map(|sum| {
sum.map(|_| {
let index = mixed_index;
mixed_index += 1;
index
})
})
.collect::<Vec<_>>(),
);
let mixed = Samples::try_from_mixed(
mixed_floats,
HistogramSamples::try_new(
mixed_indices,
histograms(&mixed_sums.iter().flatten().copied().collect::<Vec<_>>()),
)
.unwrap(),
)
.unwrap();
let mixed = if times.len() > 1 {
Samples::try_from_chunks(vec![
mixed.slice(0, 1).unwrap(),
mixed.slice(1, times.len() - 1).unwrap(),
])
.unwrap()
} else {
mixed
};
let mut histogram_index = 0;
let histogram_indices = UInt32Array::from(
histogram_sums
.iter()
.map(|sum| {
sum.map(|_| {
let index = histogram_index;
histogram_index += 1;
index
})
})
.collect::<Vec<_>>(),
);
let fields = SeriesFields::try_from_vec(vec![
Samples::from_float(Float64Array::from(floats.to_vec())),
mixed,
{
let table = histograms(&histogram_sums.iter().flatten().copied().collect::<Vec<_>>());
let samples = Samples::try_from_histogram(histogram_indices, table).unwrap();
if times.len() > 1 {
Samples::try_from_chunks(vec![
samples.slice(0, 1).unwrap(),
samples.slice(1, times.len() - 1).unwrap(),
])
.unwrap()
} else {
samples
}
},
])
.unwrap();
RawSeriesChunk::try_new(
&payload_schema(),
key,
None,
RawTimestamps::try_new(ScalarBuffer::from(times.to_vec())).unwrap(),
fields,
last,
)
.unwrap()
}
struct PayloadStream {
schema: Arc<SeriesSchema>,
labels: RawLabelSource,
items: VecDeque<Result<RawSeriesChunk, BoxedError>>,
}
impl Stream for PayloadStream {
type Item = Result<RawSeriesChunk, BoxedError>;
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Ready(self.items.pop_front())
}
}
impl RawSeriesStream for PayloadStream {
fn schema(&self) -> &Arc<SeriesSchema> {
&self.schema
}
fn label_source(&self) -> &RawLabelSource {
&self.labels
}
fn ordering(&self) -> SeriesOrdering {
SeriesOrdering::KeyThenTime
}
}
fn payload_stream(labels: RawLabelSource, chunks: Vec<RawSeriesChunk>) -> SendableRawSeriesStream {
Box::pin(PayloadStream {
schema: payload_schema(),
labels,
items: chunks.into_iter().map(Ok).collect(),
})
}
fn assert_histogram(value: HistogramView<'_>, sum: f64) {
assert_eq!(value.schema, -19);
assert_eq!(value.zero_threshold, 0.125);
assert_eq!(value.sum.to_bits(), sum.to_bits());
assert_eq!(value.count, 23.5);
assert_eq!(value.zero_count, 2.25);
assert_eq!(value.reset_hint, CounterResetHint::Unrecognized(37));
assert_eq!(value.start_timestamp, Some(-456));
assert_eq!(value.custom_values, &[1.5, 4.5]);
assert_eq!(value.positive.span_offsets, &[1]);
assert_eq!(value.positive.span_lengths, &[2]);
assert_eq!(value.positive.buckets, &[3.25, 7.5]);
assert_eq!(value.negative.span_offsets, &[-2]);
assert_eq!(value.negative.span_lengths, &[1]);
assert_eq!(value.negative.buckets, &[0.75]);
}
#[tokio::test]
async fn merges_typed_multifield_payload_through_frame_and_label_roundtrip() {
let key = read_key("rack-payload", "sensor-7");
let a = payload_stream(
label_source(1),
vec![payload_chunk(
key.clone(),
&[10, 30],
&[Some(f64::from_bits(0x7ff8_0000_0000_0042)), Some(-0.0)],
&[Some(1.25), None],
&[None, Some(11.0)],
&[Some(101.0), None],
true,
)],
);
let b = payload_stream(
label_source(2),
vec![payload_chunk(
key.clone(),
&[20, 40],
&[None, Some(f64::from_bits(0x7ff0_0000_0000_0002))],
&[None, None],
&[None, Some(21.0)],
&[Some(202.0), Some(303.0)],
true,
)],
);
let merged = merge_ordered_raw_series(vec![a, b]).unwrap();
let frame_schema = merged.schema().clone();
let frame_binding = merged.label_source().clone();
let frame_ordering = merged.ordering();
let framed = pack_raw_series(merged).unwrap();
let raw = unpack_raw_series(framed, frame_schema, frame_binding, frame_ordering).unwrap();
let rows = materialize_raw_labels(raw)
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
assert_eq!(
rows.iter()
.map(|r| r.time.timestamp(0).unwrap())
.collect::<Vec<_>>(),
[10, 20, 30, 40]
);
assert!(rows.iter().all(|r| r.primary_key == key));
for row in &rows {
let metric = row.metric.as_ref().unwrap();
assert_eq!(metric.name(), Some("room_temperature"));
assert_eq!(metric.get("rack"), Some("rack-payload"));
assert_eq!(metric.get("sensor"), Some("sensor-7"));
}
let floats: Vec<_> = rows
.iter()
.map(|r| match r.fields.get(0).unwrap().sample(0) {
SampleRef::Float(v) => Some(v.to_bits()),
SampleRef::Missing => None,
_ => panic!("unexpected temperature sample variant"),
})
.collect();
assert_eq!(
floats,
[
Some(0x7ff8_0000_0000_0042),
None,
Some((-0.0_f64).to_bits()),
Some(0x7ff0_0000_0000_0002)
]
);
let mixed: Vec<_> = rows
.iter()
.map(|r| match r.fields.get(1).unwrap().sample(0) {
SampleRef::Float(v) => Some(("float", v.to_bits())),
SampleRef::Missing => None,
SampleRef::Histogram(h) => {
assert_histogram(
h,
if r.time.timestamp(0) == Some(30) {
11.0
} else {
21.0
},
);
Some(("hist", 0))
}
})
.collect();
assert_eq!(
mixed,
[
Some(("float", 1.25_f64.to_bits())),
None,
Some(("hist", 0)),
Some(("hist", 0))
]
);
let histograms: Vec<_> = rows
.iter()
.map(|r| match r.fields.get(2).unwrap().sample(0) {
SampleRef::Histogram(h) => {
let expected = match r.time.timestamp(0).unwrap() {
10 => 101.0,
20 => 202.0,
40 => 303.0,
_ => panic!("unexpected histogram position"),
};
assert_histogram(h, expected);
Some(h.sum.to_bits())
}
SampleRef::Missing => None,
_ => panic!("unexpected observation sample variant"),
})
.collect();
assert_eq!(
histograms,
[
Some(101.0_f64.to_bits()),
Some(202.0_f64.to_bits()),
None,
Some(303.0_f64.to_bits())
]
);
assert!(!rows[0].is_last && !rows[1].is_last && !rows[2].is_last && rows[3].is_last);
}
#[tokio::test]
async fn merge_retains_sliced_float_time_and_histogram_owners_until_output_drop() {
let key = read_key("rack-slice", "sensor-9");
let times_owner = ScalarBuffer::from(vec![1, 11, 22, 99]);
let times = times_owner.slice(1, 2);
let floats_owner = Float64Array::from(vec![
Some(90.0),
Some(f64::from_bits(0x7ff8_0000_0000_0042)),
Some(-0.0),
Some(80.0),
]);
let floats = floats_owner.slice(1, 2);
let hist_owner = histograms(&[99.0, 12.5, 88.0]);
let hist_weak = Arc::downgrade(&hist_owner);
let sliced_hist =
Arc::new(HistogramBatch::try_from_arrow(hist_owner.as_arrow().slice(1, 1)).unwrap());
let hist_indices_owner = UInt32Array::from(vec![Some(9), Some(0), Some(0), Some(9)]);
let hist_indices = hist_indices_owner.slice(1, 2);
let mixed_floats_owner = Float64Array::from(vec![Some(90.0), Some(7.0), None, Some(80.0)]);
let mixed_floats = mixed_floats_owner.slice(1, 2);
let mixed_indices_owner = UInt32Array::from(vec![Some(9), None, Some(0), Some(9)]);
let mixed_indices = mixed_indices_owner.slice(1, 2);
let fields = SeriesFields::try_from_vec(vec![
Samples::from_float(floats.clone()),
Samples::try_from_mixed(
mixed_floats.clone(),
HistogramSamples::try_new(mixed_indices.clone(), sliced_hist.clone()).unwrap(),
)
.unwrap(),
Samples::try_from_histogram(hist_indices.clone(), sliced_hist.clone()).unwrap(),
])
.unwrap();
let chunk = RawSeriesChunk::try_new(
&payload_schema(),
key.clone(),
None,
RawTimestamps::try_new(times.clone()).unwrap(),
fields,
true,
)
.unwrap();
let time_ptr = times.as_ptr();
let key_ptr = key.as_ptr();
let float_ptr = floats.values().as_ptr();
let indices_ptr = hist_indices.values().as_ptr();
let mixed_float_ptr = mixed_floats.values().as_ptr();
let mixed_indices_ptr = mixed_indices.values().as_ptr();
let scalar_ptr = sliced_hist
.as_arrow()
.column(2)
.as_any()
.downcast_ref::<Float64Array>()
.unwrap()
.values()
.as_ptr();
let custom_ptr = sliced_hist
.as_arrow()
.column(7)
.as_any()
.downcast_ref::<ListArray>()
.unwrap()
.values()
.as_any()
.downcast_ref::<Float64Array>()
.unwrap()
.values()
.as_ptr();
let bucket_ptr = sliced_hist
.as_arrow()
.column(10)
.as_any()
.downcast_ref::<ListArray>()
.unwrap()
.values()
.as_any()
.downcast_ref::<Float64Array>()
.unwrap()
.values()
.as_ptr();
let batch_weak: Weak<HistogramBatch> = Arc::downgrade(&sliced_hist);
let interleaved = payload_chunk(
key.clone(),
&[15],
&[Some(6.0)],
&[None],
&[None],
&[Some(7.5)],
true,
);
let mut merged = merge_ordered_raw_series(vec![
payload_stream(label_source(1), vec![chunk]),
payload_stream(label_source(2), vec![interleaved]),
])
.unwrap();
assert_eq!(
merged
.schema()
.fields
.iter()
.map(|field| field.name.as_ref())
.collect::<Vec<_>>(),
["temperature", "latency", "observations"]
);
assert!(
matches!(merged.label_source(), RawLabelSource::PrimaryKey { metric_name, .. } if metric_name.as_ref() == "room_temperature")
);
assert_eq!(merged.ordering(), SeriesOrdering::KeyThenTime);
let prefix = merged.next().await.unwrap().unwrap();
assert_eq!(prefix.time.as_buffer().as_ref(), &[11]);
assert_eq!(prefix.time.as_buffer().as_ptr(), time_ptr);
assert_eq!(prefix.primary_key.as_ptr(), key_ptr);
let interleaved = merged.next().await.unwrap().unwrap();
assert_eq!(interleaved.time.as_buffer().as_ref(), &[15]);
let remainder = merged.next().await.unwrap().unwrap();
assert_eq!(remainder.primary_key.as_ptr(), key_ptr);
assert_eq!(remainder.time.as_buffer().as_ref(), &[22]);
assert_eq!(
remainder.time.as_buffer().as_ptr(),
time_ptr.wrapping_add(1)
);
assert!(
matches!(prefix.fields.get(0).unwrap().dense().unwrap(), DenseSamplesRef::Float(a) if a.values().as_ptr() == float_ptr)
);
assert!(
matches!(remainder.fields.get(0).unwrap().dense().unwrap(), DenseSamplesRef::Float(a) if a.values().as_ptr() == float_ptr.wrapping_add(1))
);
let DenseSamplesRef::Mixed(prefix_mixed) = prefix.fields.get(1).unwrap().dense().unwrap()
else {
panic!("expected mixed payload")
};
assert_eq!(prefix_mixed.floats().values().as_ptr(), mixed_float_ptr);
assert_eq!(
prefix_mixed.histograms().indices().values().as_ptr(),
mixed_indices_ptr
);
assert!(
matches!(prefix.fields.get(1).unwrap().sample(0), SampleRef::Float(v) if v.to_bits() == 7.0_f64.to_bits())
);
let DenseSamplesRef::Mixed(remainder_mixed) = remainder.fields.get(1).unwrap().dense().unwrap()
else {
panic!("expected mixed payload")
};
assert_eq!(
remainder_mixed.floats().values().as_ptr(),
mixed_float_ptr.wrapping_add(1)
);
assert_eq!(
remainder_mixed.histograms().indices().values().as_ptr(),
mixed_indices_ptr.wrapping_add(1)
);
assert!(
matches!(remainder.fields.get(1).unwrap().sample(0), SampleRef::Histogram(h) if h.sum.to_bits() == 12.5_f64.to_bits())
);
let DenseSamplesRef::Histogram(prefix_hist) = prefix.fields.get(2).unwrap().dense().unwrap()
else {
panic!("expected histogram payload")
};
let DenseSamplesRef::Histogram(remainder_hist) =
remainder.fields.get(2).unwrap().dense().unwrap()
else {
panic!("expected histogram payload")
};
assert_eq!(prefix_hist.indices().values().as_ptr(), indices_ptr);
assert_eq!(prefix_hist.indices().values(), &[0]);
assert_histogram(prefix_hist.view_at(0).unwrap(), 12.5);
assert!(Arc::ptr_eq(prefix_hist.batch(), remainder_hist.batch()));
let output_arrow = prefix_hist.batch().as_arrow();
assert_eq!(
output_arrow
.column(2)
.as_any()
.downcast_ref::<Float64Array>()
.unwrap()
.values()
.as_ptr(),
scalar_ptr
);
assert_eq!(
output_arrow
.column(7)
.as_any()
.downcast_ref::<ListArray>()
.unwrap()
.values()
.as_any()
.downcast_ref::<Float64Array>()
.unwrap()
.values()
.as_ptr(),
custom_ptr
);
assert_eq!(
output_arrow
.column(10)
.as_any()
.downcast_ref::<ListArray>()
.unwrap()
.values()
.as_any()
.downcast_ref::<Float64Array>()
.unwrap()
.values()
.as_ptr(),
bucket_ptr
);
let negative_bucket_ptr = sliced_hist
.as_arrow()
.column(13)
.as_any()
.downcast_ref::<ListArray>()
.unwrap()
.values()
.as_any()
.downcast_ref::<Float64Array>()
.unwrap()
.values()
.as_ptr();
assert_eq!(
output_arrow
.column(13)
.as_any()
.downcast_ref::<ListArray>()
.unwrap()
.values()
.as_any()
.downcast_ref::<Float64Array>()
.unwrap()
.values()
.as_ptr(),
negative_bucket_ptr
);
assert_eq!(
remainder_hist.indices().values().as_ptr(),
indices_ptr.wrapping_add(1)
);
assert_eq!(remainder_hist.indices().values(), &[0]);
assert_histogram(remainder_hist.view_at(0).unwrap(), 12.5);
drop(merged);
drop(times_owner);
drop(times);
drop(floats_owner);
drop(floats);
drop(mixed_floats_owner);
drop(mixed_floats);
drop(mixed_indices_owner);
drop(mixed_indices);
drop(hist_owner);
drop(sliced_hist);
drop(hist_indices_owner);
drop(hist_indices);
assert!(hist_weak.upgrade().is_none());
assert!(batch_weak.upgrade().is_some());
assert_eq!(prefix.time.as_buffer().as_ref(), &[11]);
assert_eq!(remainder.time.as_buffer().as_ref(), &[22]);
let SampleRef::Histogram(prefix_histogram) = prefix.fields.get(2).unwrap().sample(0) else {
panic!("expected histogram");
};
assert_histogram(prefix_histogram, 12.5);
let SampleRef::Histogram(remainder_histogram) = remainder.fields.get(2).unwrap().sample(0)
else {
panic!("expected histogram");
};
assert_histogram(remainder_histogram, 12.5);
let SampleRef::Histogram(mixed_histogram) = remainder.fields.get(1).unwrap().sample(0) else {
panic!("expected histogram");
};
assert_histogram(mixed_histogram, 12.5);
assert!(
matches!(prefix.fields.get(0).unwrap().sample(0), SampleRef::Float(v) if v.to_bits() == 0x7ff8_0000_0000_0042)
);
assert!(
matches!(remainder.fields.get(0).unwrap().sample(0), SampleRef::Float(v) if v.to_bits() == (-0.0_f64).to_bits())
);
drop(prefix);
assert!(batch_weak.upgrade().is_some());
let SampleRef::Histogram(remainder_histogram) = remainder.fields.get(2).unwrap().sample(0)
else {
panic!("expected histogram");
};
assert_histogram(remainder_histogram, 12.5);
assert!(
matches!(remainder.fields.get(1).unwrap().sample(0), SampleRef::Histogram(h) if h.sum.to_bits() == 12.5_f64.to_bits() && h.custom_values == [1.5, 4.5] && h.positive.span_offsets == [1] && h.positive.span_lengths == [2] && h.positive.buckets == [3.25, 7.5] && h.negative.span_offsets == [-2] && h.negative.span_lengths == [1] && h.negative.buckets == [0.75])
);
drop(remainder);
drop(interleaved);
assert!(batch_weak.upgrade().is_none());
}