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, ArrowPrimitiveType, Float64Builder, PrimitiveBuilder, StringBuilder, new_null_array,
27};
28use arrow::datatypes::{
29    DataType as ArrowDataType, Schema as ArrowSchema, TimestampMicrosecondType,
30    TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType,
31};
32use arrow::record_batch::RecordBatch;
33use arrow_schema::TimeUnit;
34use common_query::prelude::{greptime_timestamp, greptime_value};
35use common_time::Timestamp;
36use common_time::timestamp::TimeUnit as CommonTimeUnit;
37use datatypes::data_type::DataType;
38use datatypes::prelude::ConcreteDataType;
39use snafu::{OptionExt, ResultExt, ensure};
40
41use crate::batcher::logical_table::RecordBatchWithTsIdx;
42use crate::error;
43use crate::error::Result;
44
45/// Extract timestamp, field, and tag column names from a logical region schema.
46fn unzip_logical_region_schema(
47    target_schema: &ArrowSchema,
48) -> Result<(String, String, HashSet<String>)> {
49    let mut timestamp_column = None;
50    let mut field_column = None;
51    let mut tag_columns = HashSet::with_capacity(target_schema.fields.len().saturating_sub(2));
52    for field in target_schema.fields() {
53        if field.name() == greptime_timestamp() {
54            timestamp_column = Some(field.name().clone());
55            continue;
56        }
57
58        if field.name() == greptime_value() {
59            field_column = Some(field.name().clone());
60            continue;
61        }
62
63        if timestamp_column.is_none() && matches!(field.data_type(), ArrowDataType::Timestamp(_, _))
64        {
65            timestamp_column = Some(field.name().clone());
66            continue;
67        }
68
69        if field_column.is_none() && matches!(field.data_type(), ArrowDataType::Float64) {
70            field_column = Some(field.name().clone());
71            continue;
72        }
73        tag_columns.insert(field.name().clone());
74    }
75
76    let timestamp_column = timestamp_column.with_context(|| error::UnexpectedResultSnafu {
77        reason: "Failed to locate timestamp column in target schema".to_string(),
78    })?;
79    let field_column = field_column.with_context(|| error::UnexpectedResultSnafu {
80        reason: "Failed to locate field column in target schema".to_string(),
81    })?;
82
83    Ok((timestamp_column, field_column, tag_columns))
84}
85
86/// Directly converts proto `Rows` into a `RecordBatch` aligned to the given
87/// `target_schema`, handling Prometheus column renaming (timestamp/value),
88/// reordering, type casting, and null-filling in a single pass.
89pub(crate) fn rows_to_aligned_record_batch(
90    rows: &Rows,
91    target_schema: &ArrowSchema,
92) -> Result<RecordBatchWithTsIdx> {
93    let row_count = rows.rows.len();
94    let column_count = rows.schema.len();
95
96    for (idx, row) in rows.rows.iter().enumerate() {
97        ensure!(
98            row.values.len() == column_count,
99            error::InternalSnafu {
100                err_msg: format!(
101                    "Column count mismatch in row {}, expected {}, got {}",
102                    idx,
103                    column_count,
104                    row.values.len()
105                )
106            }
107        );
108    }
109
110    let (target_ts_name, target_field_name, _target_tags) =
111        unzip_logical_region_schema(target_schema)?;
112    let timestamp_index = target_schema
113        .column_with_name(&target_ts_name)
114        .map(|(index, _)| index)
115        .with_context(|| error::UnexpectedResultSnafu {
116            reason: format!(
117                "Failed to resolve timestamp column '{}' in target schema",
118                target_ts_name
119            ),
120        })?;
121
122    // Map effective target column name → (source column index, source arrow type).
123    // Handles prom renames: Timestamp → target ts name, Float64 → target field name.
124    let mut source_map: HashMap<&str, (usize, ArrowDataType)> =
125        HashMap::with_capacity(rows.schema.len());
126
127    for (src_idx, col) in rows.schema.iter().enumerate() {
128        let wrapper = ColumnDataTypeWrapper::try_new(col.datatype, col.datatype_extension.clone())?;
129        let src_arrow_type = ConcreteDataType::from(wrapper).as_arrow_type();
130
131        match &src_arrow_type {
132            ArrowDataType::Float64 => {
133                source_map.insert(&target_field_name, (src_idx, src_arrow_type));
134            }
135            ArrowDataType::Timestamp(_, _) => {
136                source_map.insert(&target_ts_name, (src_idx, src_arrow_type));
137            }
138            ArrowDataType::Utf8 => {
139                source_map.insert(&col.column_name, (src_idx, src_arrow_type));
140            }
141            other => {
142                return error::InvalidPromRemoteRequestSnafu {
143                    msg: format!(
144                        "Unexpected remote write batch field type {}, field name: {}",
145                        other, col.column_name
146                    ),
147                }
148                .fail();
149            }
150        }
151    }
152
153    // Build columns in target schema order. The timestamp column is built in
154    // the TARGET schema's unit; `build_arrow_array` converts the request's
155    // encoding unit (flooring on narrowing) into it.
156    let mut columns = Vec::with_capacity(target_schema.fields().len());
157    for target_field in target_schema.fields() {
158        if let Some((src_idx, src_arrow_type)) = source_map.get(target_field.name().as_str()) {
159            let target_type = if matches!(
160                (src_arrow_type, target_field.data_type()),
161                (
162                    ArrowDataType::Timestamp(_, _),
163                    ArrowDataType::Timestamp(_, _)
164                )
165            ) {
166                target_field.data_type().clone()
167            } else {
168                src_arrow_type.clone()
169            };
170            let array = build_arrow_array(
171                rows,
172                *src_idx,
173                &rows.schema[*src_idx].column_name,
174                target_type,
175                row_count,
176            )?;
177            columns.push(array);
178        } else {
179            columns.push(new_null_array(target_field.data_type(), row_count));
180        }
181    }
182
183    let batch = RecordBatch::try_new(Arc::new(target_schema.clone()), columns)
184        .context(error::ArrowSnafu)?;
185    RecordBatchWithTsIdx::try_new(batch, timestamp_index)
186}
187
188/// Identify tag columns in the proto `rows_schema` that are absent from the
189/// target region schema, without building an intermediate `RecordBatch`.
190pub(crate) fn identify_missing_columns_from_proto(
191    rows_schema: &[ColumnSchema],
192    target_schema: &ArrowSchema,
193) -> Result<Vec<String>> {
194    let (_, _, target_tags) = unzip_logical_region_schema(target_schema)?;
195    let mut missing = Vec::new();
196    for col in rows_schema {
197        let wrapper = ColumnDataTypeWrapper::try_new(col.datatype, col.datatype_extension.clone())?;
198        let arrow_type = ConcreteDataType::from(wrapper).as_arrow_type();
199        if matches!(arrow_type, ArrowDataType::Utf8)
200            && !target_tags.contains(&col.column_name)
201            && target_schema.column_with_name(&col.column_name).is_none()
202        {
203            missing.push(col.column_name.clone());
204        }
205    }
206    Ok(missing)
207}
208
209/// Builds a timestamp array of `T`'s unit from a proto column, appending
210/// each value directly into the builder. Values already in the target unit
211/// are appended without conversion (the unchanged-unit fast path); others
212/// are converted via `Timestamp::convert_to`, flooring on narrowing.
213fn build_timestamp_array<T: ArrowPrimitiveType<Native = i64>>(
214    rows: &Rows,
215    col_idx: usize,
216    column_name: &str,
217    row_count: usize,
218    target_unit: CommonTimeUnit,
219) -> Result<ArrayRef> {
220    let mut builder = PrimitiveBuilder::<T>::with_capacity(row_count);
221    for row in &rows.rows {
222        let Some(value) = row.values[col_idx].value_data.as_ref() else {
223            builder.append_null();
224            continue;
225        };
226        let (source_unit, raw) = match value {
227            ValueData::TimestampSecondValue(v) => (CommonTimeUnit::Second, *v),
228            ValueData::TimestampMillisecondValue(v) => (CommonTimeUnit::Millisecond, *v),
229            ValueData::DatetimeValue(v) | ValueData::TimestampMicrosecondValue(v) => {
230                (CommonTimeUnit::Microsecond, *v)
231            }
232            ValueData::TimestampNanosecondValue(v) => (CommonTimeUnit::Nanosecond, *v),
233            v => {
234                return error::InvalidPromRemoteRequestSnafu {
235                    msg: format!("Unexpected value: {:?}", v),
236                }
237                .fail();
238            }
239        };
240        if source_unit == target_unit {
241            builder.append_value(raw);
242        } else {
243            let timestamp = Timestamp::new(raw, source_unit);
244            let Some(converted) = timestamp.convert_to(target_unit) else {
245                return error::InvalidPromRemoteRequestSnafu {
246                    msg: format!(
247                        "Timestamp value in column '{column_name}' overflows when converting to unit {target_unit:?}"
248                    ),
249                }
250                .fail();
251            };
252            builder.append_value(converted.value());
253        }
254    }
255    Ok(Arc::new(builder.finish()) as ArrayRef)
256}
257
258/// Converts an arrow time unit to the common time unit.
259fn arrow_time_unit(unit: TimeUnit) -> CommonTimeUnit {
260    match unit {
261        TimeUnit::Second => CommonTimeUnit::Second,
262        TimeUnit::Millisecond => CommonTimeUnit::Millisecond,
263        TimeUnit::Microsecond => CommonTimeUnit::Microsecond,
264        TimeUnit::Nanosecond => CommonTimeUnit::Nanosecond,
265    }
266}
267
268/// Build a `Vec<ColumnSchema>` suitable for creating a new Prometheus logical table
269/// directly from the proto `rows.schema`, avoiding the round-trip through Arrow schema.
270pub fn build_prom_create_table_schema_from_proto(
271    rows_schema: &[ColumnSchema],
272) -> Result<Vec<ColumnSchema>> {
273    rows_schema
274        .iter()
275        .map(|col| {
276            let datatype = api::v1::ColumnDataType::try_from(col.datatype).map_err(|_| {
277                error::InvalidPromRemoteRequestSnafu {
278                    msg: format!(
279                        "Failed to build create table schema, column '{}' has unknown datatype {}",
280                        col.column_name, col.datatype
281                    ),
282                }
283                .build()
284            })?;
285            let semantic_type = if api::helper::timestamp_unit(datatype).is_some() {
286                SemanticType::Timestamp
287            } else if datatype == api::v1::ColumnDataType::Float64 {
288                SemanticType::Field
289            } else {
290                // tag columns must be String type
291                ensure!(datatype == api::v1::ColumnDataType::String, error::InvalidPromRemoteRequestSnafu{
292                                        msg: format!(
293                        "Failed to build create table schema, tag column '{}' must be String but got datatype {}",
294                        col.column_name, col.datatype
295                    )
296                });
297                SemanticType::Tag
298            };
299
300            Ok(ColumnSchema {
301                column_name: col.column_name.clone(),
302                datatype: col.datatype,
303                semantic_type: semantic_type as i32,
304                datatype_extension: col.datatype_extension.clone(),
305                options: None,
306            })
307        })
308        .collect()
309}
310
311/// Build a single Arrow array for the given column index from proto `Rows`.
312fn build_arrow_array(
313    rows: &Rows,
314    col_idx: usize,
315    column_name: &String,
316    column_data_type: arrow::datatypes::DataType,
317    row_count: usize,
318) -> Result<ArrayRef> {
319    macro_rules! build_array {
320        ($builder:expr, $( $pattern:pat => $value:expr ),+ $(,)?) => {{
321            let mut builder = $builder;
322            for row in &rows.rows {
323                match row.values[col_idx].value_data.as_ref() {
324                    $(Some($pattern) => builder.append_value($value),)+
325                    Some(v) => {
326                        return error::InvalidPromRemoteRequestSnafu {
327                            msg: format!("Unexpected value: {:?}", v),
328                        }
329                        .fail();
330                    }
331                    None => builder.append_null(),
332                }
333            }
334            Arc::new(builder.finish()) as ArrayRef
335        }};
336    }
337
338    let array: ArrayRef = match column_data_type {
339        arrow::datatypes::DataType::Float64 => {
340            build_array!(Float64Builder::with_capacity(row_count), ValueData::F64Value(v) => *v)
341        }
342        arrow::datatypes::DataType::Utf8 => build_array!(
343            StringBuilder::with_capacity(row_count, 0),
344            ValueData::StringValue(v) => v
345        ),
346        arrow::datatypes::DataType::Timestamp(u, _) => {
347            // Accept any timestamp encoding and append directly into the
348            // target-unit builder — no intermediate column allocation.
349            // Values already in the target unit (the common unchanged
350            // millisecond case) are appended as-is; others are converted
351            // first, flooring on narrowing (same semantics as
352            // `Timestamp::convert_to` on the ordinary insert path).
353            let target_unit = arrow_time_unit(u);
354            match u {
355                TimeUnit::Second => build_timestamp_array::<TimestampSecondType>(
356                    rows,
357                    col_idx,
358                    column_name,
359                    row_count,
360                    target_unit,
361                )?,
362                TimeUnit::Millisecond => build_timestamp_array::<TimestampMillisecondType>(
363                    rows,
364                    col_idx,
365                    column_name,
366                    row_count,
367                    target_unit,
368                )?,
369                TimeUnit::Microsecond => build_timestamp_array::<TimestampMicrosecondType>(
370                    rows,
371                    col_idx,
372                    column_name,
373                    row_count,
374                    target_unit,
375                )?,
376                TimeUnit::Nanosecond => build_timestamp_array::<TimestampNanosecondType>(
377                    rows,
378                    col_idx,
379                    column_name,
380                    row_count,
381                    target_unit,
382                )?,
383            }
384        }
385        ty => {
386            return error::InvalidPromRemoteRequestSnafu {
387                msg: format!(
388                    "Unexpected column type {:?}, column name: {}",
389                    ty, column_name
390                ),
391            }
392            .fail();
393        }
394    };
395
396    Ok(array)
397}
398
399#[cfg(test)]
400mod tests {
401    use api::v1::value::ValueData;
402    use api::v1::{ColumnDataType, ColumnSchema, Row, Rows, SemanticType, Value};
403    use arrow::array::{
404        Array, Float64Array, StringArray, TimestampMicrosecondArray, TimestampMillisecondArray,
405    };
406    use arrow::datatypes::{DataType, Field, Schema as ArrowSchema, TimeUnit};
407
408    use super::{
409        build_prom_create_table_schema_from_proto, identify_missing_columns_from_proto,
410        rows_to_aligned_record_batch,
411    };
412
413    #[test]
414    fn test_rows_to_aligned_record_batch_converts_time_units() {
415        // Microsecond-encoded rows aligned to a millisecond schema: narrowing
416        // floors towards negative infinity (-1001us -> -2ms), matching
417        // `Timestamp::convert_to` on the ordinary insert path.
418        let rows = Rows {
419            schema: vec![
420                ColumnSchema {
421                    column_name: "greptime_timestamp".to_string(),
422                    datatype: ColumnDataType::TimestampMicrosecond as i32,
423                    semantic_type: SemanticType::Timestamp as i32,
424                    ..Default::default()
425                },
426                ColumnSchema {
427                    column_name: "greptime_value".to_string(),
428                    datatype: ColumnDataType::Float64 as i32,
429                    semantic_type: SemanticType::Field as i32,
430                    ..Default::default()
431                },
432            ],
433            rows: vec![
434                Row {
435                    values: vec![
436                        Value {
437                            value_data: Some(ValueData::TimestampMicrosecondValue(1000)),
438                        },
439                        Value {
440                            value_data: Some(ValueData::F64Value(1.0)),
441                        },
442                    ],
443                },
444                Row {
445                    values: vec![
446                        Value {
447                            value_data: Some(ValueData::TimestampMicrosecondValue(-1001)),
448                        },
449                        Value {
450                            value_data: Some(ValueData::F64Value(2.0)),
451                        },
452                    ],
453                },
454            ],
455        };
456        let target = ArrowSchema::new(vec![
457            Field::new(
458                "greptime_timestamp",
459                DataType::Timestamp(TimeUnit::Millisecond, None),
460                false,
461            ),
462            Field::new("greptime_value", DataType::Float64, true),
463        ]);
464
465        let (batch, _) = rows_to_aligned_record_batch(&rows, &target)
466            .unwrap()
467            .into_parts();
468        let ts = batch
469            .column(0)
470            .as_any()
471            .downcast_ref::<TimestampMillisecondArray>()
472            .unwrap();
473        assert_eq!(ts.value(0), 1);
474        assert_eq!(ts.value(1), -2);
475
476        // Millisecond-encoded rows aligned to a microsecond schema: widening
477        // is lossless.
478        let mut rows = Rows {
479            schema: vec![rows.schema[0].clone(), rows.schema[1].clone()],
480            rows: vec![Row {
481                values: vec![
482                    Value {
483                        value_data: Some(ValueData::TimestampMillisecondValue(123)),
484                    },
485                    Value {
486                        value_data: Some(ValueData::F64Value(4.0)),
487                    },
488                ],
489            }],
490        };
491        rows.schema[0].datatype = ColumnDataType::TimestampMillisecond as i32;
492        let target = ArrowSchema::new(vec![
493            Field::new(
494                "greptime_timestamp",
495                DataType::Timestamp(TimeUnit::Microsecond, None),
496                false,
497            ),
498            Field::new("greptime_value", DataType::Float64, true),
499        ]);
500
501        let (batch, _) = rows_to_aligned_record_batch(&rows, &target)
502            .unwrap()
503            .into_parts();
504        let ts = batch
505            .column(0)
506            .as_any()
507            .downcast_ref::<TimestampMicrosecondArray>()
508            .unwrap();
509        assert_eq!(ts.value(0), 123_000);
510    }
511
512    #[test]
513    fn test_rows_to_aligned_record_batch_renames_and_reorders() {
514        let rows = Rows {
515            schema: vec![
516                ColumnSchema {
517                    column_name: "greptime_timestamp".to_string(),
518                    datatype: ColumnDataType::TimestampMillisecond as i32,
519                    semantic_type: SemanticType::Timestamp as i32,
520                    ..Default::default()
521                },
522                ColumnSchema {
523                    column_name: "host".to_string(),
524                    datatype: ColumnDataType::String as i32,
525                    semantic_type: SemanticType::Tag as i32,
526                    ..Default::default()
527                },
528                ColumnSchema {
529                    column_name: "greptime_value".to_string(),
530                    datatype: ColumnDataType::Float64 as i32,
531                    semantic_type: SemanticType::Field as i32,
532                    ..Default::default()
533                },
534            ],
535            rows: vec![
536                Row {
537                    values: vec![
538                        Value {
539                            value_data: Some(ValueData::TimestampMillisecondValue(1000)),
540                        },
541                        Value {
542                            value_data: Some(ValueData::StringValue("h1".to_string())),
543                        },
544                        Value {
545                            value_data: Some(ValueData::F64Value(42.0)),
546                        },
547                    ],
548                },
549                Row {
550                    values: vec![
551                        Value {
552                            value_data: Some(ValueData::TimestampMillisecondValue(2000)),
553                        },
554                        Value {
555                            value_data: Some(ValueData::StringValue("h2".to_string())),
556                        },
557                        Value {
558                            value_data: Some(ValueData::F64Value(99.0)),
559                        },
560                    ],
561                },
562            ],
563        };
564
565        // Target schema has renamed columns and different ordering.
566        let target = ArrowSchema::new(vec![
567            Field::new(
568                "my_ts",
569                DataType::Timestamp(TimeUnit::Millisecond, None),
570                false,
571            ),
572            Field::new("host", DataType::Utf8, true),
573            Field::new("my_value", DataType::Float64, true),
574        ]);
575
576        let aligned_batch = rows_to_aligned_record_batch(&rows, &target).unwrap();
577        let (batch, timestamp_index) = aligned_batch.into_parts();
578        assert_eq!(0, timestamp_index);
579        assert_eq!(batch.schema().as_ref(), &target);
580        assert_eq!(2, batch.num_rows());
581        assert_eq!(3, batch.num_columns());
582
583        let ts = batch
584            .column(0)
585            .as_any()
586            .downcast_ref::<TimestampMillisecondArray>()
587            .unwrap();
588        assert_eq!(ts.value(0), 1000);
589        assert_eq!(ts.value(1), 2000);
590
591        let hosts = batch
592            .column(1)
593            .as_any()
594            .downcast_ref::<StringArray>()
595            .unwrap();
596        assert_eq!(hosts.value(0), "h1");
597        assert_eq!(hosts.value(1), "h2");
598
599        let values = batch
600            .column(2)
601            .as_any()
602            .downcast_ref::<Float64Array>()
603            .unwrap();
604        assert_eq!(values.value(0), 42.0);
605        assert_eq!(values.value(1), 99.0);
606    }
607
608    #[test]
609    fn test_rows_to_aligned_record_batch_fills_nulls() {
610        let rows = Rows {
611            schema: vec![
612                ColumnSchema {
613                    column_name: "greptime_timestamp".to_string(),
614                    datatype: ColumnDataType::TimestampMillisecond as i32,
615                    semantic_type: SemanticType::Timestamp as i32,
616                    ..Default::default()
617                },
618                ColumnSchema {
619                    column_name: "host".to_string(),
620                    datatype: ColumnDataType::String as i32,
621                    semantic_type: SemanticType::Tag as i32,
622                    ..Default::default()
623                },
624                ColumnSchema {
625                    column_name: "instance".to_string(),
626                    datatype: ColumnDataType::String as i32,
627                    semantic_type: SemanticType::Tag as i32,
628                    ..Default::default()
629                },
630                ColumnSchema {
631                    column_name: "greptime_value".to_string(),
632                    datatype: ColumnDataType::Float64 as i32,
633                    semantic_type: SemanticType::Field as i32,
634                    ..Default::default()
635                },
636            ],
637            rows: vec![Row {
638                values: vec![
639                    Value {
640                        value_data: Some(ValueData::TimestampMillisecondValue(1000)),
641                    },
642                    Value {
643                        value_data: Some(ValueData::StringValue("h1".to_string())),
644                    },
645                    Value {
646                        value_data: Some(ValueData::StringValue("i1".to_string())),
647                    },
648                    Value {
649                        value_data: Some(ValueData::F64Value(1.0)),
650                    },
651                ],
652            }],
653        };
654
655        // Target schema has "host" but not "instance"; also has "region" which is missing from source.
656        let target = ArrowSchema::new(vec![
657            Field::new(
658                "my_ts",
659                DataType::Timestamp(TimeUnit::Millisecond, None),
660                false,
661            ),
662            Field::new("host", DataType::Utf8, true),
663            Field::new("region", DataType::Utf8, true),
664            Field::new("my_value", DataType::Float64, true),
665        ]);
666
667        let aligned_batch = rows_to_aligned_record_batch(&rows, &target).unwrap();
668        let (batch, timestamp_index) = aligned_batch.into_parts();
669        assert_eq!(0, timestamp_index);
670        assert_eq!(batch.schema().as_ref(), &target);
671        assert_eq!(1, batch.num_rows());
672        assert_eq!(4, batch.num_columns());
673
674        // "region" column should be null-filled.
675        let region = batch
676            .column(2)
677            .as_any()
678            .downcast_ref::<StringArray>()
679            .unwrap();
680        assert!(region.is_null(0));
681    }
682
683    #[test]
684    fn test_identify_missing_columns_from_proto() {
685        let rows_schema = vec![
686            ColumnSchema {
687                column_name: "greptime_timestamp".to_string(),
688                datatype: ColumnDataType::TimestampMillisecond as i32,
689                semantic_type: SemanticType::Timestamp as i32,
690                ..Default::default()
691            },
692            ColumnSchema {
693                column_name: "host".to_string(),
694                datatype: ColumnDataType::String as i32,
695                semantic_type: SemanticType::Tag as i32,
696                ..Default::default()
697            },
698            ColumnSchema {
699                column_name: "instance".to_string(),
700                datatype: ColumnDataType::String as i32,
701                semantic_type: SemanticType::Tag as i32,
702                ..Default::default()
703            },
704            ColumnSchema {
705                column_name: "greptime_value".to_string(),
706                datatype: ColumnDataType::Float64 as i32,
707                semantic_type: SemanticType::Field as i32,
708                ..Default::default()
709            },
710        ];
711
712        let target = ArrowSchema::new(vec![
713            Field::new(
714                "my_ts",
715                DataType::Timestamp(TimeUnit::Millisecond, None),
716                false,
717            ),
718            Field::new("host", DataType::Utf8, true),
719            Field::new("my_value", DataType::Float64, true),
720        ]);
721
722        let missing = identify_missing_columns_from_proto(&rows_schema, &target).unwrap();
723        assert_eq!(missing, vec!["instance".to_string()]);
724    }
725
726    #[test]
727    fn test_build_prom_create_table_schema_from_proto() {
728        let rows_schema = vec![
729            ColumnSchema {
730                column_name: "greptime_timestamp".to_string(),
731                datatype: ColumnDataType::TimestampMillisecond as i32,
732                semantic_type: SemanticType::Timestamp as i32,
733                ..Default::default()
734            },
735            ColumnSchema {
736                column_name: "job".to_string(),
737                datatype: ColumnDataType::String as i32,
738                semantic_type: SemanticType::Tag as i32,
739                ..Default::default()
740            },
741            ColumnSchema {
742                column_name: "greptime_value".to_string(),
743                datatype: ColumnDataType::Float64 as i32,
744                semantic_type: SemanticType::Field as i32,
745                ..Default::default()
746            },
747        ];
748
749        let schema = build_prom_create_table_schema_from_proto(&rows_schema).unwrap();
750        assert_eq!(3, schema.len());
751
752        assert_eq!("greptime_timestamp", schema[0].column_name);
753        assert_eq!(SemanticType::Timestamp as i32, schema[0].semantic_type);
754        assert_eq!(
755            ColumnDataType::TimestampMillisecond as i32,
756            schema[0].datatype
757        );
758
759        assert_eq!("job", schema[1].column_name);
760        assert_eq!(SemanticType::Tag as i32, schema[1].semantic_type);
761        assert_eq!(ColumnDataType::String as i32, schema[1].datatype);
762
763        assert_eq!("greptime_value", schema[2].column_name);
764        assert_eq!(SemanticType::Field as i32, schema[2].semantic_type);
765        assert_eq!(ColumnDataType::Float64 as i32, schema[2].datatype);
766    }
767}