Skip to main content

servers/
prom_row_builder.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Prometheus row-level helpers for converting proto `Rows` into Arrow
16//! `RecordBatch`es and aligning / normalizing their schemas against
17//! existing table schemas in the catalog.
18
19use std::collections::{HashMap, HashSet};
20use std::sync::Arc;
21
22use api::helper::ColumnDataTypeWrapper;
23use api::v1::value::ValueData;
24use api::v1::{ColumnSchema, Rows, SemanticType};
25use arrow::array::{
26    ArrayRef, Float64Builder, StringBuilder, TimestampMicrosecondBuilder,
27    TimestampMillisecondBuilder, TimestampNanosecondBuilder, TimestampSecondBuilder,
28    new_null_array,
29};
30use arrow::datatypes::{DataType as ArrowDataType, Schema as ArrowSchema};
31use arrow::record_batch::RecordBatch;
32use arrow_schema::TimeUnit;
33use common_query::prelude::{greptime_timestamp, greptime_value};
34use datatypes::data_type::DataType;
35use datatypes::prelude::ConcreteDataType;
36use snafu::{OptionExt, ResultExt, ensure};
37
38use crate::error;
39use crate::error::Result;
40use crate::pending_rows_batcher::RecordBatchWithTsIdx;
41
42/// Extract timestamp, field, and tag column names from a logical region schema.
43fn unzip_logical_region_schema(
44    target_schema: &ArrowSchema,
45) -> Result<(String, String, HashSet<String>)> {
46    let mut timestamp_column = None;
47    let mut field_column = None;
48    let mut tag_columns = HashSet::with_capacity(target_schema.fields.len().saturating_sub(2));
49    for field in target_schema.fields() {
50        if field.name() == greptime_timestamp() {
51            timestamp_column = Some(field.name().clone());
52            continue;
53        }
54
55        if field.name() == greptime_value() {
56            field_column = Some(field.name().clone());
57            continue;
58        }
59
60        if timestamp_column.is_none() && matches!(field.data_type(), ArrowDataType::Timestamp(_, _))
61        {
62            timestamp_column = Some(field.name().clone());
63            continue;
64        }
65
66        if field_column.is_none() && matches!(field.data_type(), ArrowDataType::Float64) {
67            field_column = Some(field.name().clone());
68            continue;
69        }
70        tag_columns.insert(field.name().clone());
71    }
72
73    let timestamp_column = timestamp_column.with_context(|| error::UnexpectedResultSnafu {
74        reason: "Failed to locate timestamp column in target schema".to_string(),
75    })?;
76    let field_column = field_column.with_context(|| error::UnexpectedResultSnafu {
77        reason: "Failed to locate field column in target schema".to_string(),
78    })?;
79
80    Ok((timestamp_column, field_column, tag_columns))
81}
82
83/// Directly converts proto `Rows` into a `RecordBatch` aligned to the given
84/// `target_schema`, handling Prometheus column renaming (timestamp/value),
85/// reordering, type casting, and null-filling in a single pass.
86pub(crate) fn rows_to_aligned_record_batch(
87    rows: &Rows,
88    target_schema: &ArrowSchema,
89) -> Result<RecordBatchWithTsIdx> {
90    let row_count = rows.rows.len();
91    let column_count = rows.schema.len();
92
93    for (idx, row) in rows.rows.iter().enumerate() {
94        ensure!(
95            row.values.len() == column_count,
96            error::InternalSnafu {
97                err_msg: format!(
98                    "Column count mismatch in row {}, expected {}, got {}",
99                    idx,
100                    column_count,
101                    row.values.len()
102                )
103            }
104        );
105    }
106
107    let (target_ts_name, target_field_name, _target_tags) =
108        unzip_logical_region_schema(target_schema)?;
109    let timestamp_index = target_schema
110        .column_with_name(&target_ts_name)
111        .map(|(index, _)| index)
112        .with_context(|| error::UnexpectedResultSnafu {
113            reason: format!(
114                "Failed to resolve timestamp column '{}' in target schema",
115                target_ts_name
116            ),
117        })?;
118
119    // Map effective target column name → (source column index, source arrow type).
120    // Handles prom renames: Timestamp → target ts name, Float64 → target field name.
121    let mut source_map: HashMap<&str, (usize, ArrowDataType)> =
122        HashMap::with_capacity(rows.schema.len());
123
124    for (src_idx, col) in rows.schema.iter().enumerate() {
125        let wrapper = ColumnDataTypeWrapper::try_new(col.datatype, col.datatype_extension.clone())?;
126        let src_arrow_type = ConcreteDataType::from(wrapper).as_arrow_type();
127
128        match &src_arrow_type {
129            ArrowDataType::Float64 => {
130                source_map.insert(&target_field_name, (src_idx, src_arrow_type));
131            }
132            ArrowDataType::Timestamp(unit, _) => {
133                ensure!(
134                    unit == &TimeUnit::Millisecond,
135                    error::InvalidPromRemoteRequestSnafu {
136                        msg: format!(
137                            "Unexpected remote write batch timestamp unit, expect millisecond, got: {}",
138                            unit
139                        )
140                    }
141                );
142                source_map.insert(&target_ts_name, (src_idx, src_arrow_type));
143            }
144            ArrowDataType::Utf8 => {
145                source_map.insert(&col.column_name, (src_idx, src_arrow_type));
146            }
147            other => {
148                return error::InvalidPromRemoteRequestSnafu {
149                    msg: format!(
150                        "Unexpected remote write batch field type {}, field name: {}",
151                        other, col.column_name
152                    ),
153                }
154                .fail();
155            }
156        }
157    }
158
159    // Build columns in target schema order
160    let mut columns = Vec::with_capacity(target_schema.fields().len());
161    for target_field in target_schema.fields() {
162        if let Some((src_idx, src_arrow_type)) = source_map.get(target_field.name().as_str()) {
163            let array = build_arrow_array(
164                rows,
165                *src_idx,
166                &rows.schema[*src_idx].column_name,
167                src_arrow_type.clone(),
168                row_count,
169            )?;
170            columns.push(array);
171        } else {
172            columns.push(new_null_array(target_field.data_type(), row_count));
173        }
174    }
175
176    let batch = RecordBatch::try_new(Arc::new(target_schema.clone()), columns)
177        .context(error::ArrowSnafu)?;
178    RecordBatchWithTsIdx::try_new(batch, timestamp_index)
179}
180
181/// Identify tag columns in the proto `rows_schema` that are absent from the
182/// target region schema, without building an intermediate `RecordBatch`.
183pub(crate) fn identify_missing_columns_from_proto(
184    rows_schema: &[ColumnSchema],
185    target_schema: &ArrowSchema,
186) -> Result<Vec<String>> {
187    let (_, _, target_tags) = unzip_logical_region_schema(target_schema)?;
188    let mut missing = Vec::new();
189    for col in rows_schema {
190        let wrapper = ColumnDataTypeWrapper::try_new(col.datatype, col.datatype_extension.clone())?;
191        let arrow_type = ConcreteDataType::from(wrapper).as_arrow_type();
192        if matches!(arrow_type, ArrowDataType::Utf8)
193            && !target_tags.contains(&col.column_name)
194            && target_schema.column_with_name(&col.column_name).is_none()
195        {
196            missing.push(col.column_name.clone());
197        }
198    }
199    Ok(missing)
200}
201
202/// Build a `Vec<ColumnSchema>` suitable for creating a new Prometheus logical table
203/// directly from the proto `rows.schema`, avoiding the round-trip through Arrow schema.
204pub fn build_prom_create_table_schema_from_proto(
205    rows_schema: &[ColumnSchema],
206) -> Result<Vec<ColumnSchema>> {
207    rows_schema
208        .iter()
209        .map(|col| {
210            let semantic_type = if col.datatype == api::v1::ColumnDataType::TimestampMillisecond as i32 {
211                SemanticType::Timestamp
212            } else if col.datatype == api::v1::ColumnDataType::Float64 as i32 {
213                SemanticType::Field
214            } else {
215                // tag columns must be String type
216                ensure!(col.datatype == api::v1::ColumnDataType::String as i32, error::InvalidPromRemoteRequestSnafu{
217                                        msg: format!(
218                        "Failed to build create table schema, tag column '{}' must be String but got datatype {}",
219                        col.column_name, col.datatype
220                    )
221                });
222                SemanticType::Tag
223            };
224
225            Ok(ColumnSchema {
226                column_name: col.column_name.clone(),
227                datatype: col.datatype,
228                semantic_type: semantic_type as i32,
229                datatype_extension: col.datatype_extension.clone(),
230                options: None,
231            })
232        })
233        .collect()
234}
235
236/// Build a single Arrow array for the given column index from proto `Rows`.
237fn build_arrow_array(
238    rows: &Rows,
239    col_idx: usize,
240    column_name: &String,
241    column_data_type: arrow::datatypes::DataType,
242    row_count: usize,
243) -> Result<ArrayRef> {
244    macro_rules! build_array {
245        ($builder:expr, $( $pattern:pat => $value:expr ),+ $(,)?) => {{
246            let mut builder = $builder;
247            for row in &rows.rows {
248                match row.values[col_idx].value_data.as_ref() {
249                    $(Some($pattern) => builder.append_value($value),)+
250                    Some(v) => {
251                        return error::InvalidPromRemoteRequestSnafu {
252                            msg: format!("Unexpected value: {:?}", v),
253                        }
254                        .fail();
255                    }
256                    None => builder.append_null(),
257                }
258            }
259            Arc::new(builder.finish()) as ArrayRef
260        }};
261    }
262
263    let array: ArrayRef = match column_data_type {
264        arrow::datatypes::DataType::Float64 => {
265            build_array!(Float64Builder::with_capacity(row_count), ValueData::F64Value(v) => *v)
266        }
267        arrow::datatypes::DataType::Utf8 => build_array!(
268            StringBuilder::with_capacity(row_count, 0),
269            ValueData::StringValue(v) => v
270        ),
271        arrow::datatypes::DataType::Timestamp(u, _) => match u {
272            TimeUnit::Second => build_array!(
273                TimestampSecondBuilder::with_capacity(row_count),
274                ValueData::TimestampSecondValue(v) => *v
275            ),
276            TimeUnit::Millisecond => build_array!(
277                TimestampMillisecondBuilder::with_capacity(row_count),
278                ValueData::TimestampMillisecondValue(v) => *v
279            ),
280            TimeUnit::Microsecond => build_array!(
281                TimestampMicrosecondBuilder::with_capacity(row_count),
282                ValueData::DatetimeValue(v) => *v,
283                ValueData::TimestampMicrosecondValue(v) => *v
284            ),
285            TimeUnit::Nanosecond => build_array!(
286                TimestampNanosecondBuilder::with_capacity(row_count),
287                ValueData::TimestampNanosecondValue(v) => *v
288            ),
289        },
290        ty => {
291            return error::InvalidPromRemoteRequestSnafu {
292                msg: format!(
293                    "Unexpected column type {:?}, column name: {}",
294                    ty, column_name
295                ),
296            }
297            .fail();
298        }
299    };
300
301    Ok(array)
302}
303
304#[cfg(test)]
305mod tests {
306    use api::v1::value::ValueData;
307    use api::v1::{ColumnDataType, ColumnSchema, Row, Rows, SemanticType, Value};
308    use arrow::array::{Array, Float64Array, StringArray, TimestampMillisecondArray};
309    use arrow::datatypes::{DataType, Field, Schema as ArrowSchema, TimeUnit};
310
311    use super::{
312        build_prom_create_table_schema_from_proto, identify_missing_columns_from_proto,
313        rows_to_aligned_record_batch,
314    };
315
316    #[test]
317    fn test_rows_to_aligned_record_batch_renames_and_reorders() {
318        let rows = Rows {
319            schema: vec![
320                ColumnSchema {
321                    column_name: "greptime_timestamp".to_string(),
322                    datatype: ColumnDataType::TimestampMillisecond as i32,
323                    semantic_type: SemanticType::Timestamp as i32,
324                    ..Default::default()
325                },
326                ColumnSchema {
327                    column_name: "host".to_string(),
328                    datatype: ColumnDataType::String as i32,
329                    semantic_type: SemanticType::Tag as i32,
330                    ..Default::default()
331                },
332                ColumnSchema {
333                    column_name: "greptime_value".to_string(),
334                    datatype: ColumnDataType::Float64 as i32,
335                    semantic_type: SemanticType::Field as i32,
336                    ..Default::default()
337                },
338            ],
339            rows: vec![
340                Row {
341                    values: vec![
342                        Value {
343                            value_data: Some(ValueData::TimestampMillisecondValue(1000)),
344                        },
345                        Value {
346                            value_data: Some(ValueData::StringValue("h1".to_string())),
347                        },
348                        Value {
349                            value_data: Some(ValueData::F64Value(42.0)),
350                        },
351                    ],
352                },
353                Row {
354                    values: vec![
355                        Value {
356                            value_data: Some(ValueData::TimestampMillisecondValue(2000)),
357                        },
358                        Value {
359                            value_data: Some(ValueData::StringValue("h2".to_string())),
360                        },
361                        Value {
362                            value_data: Some(ValueData::F64Value(99.0)),
363                        },
364                    ],
365                },
366            ],
367        };
368
369        // Target schema has renamed columns and different ordering.
370        let target = ArrowSchema::new(vec![
371            Field::new(
372                "my_ts",
373                DataType::Timestamp(TimeUnit::Millisecond, None),
374                false,
375            ),
376            Field::new("host", DataType::Utf8, true),
377            Field::new("my_value", DataType::Float64, true),
378        ]);
379
380        let aligned_batch = rows_to_aligned_record_batch(&rows, &target).unwrap();
381        let (batch, timestamp_index) = aligned_batch.into_parts();
382        assert_eq!(0, timestamp_index);
383        assert_eq!(batch.schema().as_ref(), &target);
384        assert_eq!(2, batch.num_rows());
385        assert_eq!(3, batch.num_columns());
386
387        let ts = batch
388            .column(0)
389            .as_any()
390            .downcast_ref::<TimestampMillisecondArray>()
391            .unwrap();
392        assert_eq!(ts.value(0), 1000);
393        assert_eq!(ts.value(1), 2000);
394
395        let hosts = batch
396            .column(1)
397            .as_any()
398            .downcast_ref::<StringArray>()
399            .unwrap();
400        assert_eq!(hosts.value(0), "h1");
401        assert_eq!(hosts.value(1), "h2");
402
403        let values = batch
404            .column(2)
405            .as_any()
406            .downcast_ref::<Float64Array>()
407            .unwrap();
408        assert_eq!(values.value(0), 42.0);
409        assert_eq!(values.value(1), 99.0);
410    }
411
412    #[test]
413    fn test_rows_to_aligned_record_batch_fills_nulls() {
414        let rows = Rows {
415            schema: vec![
416                ColumnSchema {
417                    column_name: "greptime_timestamp".to_string(),
418                    datatype: ColumnDataType::TimestampMillisecond as i32,
419                    semantic_type: SemanticType::Timestamp as i32,
420                    ..Default::default()
421                },
422                ColumnSchema {
423                    column_name: "host".to_string(),
424                    datatype: ColumnDataType::String as i32,
425                    semantic_type: SemanticType::Tag as i32,
426                    ..Default::default()
427                },
428                ColumnSchema {
429                    column_name: "instance".to_string(),
430                    datatype: ColumnDataType::String as i32,
431                    semantic_type: SemanticType::Tag as i32,
432                    ..Default::default()
433                },
434                ColumnSchema {
435                    column_name: "greptime_value".to_string(),
436                    datatype: ColumnDataType::Float64 as i32,
437                    semantic_type: SemanticType::Field as i32,
438                    ..Default::default()
439                },
440            ],
441            rows: vec![Row {
442                values: vec![
443                    Value {
444                        value_data: Some(ValueData::TimestampMillisecondValue(1000)),
445                    },
446                    Value {
447                        value_data: Some(ValueData::StringValue("h1".to_string())),
448                    },
449                    Value {
450                        value_data: Some(ValueData::StringValue("i1".to_string())),
451                    },
452                    Value {
453                        value_data: Some(ValueData::F64Value(1.0)),
454                    },
455                ],
456            }],
457        };
458
459        // Target schema has "host" but not "instance"; also has "region" which is missing from source.
460        let target = ArrowSchema::new(vec![
461            Field::new(
462                "my_ts",
463                DataType::Timestamp(TimeUnit::Millisecond, None),
464                false,
465            ),
466            Field::new("host", DataType::Utf8, true),
467            Field::new("region", DataType::Utf8, true),
468            Field::new("my_value", DataType::Float64, true),
469        ]);
470
471        let aligned_batch = rows_to_aligned_record_batch(&rows, &target).unwrap();
472        let (batch, timestamp_index) = aligned_batch.into_parts();
473        assert_eq!(0, timestamp_index);
474        assert_eq!(batch.schema().as_ref(), &target);
475        assert_eq!(1, batch.num_rows());
476        assert_eq!(4, batch.num_columns());
477
478        // "region" column should be null-filled.
479        let region = batch
480            .column(2)
481            .as_any()
482            .downcast_ref::<StringArray>()
483            .unwrap();
484        assert!(region.is_null(0));
485    }
486
487    #[test]
488    fn test_identify_missing_columns_from_proto() {
489        let rows_schema = vec![
490            ColumnSchema {
491                column_name: "greptime_timestamp".to_string(),
492                datatype: ColumnDataType::TimestampMillisecond as i32,
493                semantic_type: SemanticType::Timestamp as i32,
494                ..Default::default()
495            },
496            ColumnSchema {
497                column_name: "host".to_string(),
498                datatype: ColumnDataType::String as i32,
499                semantic_type: SemanticType::Tag as i32,
500                ..Default::default()
501            },
502            ColumnSchema {
503                column_name: "instance".to_string(),
504                datatype: ColumnDataType::String as i32,
505                semantic_type: SemanticType::Tag as i32,
506                ..Default::default()
507            },
508            ColumnSchema {
509                column_name: "greptime_value".to_string(),
510                datatype: ColumnDataType::Float64 as i32,
511                semantic_type: SemanticType::Field as i32,
512                ..Default::default()
513            },
514        ];
515
516        let target = ArrowSchema::new(vec![
517            Field::new(
518                "my_ts",
519                DataType::Timestamp(TimeUnit::Millisecond, None),
520                false,
521            ),
522            Field::new("host", DataType::Utf8, true),
523            Field::new("my_value", DataType::Float64, true),
524        ]);
525
526        let missing = identify_missing_columns_from_proto(&rows_schema, &target).unwrap();
527        assert_eq!(missing, vec!["instance".to_string()]);
528    }
529
530    #[test]
531    fn test_build_prom_create_table_schema_from_proto() {
532        let rows_schema = vec![
533            ColumnSchema {
534                column_name: "greptime_timestamp".to_string(),
535                datatype: ColumnDataType::TimestampMillisecond as i32,
536                semantic_type: SemanticType::Timestamp as i32,
537                ..Default::default()
538            },
539            ColumnSchema {
540                column_name: "job".to_string(),
541                datatype: ColumnDataType::String as i32,
542                semantic_type: SemanticType::Tag as i32,
543                ..Default::default()
544            },
545            ColumnSchema {
546                column_name: "greptime_value".to_string(),
547                datatype: ColumnDataType::Float64 as i32,
548                semantic_type: SemanticType::Field as i32,
549                ..Default::default()
550            },
551        ];
552
553        let schema = build_prom_create_table_schema_from_proto(&rows_schema).unwrap();
554        assert_eq!(3, schema.len());
555
556        assert_eq!("greptime_timestamp", schema[0].column_name);
557        assert_eq!(SemanticType::Timestamp as i32, schema[0].semantic_type);
558        assert_eq!(
559            ColumnDataType::TimestampMillisecond as i32,
560            schema[0].datatype
561        );
562
563        assert_eq!("job", schema[1].column_name);
564        assert_eq!(SemanticType::Tag as i32, schema[1].semantic_type);
565        assert_eq!(ColumnDataType::String as i32, schema[1].datatype);
566
567        assert_eq!("greptime_value", schema[2].column_name);
568        assert_eq!(SemanticType::Field as i32, schema[2].semantic_type);
569        assert_eq!(ColumnDataType::Float64 as i32, schema[2].datatype);
570    }
571}