1pub mod coerce;
16
17use std::borrow::Cow;
18use std::collections::{BTreeMap, HashSet};
19use std::sync::Arc;
20
21use ahash::{HashMap, HashMapExt};
22use api::helper::{ColumnDataTypeWrapper, encode_json_value};
23use api::v1::column_def::{collect_column_options, options_from_column_schema};
24use api::v1::value::ValueData;
25use api::v1::{ColumnDataType, SemanticType};
26use arrow_schema::extension::ExtensionType;
27use coerce::{coerce_columns, coerce_value};
28use common_query::prelude::{greptime_timestamp, greptime_value};
29use common_telemetry::warn;
30use datatypes::data_type::ConcreteDataType;
31use datatypes::extension::json::{Json2ExtensionType, parse_legacy_json2_settings};
32use datatypes::json::JsonSettings;
33use datatypes::value::Value;
34use greptime_proto::v1::{ColumnSchema, Row, Rows, Value as GreptimeValue};
35use itertools::Itertools;
36use jsonb::Number;
37use once_cell::sync::OnceCell;
38use serde_json as serde_json_crate;
39use session::context::Channel;
40use snafu::OptionExt;
41use table::Table;
42use vrl::prelude::{Bytes, VrlValueConvert};
43use vrl::value::value::StdError;
44use vrl::value::{KeyString, Value as VrlValue};
45
46use crate::error::{
47 ArrayElementMustBeObjectSnafu, CoerceIncompatibleTypesSnafu,
48 IdentifyPipelineColumnTypeMismatchSnafu, InvalidTimestampSnafu, Result,
49 TimeIndexMustBeNonNullSnafu, TransformColumnNameMustBeUniqueSnafu,
50 TransformMultipleTimestampIndexSnafu, TransformTimestampIndexCountSnafu, ValueMustBeMapSnafu,
51};
52use crate::etl::PipelineDocVersion;
53use crate::etl::ctx_req::ContextOpt;
54use crate::etl::field::{Field, Fields};
55use crate::etl::transform::index::Index;
56use crate::etl::transform::{Transform, Transforms};
57use crate::{PipelineContext, truthy, unwrap_or_continue_if_err};
58
59const DEFAULT_MAX_NESTED_LEVELS_FOR_JSON_FLATTENING: usize = 10;
60
61pub type RowWithTableSuffix = (Row, Option<String>);
63
64#[derive(Debug, Clone)]
67pub struct GreptimeTransformer {
68 transforms: Transforms,
69 schema: Vec<ColumnSchema>,
70}
71
72#[derive(Debug, Default)]
74pub struct GreptimePipelineParams {
75 options: HashMap<String, String>,
78
79 pub skip_error: OnceCell<bool>,
81 pub max_nested_levels: OnceCell<usize>,
84}
85
86impl GreptimePipelineParams {
87 pub fn from_params(params: Option<&str>) -> Self {
91 let options = Self::parse_header_str_to_map(params);
92
93 Self {
94 options,
95 skip_error: OnceCell::new(),
96 max_nested_levels: OnceCell::new(),
97 }
98 }
99
100 pub fn from_map(options: HashMap<String, String>) -> Self {
101 Self {
102 options,
103 skip_error: OnceCell::new(),
104 max_nested_levels: OnceCell::new(),
105 }
106 }
107
108 pub fn parse_header_str_to_map(params: Option<&str>) -> HashMap<String, String> {
109 if let Some(params) = params {
110 if params.is_empty() {
111 HashMap::new()
112 } else {
113 params
114 .split('&')
115 .filter_map(|s| s.split_once('='))
116 .map(|(k, v)| (k.to_string(), v.to_string()))
117 .collect::<HashMap<String, String>>()
118 }
119 } else {
120 HashMap::new()
121 }
122 }
123
124 pub fn skip_error(&self) -> bool {
126 *self
127 .skip_error
128 .get_or_init(|| self.options.get("skip_error").map(truthy).unwrap_or(false))
129 }
130
131 pub fn max_nested_levels(&self) -> usize {
134 *self.max_nested_levels.get_or_init(|| {
135 self.options
136 .get("max_nested_levels")
137 .and_then(|s| s.parse::<usize>().ok())
138 .filter(|v| *v > 0)
139 .unwrap_or(DEFAULT_MAX_NESTED_LEVELS_FOR_JSON_FLATTENING)
140 })
141 }
142}
143
144impl GreptimeTransformer {
145 fn add_greptime_timestamp_column(transforms: &mut Transforms) {
147 let type_ = ColumnDataType::TimestampNanosecond;
148 let default = None;
149
150 let transform = Transform {
151 fields: Fields::one(Field::new(greptime_timestamp().to_string(), None)),
152 type_,
153 json_settings: None,
154 default,
155 index: Some(Index::Time),
156 index_options: None,
157 on_failure: Some(crate::etl::transform::OnFailure::Default),
158 tag: false,
159 };
160 transforms.push(transform);
161 }
162
163 fn init_schemas(transforms: &Transforms) -> Result<Vec<ColumnSchema>> {
165 let mut schema = vec![];
166 for transform in transforms.iter() {
167 schema.extend(coerce_columns(transform)?);
168 }
169 Ok(schema)
170 }
171}
172
173impl GreptimeTransformer {
174 pub fn new(mut transforms: Transforms, doc_version: &PipelineDocVersion) -> Result<Self> {
175 let mut column_names_set = HashSet::new();
177 let mut timestamp_columns = vec![];
178
179 for transform in transforms.iter() {
180 let target_fields_set = transform
181 .fields
182 .iter()
183 .map(|f| f.target_or_input_field())
184 .collect::<HashSet<_>>();
185
186 let intersections: Vec<_> = column_names_set.intersection(&target_fields_set).collect();
187 if !intersections.is_empty() {
188 let duplicates = intersections.iter().join(",");
189 return TransformColumnNameMustBeUniqueSnafu { duplicates }.fail();
190 }
191
192 column_names_set.extend(target_fields_set);
193
194 if let Some(idx) = transform.index
195 && idx == Index::Time
196 {
197 match transform.fields.len() {
198 1 => timestamp_columns.push(transform.fields.first().unwrap().input_field()),
200 _ => {
201 return TransformMultipleTimestampIndexSnafu {
202 columns: transform.fields.iter().map(|x| x.input_field()).join(", "),
203 }
204 .fail();
205 }
206 }
207 }
208 }
209
210 let schema = match timestamp_columns.len() {
211 0 if doc_version == &PipelineDocVersion::V1 => {
212 GreptimeTransformer::add_greptime_timestamp_column(&mut transforms);
214 GreptimeTransformer::init_schemas(&transforms)?
215 }
216 1 => GreptimeTransformer::init_schemas(&transforms)?,
217 count => {
218 let columns = timestamp_columns.iter().join(", ");
219 return TransformTimestampIndexCountSnafu { count, columns }.fail();
220 }
221 };
222 Ok(GreptimeTransformer { transforms, schema })
223 }
224
225 pub fn transform_mut(
226 &self,
227 pipeline_map: &mut VrlValue,
228 is_v1: bool,
229 ) -> Result<Vec<GreptimeValue>> {
230 self.transform_mut_with_schema(pipeline_map, is_v1, &SchemaInfo::default(), None)
231 }
232
233 pub(crate) fn transform_mut_with_schema(
234 &self,
235 pipeline_map: &mut VrlValue,
236 is_v1: bool,
237 schema_info: &SchemaInfo,
238 table_suffix: Option<&str>,
239 ) -> Result<Vec<GreptimeValue>> {
240 let mut values = vec![GreptimeValue { value_data: None }; self.schema.len()];
241 let mut output_index = 0;
242 for transform in self.transforms.iter() {
243 for field in transform.fields.iter() {
244 let column_name = field.input_field();
245
246 let pipeline_map = pipeline_map.as_object_mut().context(ValueMustBeMapSnafu)?;
247 match pipeline_map.get(column_name) {
249 Some(v) => {
250 let json_settings = if transform.type_ == ColumnDataType::Json
251 && matches!(v, VrlValue::Array(_) | VrlValue::Object(_))
252 {
253 schema_info.json_settings_for_column(
254 field.target_or_input_field(),
255 table_suffix,
256 )?
257 } else {
258 None
259 };
260 let value_data = coerce_value(v, transform, json_settings.as_ref())?;
261 values[output_index] = GreptimeValue { value_data };
263 }
264 None => {
265 let value_data = match transform.on_failure {
266 Some(crate::etl::transform::OnFailure::Default) => {
267 match transform.get_default() {
268 Some(default) => Some(default.clone()),
269 None => transform.get_default_value_when_data_is_none(),
270 }
271 }
272 Some(crate::etl::transform::OnFailure::Ignore) => None,
273 None => None,
274 };
275 if transform.is_timeindex() && value_data.is_none() {
276 return TimeIndexMustBeNonNullSnafu.fail();
277 }
278 values[output_index] = GreptimeValue { value_data };
279 }
280 }
281 output_index += 1;
282 if !is_v1 {
283 pipeline_map.remove(column_name);
286 }
287 }
288 }
289 Ok(values)
290 }
291
292 pub fn transforms(&self) -> &Transforms {
293 &self.transforms
294 }
295
296 pub fn schemas(&self) -> &Vec<greptime_proto::v1::ColumnSchema> {
297 &self.schema
298 }
299
300 pub fn has_json_transform(&self) -> bool {
301 self.transforms
302 .iter()
303 .any(|transform| transform.type_ == ColumnDataType::Json)
304 }
305
306 pub fn transforms_mut(&mut self) -> &mut Transforms {
307 &mut self.transforms
308 }
309}
310
311#[derive(Clone)]
312pub struct ColumnMetadata {
313 column_schema: datatypes::schema::ColumnSchema,
314 semantic_type: SemanticType,
315 json_settings: OnceCell<JsonSettings>,
316}
317
318impl From<ColumnSchema> for ColumnMetadata {
319 fn from(value: ColumnSchema) -> Self {
320 let datatype = value.datatype();
321 let semantic_type = value.semantic_type();
322 let ColumnSchema {
323 column_name,
324 datatype: _,
325 semantic_type: _,
326 datatype_extension,
327 options,
328 } = value;
329
330 let column_schema = datatypes::schema::ColumnSchema::new(
331 column_name,
332 ColumnDataTypeWrapper::new(datatype, datatype_extension).into(),
333 semantic_type != SemanticType::Timestamp,
334 );
335
336 let metadata = collect_column_options(options.as_ref());
337 let column_schema = column_schema.with_metadata(metadata);
338
339 Self {
340 column_schema,
341 semantic_type,
342 json_settings: OnceCell::new(),
343 }
344 }
345}
346
347impl ColumnMetadata {
348 fn json_settings(&self) -> Result<&JsonSettings> {
349 self.json_settings.get_or_try_init(|| {
350 if let Some(extension) = self.column_schema.extension_type::<Json2ExtensionType>()? {
351 Ok(extension.metadata().json_settings().clone())
352 } else {
353 Ok(parse_legacy_json2_settings(self.column_schema.metadata())?.unwrap_or_default())
354 }
355 })
356 }
357}
358
359impl TryFrom<ColumnMetadata> for ColumnSchema {
360 type Error = api::error::Error;
361
362 fn try_from(value: ColumnMetadata) -> std::result::Result<Self, Self::Error> {
363 let ColumnMetadata {
364 column_schema,
365 semantic_type,
366 ..
367 } = value;
368
369 let options = options_from_column_schema(&column_schema);
370
371 let (datatype, datatype_extension) =
372 ColumnDataTypeWrapper::try_from(column_schema.data_type).map(|x| x.into_parts())?;
373
374 Ok(ColumnSchema {
375 column_name: column_schema.name,
376 datatype: datatype as _,
377 semantic_type: semantic_type as _,
378 datatype_extension,
379 options,
380 })
381 }
382}
383
384#[derive(Default)]
388pub struct SchemaInfo {
389 pub schema: Vec<ColumnMetadata>,
391 pub index: HashMap<String, usize>,
393 tables: HashMap<String, Option<Arc<Table>>>,
395}
396
397impl SchemaInfo {
398 pub fn with_capacity(capacity: usize) -> Self {
399 Self {
400 schema: Vec::with_capacity(capacity),
401 index: HashMap::with_capacity(capacity),
402 tables: HashMap::new(),
403 }
404 }
405
406 pub fn from_schema_list(schema_list: Vec<ColumnSchema>) -> Self {
407 let mut index = HashMap::new();
408 for (i, schema) in schema_list.iter().enumerate() {
409 index.insert(schema.column_name.clone(), i);
410 }
411 Self {
412 schema: schema_list.into_iter().map(Into::into).collect(),
413 index,
414 tables: HashMap::new(),
415 }
416 }
417
418 pub fn set_table(&mut self, table: Option<Arc<Table>>) {
419 self.set_table_for_suffix(String::new(), table);
420 }
421
422 pub fn has_table_for_suffix(&self, table_suffix: &str) -> bool {
423 self.tables.contains_key(table_suffix)
424 }
425
426 pub fn set_table_for_suffix(&mut self, table_suffix: String, table: Option<Arc<Table>>) {
427 self.tables.insert(table_suffix, table);
428 }
429
430 fn table_for_suffix(&self, table_suffix: Option<&str>) -> Option<&Arc<Table>> {
431 let table_suffix = table_suffix.unwrap_or_default();
432 match self.tables.get(table_suffix) {
433 Some(table) => table.as_ref(),
434 None if !table_suffix.is_empty() => self.tables.get("").and_then(Option::as_ref),
435 None => None,
436 }
437 }
438
439 fn find_column_schema_in_table(&self, column_name: &str) -> Option<ColumnMetadata> {
440 if let Some(table) = self.table_for_suffix(None)
441 && let Some(i) = table.schema_ref().column_index_by_name(column_name)
442 {
443 let column_schema = table.schema_ref().column_schemas()[i].clone();
444
445 let semantic_type = if column_schema.is_time_index() {
446 SemanticType::Timestamp
447 } else if table.table_info().meta.primary_key_indices.contains(&i) {
448 SemanticType::Tag
449 } else {
450 SemanticType::Field
451 };
452
453 Some(ColumnMetadata {
454 column_schema,
455 semantic_type,
456 json_settings: OnceCell::new(),
457 })
458 } else {
459 None
460 }
461 }
462
463 fn json_settings_for_column(
464 &self,
465 column_name: &str,
466 table_suffix: Option<&str>,
467 ) -> Result<Option<JsonSettings>> {
468 let Some(column_schema) = self
469 .table_for_suffix(table_suffix)
470 .and_then(|table| table.schema_ref().column_schema_by_name(column_name))
471 else {
472 return Ok(None);
473 };
474 if !column_schema.data_type.is_json2() {
475 return Ok(None);
476 }
477
478 if let Some(extension) = column_schema.extension_type::<Json2ExtensionType>()? {
479 Ok(Some(extension.metadata().json_settings().clone()))
480 } else {
481 Ok(Some(
482 parse_legacy_json2_settings(column_schema.metadata())?.unwrap_or_default(),
483 ))
484 }
485 }
486
487 pub fn column_schemas(&self) -> api::error::Result<Vec<ColumnSchema>> {
488 self.schema
489 .iter()
490 .map(|x| x.clone().try_into())
491 .collect::<api::error::Result<Vec<_>>>()
492 }
493}
494
495fn resolve_schema(
496 index: Option<usize>,
497 pipeline_context: &PipelineContext,
498 column: &str,
499 value_type: &ConcreteDataType,
500 schema_info: &mut SchemaInfo,
501) -> Result<()> {
502 if let Some(index) = index {
503 let column_type = &mut schema_info.schema[index].column_schema.data_type;
504 match (column_type, value_type) {
505 (column_type, value_type) if column_type == value_type => Ok(()),
506 (ConcreteDataType::Json(column_type), ConcreteDataType::Json(value_type))
507 if column_type.is_json2() && value_type.is_json2() =>
508 {
509 Ok(())
510 }
511 (column_type, value_type) => IdentifyPipelineColumnTypeMismatchSnafu {
512 column,
513 expected: column_type.to_string(),
514 actual: value_type.to_string(),
515 }
516 .fail(),
517 }
518 } else {
519 let column_schema = schema_info
520 .find_column_schema_in_table(column)
521 .unwrap_or_else(|| {
522 let semantic_type = decide_semantic(pipeline_context, column);
523 let column_schema = datatypes::schema::ColumnSchema::new(
524 column,
525 value_type.clone(),
526 semantic_type != SemanticType::Timestamp,
527 );
528 ColumnMetadata {
529 column_schema,
530 semantic_type,
531 json_settings: OnceCell::new(),
532 }
533 });
534 let key = column.to_string();
535 schema_info.schema.push(column_schema);
536 schema_info.index.insert(key, schema_info.schema.len() - 1);
537 Ok(())
538 }
539}
540
541fn calc_ts(p_ctx: &PipelineContext, values: &VrlValue) -> Result<Option<ValueData>> {
542 match p_ctx.channel {
543 Channel::Prometheus => {
544 let ts = values
545 .as_object()
546 .and_then(|m| m.get(greptime_timestamp()))
547 .and_then(|ts| ts.try_into_i64().ok())
548 .unwrap_or_default();
549 Ok(Some(ValueData::TimestampMillisecondValue(ts)))
550 }
551 _ => {
552 let custom_ts = p_ctx.pipeline_definition.get_custom_ts();
553 match custom_ts {
554 Some(ts) => {
555 let ts_field = values.as_object().and_then(|m| m.get(ts.get_column_name()));
556 Some(ts.get_timestamp_value(ts_field)).transpose()
557 }
558 None => Ok(Some(ValueData::TimestampNanosecondValue(
559 chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default(),
560 ))),
561 }
562 }
563 }
564}
565
566pub(crate) fn values_to_rows(
575 schema_info: &mut SchemaInfo,
576 mut values: VrlValue,
577 pipeline_ctx: &PipelineContext<'_>,
578 row: Option<Vec<GreptimeValue>>,
579 need_calc_ts: bool,
580 tablesuffix_template: Option<&crate::tablesuffix::TableSuffixTemplate>,
581) -> Result<std::collections::HashMap<ContextOpt, Vec<RowWithTableSuffix>>> {
582 let skip_error = pipeline_ctx.pipeline_param.skip_error();
583 let VrlValue::Array(arr) = values else {
584 let mut result = std::collections::HashMap::new();
586
587 let table_suffix = ContextOpt::resolve_table_suffix(tablesuffix_template, &values);
588 let opt = match ContextOpt::from_pipeline_map_to_opt(&mut values) {
589 Ok(r) => r,
590 Err(e) => return if skip_error { Ok(result) } else { Err(e) },
591 };
592
593 let row = match values_to_row(schema_info, values, pipeline_ctx, row, need_calc_ts) {
594 Ok(r) => r,
595 Err(e) => return if skip_error { Ok(result) } else { Err(e) },
596 };
597 result.insert(opt, vec![(row, table_suffix)]);
598 return Ok(result);
599 };
600
601 let mut rows_by_context: std::collections::HashMap<ContextOpt, Vec<RowWithTableSuffix>> =
602 std::collections::HashMap::new();
603 for (index, mut value) in arr.into_iter().enumerate() {
604 if !value.is_object() {
605 unwrap_or_continue_if_err!(
606 ArrayElementMustBeObjectSnafu {
607 index,
608 actual_type: value.kind_str().to_string(),
609 }
610 .fail(),
611 skip_error
612 );
613 }
614
615 let table_suffix = ContextOpt::resolve_table_suffix(tablesuffix_template, &value);
617 let opt = unwrap_or_continue_if_err!(
618 ContextOpt::from_pipeline_map_to_opt(&mut value),
619 skip_error
620 );
621 let transformed_row = unwrap_or_continue_if_err!(
622 values_to_row(schema_info, value, pipeline_ctx, row.clone(), need_calc_ts),
623 skip_error
624 );
625 rows_by_context
626 .entry(opt)
627 .or_default()
628 .push((transformed_row, table_suffix));
629 }
630 Ok(rows_by_context)
631}
632
633pub(crate) fn values_to_row(
640 schema_info: &mut SchemaInfo,
641 values: VrlValue,
642 pipeline_ctx: &PipelineContext<'_>,
643 row: Option<Vec<GreptimeValue>>,
644 need_calc_ts: bool,
645) -> Result<Row> {
646 let mut row: Vec<GreptimeValue> =
647 row.unwrap_or_else(|| Vec::with_capacity(schema_info.schema.len()));
648 let custom_ts = pipeline_ctx.pipeline_definition.get_custom_ts();
649
650 if need_calc_ts {
651 let ts = calc_ts(pipeline_ctx, &values)?;
653 row.push(GreptimeValue { value_data: ts });
654 }
655
656 row.resize(schema_info.schema.len(), GreptimeValue { value_data: None });
657
658 let ts_column_name = custom_ts
660 .as_ref()
661 .map_or(greptime_timestamp(), |ts| ts.get_column_name());
662
663 let values = values.into_object().context(ValueMustBeMapSnafu)?;
664
665 for (column_name, value) in values {
666 if need_calc_ts && column_name.as_str() == ts_column_name {
667 continue;
668 }
669
670 resolve_value(
671 value,
672 column_name.into(),
673 &mut row,
674 schema_info,
675 pipeline_ctx,
676 )?;
677 }
678 Ok(Row { values: row })
679}
680
681fn decide_semantic(p_ctx: &PipelineContext, column_name: &str) -> SemanticType {
682 if p_ctx.channel == Channel::Prometheus && column_name != greptime_value() {
683 SemanticType::Tag
684 } else {
685 SemanticType::Field
686 }
687}
688
689fn resolve_value(
690 value: VrlValue,
691 column_name: String,
692 row: &mut Vec<GreptimeValue>,
693 schema_info: &mut SchemaInfo,
694 p_ctx: &PipelineContext,
695) -> Result<()> {
696 let index = schema_info.index.get(&column_name).copied();
697
698 let value_data = match value {
699 VrlValue::Null => return Ok(()),
700
701 VrlValue::Integer(v) => {
702 resolve_schema(
704 index,
705 p_ctx,
706 &column_name,
707 &ConcreteDataType::int64_datatype(),
708 schema_info,
709 )?;
710 Some(ValueData::I64Value(v))
711 }
712
713 VrlValue::Float(v) => {
714 resolve_schema(
716 index,
717 p_ctx,
718 &column_name,
719 &ConcreteDataType::float64_datatype(),
720 schema_info,
721 )?;
722 Some(ValueData::F64Value(v.into()))
723 }
724
725 VrlValue::Boolean(v) => {
726 resolve_schema(
727 index,
728 p_ctx,
729 &column_name,
730 &ConcreteDataType::boolean_datatype(),
731 schema_info,
732 )?;
733 Some(ValueData::BoolValue(v))
734 }
735
736 VrlValue::Bytes(v) => {
737 resolve_schema(
738 index,
739 p_ctx,
740 &column_name,
741 &ConcreteDataType::string_datatype(),
742 schema_info,
743 )?;
744 Some(ValueData::StringValue(String::from_utf8_lossy_owned(
745 v.to_vec(),
746 )))
747 }
748
749 VrlValue::Regex(v) => {
750 warn!(
751 "Persisting regex value in the table, this should not happen, column_name: {}",
752 column_name
753 );
754 resolve_schema(
755 index,
756 p_ctx,
757 &column_name,
758 &ConcreteDataType::string_datatype(),
759 schema_info,
760 )?;
761 Some(ValueData::StringValue(v.to_string()))
762 }
763
764 VrlValue::Timestamp(ts) => {
765 let ns = ts.timestamp_nanos_opt().context(InvalidTimestampSnafu {
766 input: ts.to_rfc3339(),
767 })?;
768 resolve_schema(
769 index,
770 p_ctx,
771 &column_name,
772 &ConcreteDataType::timestamp_nanosecond_datatype(),
773 schema_info,
774 )?;
775 Some(ValueData::TimestampNanosecondValue(ns))
776 }
777
778 VrlValue::Array(_) | VrlValue::Object(_) => {
779 let index = index.or_else(|| {
780 let column = schema_info.find_column_schema_in_table(&column_name)?;
781 let index = schema_info.schema.len();
782 schema_info.schema.push(column);
783 schema_info.index.insert(column_name.clone(), index);
784 Some(index)
785 });
786 let json2_index = index.filter(|&index| {
788 matches!(
789 &schema_info.schema[index].column_schema.data_type,
790 ConcreteDataType::Json(column_type) if column_type.is_json2()
791 )
792 });
793
794 let value = if let Some(index) = json2_index {
795 let value: serde_json::Value = value.try_into().map_err(|e: StdError| {
796 CoerceIncompatibleTypesSnafu { msg: e.to_string() }.build()
797 })?;
798 let value = schema_info.schema[index].json_settings()?.encode(value)?;
799
800 resolve_schema(
801 Some(index),
802 p_ctx,
803 &column_name,
804 &ConcreteDataType::json2(Default::default()),
805 schema_info,
806 )?;
807
808 let Value::Json(value) = value else {
809 unreachable!()
810 };
811 ValueData::JsonValue(encode_json_value(*value))
812 } else {
813 resolve_schema(
814 index,
815 p_ctx,
816 &column_name,
817 &ConcreteDataType::binary_datatype(),
818 schema_info,
819 )?;
820
821 let value = vrl_value_to_jsonb_value(&value);
822 ValueData::BinaryValue(value.to_vec())
823 };
824 Some(value)
825 }
826 };
827
828 let value = GreptimeValue { value_data };
829 if let Some(index) = index {
830 row[index] = value;
831 } else {
832 row.push(value);
833 }
834 Ok(())
835}
836
837fn vrl_value_to_jsonb_value<'a>(value: &'a VrlValue) -> jsonb::Value<'a> {
838 match value {
839 VrlValue::Bytes(bytes) => jsonb::Value::String(String::from_utf8_lossy(bytes)),
840 VrlValue::Regex(value_regex) => jsonb::Value::String(Cow::Borrowed(value_regex.as_str())),
841 VrlValue::Integer(i) => jsonb::Value::Number(Number::Int64(*i)),
842 VrlValue::Float(not_nan) => jsonb::Value::Number(Number::Float64(not_nan.into_inner())),
843 VrlValue::Boolean(b) => jsonb::Value::Bool(*b),
844 VrlValue::Timestamp(date_time) => jsonb::Value::String(Cow::Owned(date_time.to_rfc3339())),
845 VrlValue::Object(btree_map) => jsonb::Value::Object(
846 btree_map
847 .iter()
848 .map(|(key, value)| (key.to_string(), vrl_value_to_jsonb_value(value)))
849 .collect(),
850 ),
851 VrlValue::Array(values) => jsonb::Value::Array(
852 values
853 .iter()
854 .map(|value| vrl_value_to_jsonb_value(value))
855 .collect(),
856 ),
857 VrlValue::Null => jsonb::Value::Null,
858 }
859}
860
861fn identity_pipeline_inner(
862 pipeline_maps: Vec<VrlValue>,
863 pipeline_ctx: &PipelineContext<'_>,
864 max_nested_levels: usize,
865) -> Result<(SchemaInfo, HashMap<ContextOpt, Vec<Row>>)> {
866 let skip_error = pipeline_ctx.pipeline_param.skip_error();
867 let mut schema_info = SchemaInfo::default();
868 let custom_ts = pipeline_ctx.pipeline_definition.get_custom_ts();
869
870 let column_schema = datatypes::schema::ColumnSchema::new(
872 custom_ts
873 .map(|ts| ts.get_column_name().to_string())
874 .unwrap_or_else(|| greptime_timestamp().to_string()),
875 custom_ts
876 .map(|c| ConcreteDataType::from(ColumnDataTypeWrapper::new(c.get_datatype(), None)))
877 .unwrap_or_else(|| {
878 if pipeline_ctx.channel == Channel::Prometheus {
879 ConcreteDataType::timestamp_millisecond_datatype()
880 } else {
881 ConcreteDataType::timestamp_nanosecond_datatype()
882 }
883 }),
884 false,
885 );
886 schema_info.schema.push(ColumnMetadata {
887 column_schema,
888 semantic_type: SemanticType::Timestamp,
889 json_settings: OnceCell::new(),
890 });
891
892 let mut opt_map = HashMap::new();
893 let len = pipeline_maps.len();
894
895 for pipeline_map in pipeline_maps {
896 let mut pipeline_map =
897 unwrap_or_continue_if_err!(flatten_object(pipeline_map, max_nested_levels), skip_error);
898 let opt = unwrap_or_continue_if_err!(
899 ContextOpt::from_pipeline_map_to_opt(&mut pipeline_map),
900 skip_error
901 );
902 let row = unwrap_or_continue_if_err!(
903 values_to_row(&mut schema_info, pipeline_map, pipeline_ctx, None, true),
904 skip_error
905 );
906
907 opt_map
908 .entry(opt)
909 .or_insert_with(|| Vec::with_capacity(len))
910 .push(row);
911 }
912
913 let column_count = schema_info.schema.len();
914 for (_, row) in opt_map.iter_mut() {
915 for row in row.iter_mut() {
916 assert!(
917 column_count >= row.values.len(),
918 "column_count: {}, row.values.len(): {}",
919 column_count,
920 row.values.len()
921 );
922 row.values
923 .resize(column_count, GreptimeValue { value_data: None });
924 }
925 }
926
927 Ok((schema_info, opt_map))
928}
929
930pub fn identity_pipeline(
939 array: Vec<VrlValue>,
940 table: Option<Arc<table::Table>>,
941 pipeline_ctx: &PipelineContext<'_>,
942) -> Result<HashMap<ContextOpt, Rows>> {
943 let max_nested_levels = pipeline_ctx.pipeline_param.max_nested_levels();
944
945 let (mut schema, opt_map) = identity_pipeline_inner(array, pipeline_ctx, max_nested_levels)?;
946 if let Some(table) = table {
947 let table_info = table.table_info();
948 for tag_name in table_info.meta.row_key_column_names() {
949 if let Some(index) = schema.index.get(tag_name) {
950 schema.schema[*index].semantic_type = SemanticType::Tag;
951 }
952 }
953 }
954
955 let column_schemas = schema.column_schemas()?;
956 Ok(opt_map
957 .into_iter()
958 .map(|(opt, rows)| {
959 (
960 opt,
961 Rows {
962 schema: column_schemas.clone(),
963 rows,
964 },
965 )
966 })
967 .collect::<HashMap<ContextOpt, Rows>>())
968}
969
970pub fn flatten_object(object: VrlValue, max_nested_levels: usize) -> Result<VrlValue> {
976 let mut flattened = BTreeMap::new();
977 let object = object.into_object().context(ValueMustBeMapSnafu)?;
978
979 if !object.is_empty() {
980 do_flatten_object(&mut flattened, None, object, 1, max_nested_levels);
982 }
983
984 Ok(VrlValue::Object(flattened))
985}
986
987pub(crate) fn vrl_value_to_serde_json(value: &VrlValue) -> serde_json_crate::Value {
988 match value {
989 VrlValue::Null => serde_json_crate::Value::Null,
990 VrlValue::Boolean(b) => serde_json_crate::Value::Bool(*b),
991 VrlValue::Integer(i) => serde_json_crate::Value::Number((*i).into()),
992 VrlValue::Float(not_nan) => serde_json_crate::Number::from_f64(not_nan.into_inner())
993 .map(serde_json_crate::Value::Number)
994 .unwrap_or(serde_json_crate::Value::Null),
995 VrlValue::Bytes(bytes) => {
996 serde_json_crate::Value::String(String::from_utf8_lossy(bytes).into_owned())
997 }
998 VrlValue::Regex(re) => serde_json_crate::Value::String(re.as_str().to_string()),
999 VrlValue::Timestamp(ts) => serde_json_crate::Value::String(ts.to_rfc3339()),
1000 VrlValue::Array(arr) => {
1001 serde_json_crate::Value::Array(arr.iter().map(vrl_value_to_serde_json).collect())
1002 }
1003 VrlValue::Object(map) => serde_json_crate::Value::Object(
1004 map.iter()
1005 .map(|(k, v)| (k.to_string(), vrl_value_to_serde_json(v)))
1006 .collect(),
1007 ),
1008 }
1009}
1010
1011fn do_flatten_object(
1012 dest: &mut BTreeMap<KeyString, VrlValue>,
1013 base: Option<&str>,
1014 object: BTreeMap<KeyString, VrlValue>,
1015 current_level: usize,
1016 max_nested_levels: usize,
1017) {
1018 for (key, value) in object {
1019 let new_key = base.map_or_else(
1020 || key.clone(),
1021 |base_key| format!("{base_key}.{key}").into(),
1022 );
1023
1024 match value {
1025 VrlValue::Object(object) => {
1026 if current_level >= max_nested_levels {
1027 let json_string = serde_json_crate::to_string(&vrl_value_to_serde_json(
1029 &VrlValue::Object(object),
1030 ))
1031 .unwrap_or_else(|_| String::from("{}"));
1032 dest.insert(new_key, VrlValue::Bytes(Bytes::from(json_string)));
1033 } else {
1034 do_flatten_object(
1035 dest,
1036 Some(&new_key),
1037 object,
1038 current_level + 1,
1039 max_nested_levels,
1040 );
1041 }
1042 }
1043 VrlValue::Array(_) => {
1045 let json_string = serde_json_crate::to_string(&vrl_value_to_serde_json(&value))
1046 .unwrap_or_else(|_| String::from("[]"));
1047 dest.insert(new_key, VrlValue::Bytes(Bytes::from(json_string)));
1048 }
1049 _ => {
1051 dest.insert(new_key, value);
1052 }
1053 }
1054 }
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use api::v1::SemanticType;
1060 use common_recordbatch::RecordBatch;
1061 use datatypes::extension::json::JsonMetadata;
1062 use datatypes::json::JsonTypeHint;
1063 use datatypes::schema::{ColumnSchema as DatatypeColumnSchema, Schema};
1064 use table::test_util::MemTable;
1065
1066 use super::*;
1067 use crate::{PipelineDefinition, identity_pipeline};
1068
1069 #[test]
1070 fn test_column_metadata_caches_json_settings() -> Result<()> {
1071 let column = ColumnMetadata {
1072 column_schema: datatypes::schema::ColumnSchema::new(
1073 "data",
1074 ConcreteDataType::json2(Default::default()),
1075 true,
1076 ),
1077 semantic_type: SemanticType::Field,
1078 json_settings: OnceCell::new(),
1079 };
1080
1081 let first = column.json_settings()?;
1082 let second = column.json_settings()?;
1083 assert!(std::ptr::eq(first, second));
1084 Ok(())
1085 }
1086
1087 #[test]
1088 fn test_transform_json2_uses_destination_table_settings() {
1089 let table = |name: &str, settings: JsonSettings, sample: serde_json::Value| {
1090 let data_type = settings.encode(sample).unwrap().data_type();
1091 let mut column_schema = DatatypeColumnSchema::new("payload", data_type, true);
1092 column_schema.with_extension_type(&Json2ExtensionType::new(Arc::new(
1093 JsonMetadata::new(settings),
1094 )));
1095 MemTable::table(
1096 name,
1097 RecordBatch::new_empty(Arc::new(Schema::new(vec![column_schema]))),
1098 )
1099 };
1100 let int_settings = JsonSettings::try_new(
1101 vec![JsonTypeHint {
1102 path: vec!["age".to_string()],
1103 data_type: ConcreteDataType::int64_datatype(),
1104 nullable: false,
1105 default_constraint: None,
1106 inverted_index: false,
1107 }],
1108 None,
1109 )
1110 .unwrap();
1111 let mut schema_info = SchemaInfo::default();
1112 schema_info.set_table(Some(table(
1113 "events",
1114 int_settings,
1115 serde_json::json!({"age": 42}),
1116 )));
1117 let string_settings = JsonSettings::try_new(
1118 vec![JsonTypeHint {
1119 path: vec!["age".to_string()],
1120 data_type: ConcreteDataType::string_datatype(),
1121 nullable: false,
1122 default_constraint: None,
1123 inverted_index: false,
1124 }],
1125 None,
1126 )
1127 .unwrap();
1128 schema_info.set_table_for_suffix(
1129 "_mobile".to_string(),
1130 Some(table(
1131 "events_mobile",
1132 string_settings,
1133 serde_json::json!({"age": "42"}),
1134 )),
1135 );
1136
1137 let pipeline = crate::parse(&crate::Content::Yaml(
1138 r#"
1139transform:
1140 - field: source, payload
1141 type: json2
1142table_suffix: _${device}
1143"#,
1144 ))
1145 .unwrap();
1146 let (pipeline, _, pipeline_definition, pipeline_params) = crate::setup_pipeline!(pipeline);
1147 let pipeline_context =
1148 PipelineContext::new(&pipeline_definition, &pipeline_params, Channel::Unknown);
1149
1150 let error = pipeline
1151 .exec_mut(
1152 serde_json::json!({"source": {"age": "42"}}).into(),
1153 &pipeline_context,
1154 &mut schema_info,
1155 )
1156 .unwrap_err();
1157 assert!(
1158 error.to_string().contains("does not match JSON2 type hint"),
1159 "{error:?}"
1160 );
1161
1162 let mut rows = pipeline
1163 .exec_mut(
1164 serde_json::json!({"source": {"age": "42"}, "device": "mobile"}).into(),
1165 &pipeline_context,
1166 &mut schema_info,
1167 )
1168 .unwrap()
1169 .into_transformed()
1170 .unwrap();
1171 let (row, table_suffix) = rows.swap_remove(0);
1172 assert_eq!(table_suffix.as_deref(), Some("_mobile"));
1173 assert!(matches!(
1174 &row.values[0].value_data,
1175 Some(ValueData::JsonValue(_))
1176 ));
1177 }
1178
1179 #[test]
1180 fn test_identify_pipeline() {
1181 let params = GreptimePipelineParams::default();
1182 let pipeline_ctx = PipelineContext::new(
1183 &PipelineDefinition::GreptimeIdentityPipeline(None),
1184 ¶ms,
1185 Channel::Unknown,
1186 );
1187 {
1188 let array = [
1189 serde_json::json!({
1190 "woshinull": null,
1191 "name": "Alice",
1192 "age": 20,
1193 "is_student": true,
1194 "score": 99.5,
1195 "hobbies": "reading",
1196 "address": "Beijing",
1197 }),
1198 serde_json::json!({
1199 "name": "Bob",
1200 "age": 21,
1201 "is_student": false,
1202 "score": "88.5",
1203 "hobbies": "swimming",
1204 "address": "Shanghai",
1205 "gaga": "gaga"
1206 }),
1207 ];
1208 let array = array.iter().map(|v| v.into()).collect();
1209 let rows = identity_pipeline(array, None, &pipeline_ctx);
1210 assert!(rows.is_err());
1211 assert_eq!(
1212 rows.err().unwrap().to_string(),
1213 "Column datatype mismatch. For column: score, expected datatype: Float64, actual datatype: String".to_string(),
1214 );
1215 }
1216 {
1217 let array = [
1218 serde_json::json!({
1219 "woshinull": null,
1220 "name": "Alice",
1221 "age": 20,
1222 "is_student": true,
1223 "score": 99.5,
1224 "hobbies": "reading",
1225 "address": "Beijing",
1226 }),
1227 serde_json::json!({
1228 "name": "Bob",
1229 "age": 21,
1230 "is_student": false,
1231 "score": 88,
1232 "hobbies": "swimming",
1233 "address": "Shanghai",
1234 "gaga": "gaga"
1235 }),
1236 ];
1237 let array = array.iter().map(|v| v.into()).collect();
1238 let rows = identity_pipeline(array, None, &pipeline_ctx);
1239 assert!(rows.is_err());
1240 assert_eq!(
1241 rows.err().unwrap().to_string(),
1242 "Column datatype mismatch. For column: score, expected datatype: Float64, actual datatype: Int64".to_string(),
1243 );
1244 }
1245 {
1246 let array = [
1247 serde_json::json!({
1248 "woshinull": null,
1249 "name": "Alice",
1250 "age": 20,
1251 "is_student": true,
1252 "score": 99.5,
1253 "hobbies": "reading",
1254 "address": "Beijing",
1255 }),
1256 serde_json::json!({
1257 "name": "Bob",
1258 "age": 21,
1259 "is_student": false,
1260 "score": 88.5,
1261 "hobbies": "swimming",
1262 "address": "Shanghai",
1263 "gaga": "gaga"
1264 }),
1265 ];
1266 let array = array.iter().map(|v| v.into()).collect();
1267 let rows = identity_pipeline(array, None, &pipeline_ctx);
1268 assert!(rows.is_ok());
1269 let mut rows = rows.unwrap();
1270 assert!(rows.len() == 1);
1271 let rows = rows.remove(&ContextOpt::default()).unwrap();
1272 assert_eq!(rows.schema.len(), 8);
1273 assert_eq!(rows.rows.len(), 2);
1274 assert_eq!(8, rows.rows[0].values.len());
1275 assert_eq!(8, rows.rows[1].values.len());
1276 }
1277 {
1278 let array = [
1279 serde_json::json!({
1280 "woshinull": null,
1281 "name": "Alice",
1282 "age": 20,
1283 "is_student": true,
1284 "score": 99.5,
1285 "hobbies": "reading",
1286 "address": "Beijing",
1287 }),
1288 serde_json::json!({
1289 "name": "Bob",
1290 "age": 21,
1291 "is_student": false,
1292 "score": 88.5,
1293 "hobbies": "swimming",
1294 "address": "Shanghai",
1295 "gaga": "gaga"
1296 }),
1297 ];
1298 let tag_column_names = ["name".to_string(), "address".to_string()];
1299
1300 let rows = identity_pipeline_inner(
1301 array.iter().map(|v| v.into()).collect(),
1302 &pipeline_ctx,
1303 pipeline_ctx.pipeline_param.max_nested_levels(),
1304 )
1305 .map(|(mut schema, mut rows)| {
1306 for name in tag_column_names {
1307 if let Some(index) = schema.index.get(&name) {
1308 schema.schema[*index].semantic_type = SemanticType::Tag;
1309 }
1310 }
1311
1312 assert!(rows.len() == 1);
1313 let rows = rows.remove(&ContextOpt::default()).unwrap();
1314
1315 Rows {
1316 schema: schema.column_schemas().unwrap(),
1317 rows,
1318 }
1319 });
1320
1321 assert!(rows.is_ok());
1322 let rows = rows.unwrap();
1323 assert_eq!(rows.schema.len(), 8);
1324 assert_eq!(rows.rows.len(), 2);
1325 assert_eq!(8, rows.rows[0].values.len());
1326 assert_eq!(8, rows.rows[1].values.len());
1327 assert_eq!(
1328 rows.schema
1329 .iter()
1330 .find(|x| x.column_name == "name")
1331 .unwrap()
1332 .semantic_type,
1333 SemanticType::Tag as i32
1334 );
1335 assert_eq!(
1336 rows.schema
1337 .iter()
1338 .find(|x| x.column_name == "address")
1339 .unwrap()
1340 .semantic_type,
1341 SemanticType::Tag as i32
1342 );
1343 assert_eq!(
1344 rows.schema
1345 .iter()
1346 .filter(|x| x.semantic_type == SemanticType::Tag as i32)
1347 .count(),
1348 2
1349 );
1350 }
1351 }
1352
1353 #[test]
1354 fn test_flatten() {
1355 let test_cases = vec![
1356 (
1358 serde_json::json!(
1359 {
1360 "a": {
1361 "b": {
1362 "c": [1, 2, 3]
1363 }
1364 },
1365 "d": [
1366 "foo",
1367 "bar"
1368 ],
1369 "e": {
1370 "f": [7, 8, 9],
1371 "g": {
1372 "h": 123,
1373 "i": "hello",
1374 "j": {
1375 "k": true
1376 }
1377 }
1378 }
1379 }
1380 ),
1381 10,
1382 Some(serde_json::json!(
1383 {
1384 "a.b.c": "[1,2,3]",
1385 "d": "[\"foo\",\"bar\"]",
1386 "e.f": "[7,8,9]",
1387 "e.g.h": 123,
1388 "e.g.i": "hello",
1389 "e.g.j.k": true
1390 }
1391 )),
1392 ),
1393 (
1395 serde_json::json!(
1396 {
1397 "a": {
1398 "b": {
1399 "c": {
1400 "d": [1, 2, 3]
1401 }
1402 }
1403 },
1404 "e": [
1405 "foo",
1406 "bar"
1407 ]
1408 }
1409 ),
1410 3,
1411 Some(serde_json::json!(
1412 {
1413 "a.b.c": "{\"d\":[1,2,3]}",
1414 "e": "[\"foo\",\"bar\"]"
1415 }
1416 )),
1417 ),
1418 ];
1419
1420 for (input, max_depth, expected) in test_cases {
1421 let input = input.into();
1422 let expected = expected.map(|e| e.into());
1423
1424 let flattened_object = flatten_object(input, max_depth).ok();
1425 assert_eq!(flattened_object, expected);
1426 }
1427 }
1428
1429 #[test]
1430 fn test_identity_pipeline_skip_error_flattens_valid_rows() {
1431 let params = GreptimePipelineParams::from_map(ahash::HashMap::from_iter([(
1432 "skip_error".to_string(),
1433 "true".to_string(),
1434 )]));
1435 let pipeline_def = PipelineDefinition::GreptimeIdentityPipeline(None);
1436 let pipeline_ctx = PipelineContext::new(&pipeline_def, ¶ms, Channel::Unknown);
1437 let array = vec![
1438 serde_json::json!({
1439 "service": "frontend",
1440 "nested": {
1441 "status": 200,
1442 "path": "/v1/ingest"
1443 },
1444 "labels": ["pipeline", "identity"]
1445 })
1446 .into(),
1447 VrlValue::Bytes("invalid_string".into()),
1448 serde_json::json!({
1449 "service": "frontend",
1450 "nested": {
1451 "status": 201,
1452 "path": "/v1/ingest"
1453 },
1454 "labels": ["pipeline", "identity"]
1455 })
1456 .into(),
1457 ];
1458
1459 let mut rows_by_opt = identity_pipeline(array, None, &pipeline_ctx).unwrap();
1460 let rows = rows_by_opt.remove(&ContextOpt::default()).unwrap();
1461
1462 assert_eq!(rows.rows.len(), 2);
1463 assert_eq!(rows.schema.len(), rows.rows[0].values.len());
1464 assert!(rows.schema.iter().any(|s| s.column_name == "nested.status"));
1465 assert!(rows.schema.iter().any(|s| s.column_name == "nested.path"));
1466 assert!(rows.schema.iter().any(|s| s.column_name == "labels"));
1467 }
1468
1469 use ahash::HashMap as AHashMap;
1470 #[test]
1471 fn test_values_to_rows_skip_error_handling() {
1472 let table_suffix_template: Option<crate::tablesuffix::TableSuffixTemplate> = None;
1473
1474 {
1476 let schema_info = &mut SchemaInfo::default();
1477 let input_array = vec![
1478 serde_json::json!({"name": "Alice", "age": 25}).into(),
1480 VrlValue::Bytes("invalid_string".into()),
1482 serde_json::json!({"name": "Bob", "age": 30}).into(),
1484 VrlValue::Integer(42),
1486 serde_json::json!({"name": "Charlie", "age": 35}).into(),
1488 ];
1489
1490 let params = GreptimePipelineParams::from_map(AHashMap::from_iter([(
1491 "skip_error".to_string(),
1492 "true".to_string(),
1493 )]));
1494
1495 let pipeline_ctx = PipelineContext::new(
1496 &PipelineDefinition::GreptimeIdentityPipeline(None),
1497 ¶ms,
1498 Channel::Unknown,
1499 );
1500
1501 let result = values_to_rows(
1502 schema_info,
1503 VrlValue::Array(input_array),
1504 &pipeline_ctx,
1505 None,
1506 true,
1507 table_suffix_template.as_ref(),
1508 );
1509
1510 assert!(result.is_ok());
1512 let rows_by_context = result.unwrap();
1513 let total_rows: usize = rows_by_context.values().map(|v| v.len()).sum();
1515 assert_eq!(total_rows, 3); }
1517
1518 {
1520 let schema_info = &mut SchemaInfo::default();
1521 let input_array = vec![
1522 serde_json::json!({"name": "Alice", "age": 25}).into(),
1523 VrlValue::Bytes("invalid_string".into()), ];
1525
1526 let params = GreptimePipelineParams::default(); let pipeline_ctx = PipelineContext::new(
1529 &PipelineDefinition::GreptimeIdentityPipeline(None),
1530 ¶ms,
1531 Channel::Unknown,
1532 );
1533
1534 let result = values_to_rows(
1535 schema_info,
1536 VrlValue::Array(input_array),
1537 &pipeline_ctx,
1538 None,
1539 true,
1540 table_suffix_template.as_ref(),
1541 );
1542
1543 assert!(result.is_err());
1545 let error_msg = result.unwrap_err().to_string();
1546 assert!(error_msg.contains("Array element at index 1 must be an object for one-to-many transformation, got string"));
1547 }
1548 }
1549
1550 #[test]
1552 fn test_values_to_rows_per_element_context_opt() {
1553 let table_suffix_template: Option<crate::tablesuffix::TableSuffixTemplate> = None;
1554 let schema_info = &mut SchemaInfo::default();
1555
1556 let input_array = vec![
1558 serde_json::json!({"name": "Alice", "greptime_ttl": "1h"}).into(),
1559 serde_json::json!({"name": "Bob", "greptime_ttl": "1h"}).into(),
1560 serde_json::json!({"name": "Charlie", "greptime_ttl": "24h"}).into(),
1561 ];
1562
1563 let params = GreptimePipelineParams::default();
1564 let pipeline_ctx = PipelineContext::new(
1565 &PipelineDefinition::GreptimeIdentityPipeline(None),
1566 ¶ms,
1567 Channel::Unknown,
1568 );
1569
1570 let result = values_to_rows(
1571 schema_info,
1572 VrlValue::Array(input_array),
1573 &pipeline_ctx,
1574 None,
1575 true,
1576 table_suffix_template.as_ref(),
1577 );
1578
1579 assert!(result.is_ok());
1580 let rows_by_context = result.unwrap();
1581
1582 assert_eq!(rows_by_context.len(), 2);
1584
1585 let total_rows: usize = rows_by_context.values().map(|v| v.len()).sum();
1587 assert_eq!(total_rows, 3);
1588
1589 let mut ttl_1h_count = 0;
1591 let mut ttl_24h_count = 0;
1592 for rows in rows_by_context.values() {
1593 if rows.len() == 2 {
1595 ttl_1h_count = rows.len();
1596 } else if rows.len() == 1 {
1597 ttl_24h_count = rows.len();
1598 }
1599 }
1600 assert_eq!(ttl_1h_count, 2); assert_eq!(ttl_24h_count, 1); }
1603}