Skip to main content

pipeline/etl/transform/transformer/
greptime.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
15pub 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
61/// Row with potentially designated table suffix.
62pub type RowWithTableSuffix = (Row, Option<String>);
63
64/// fields not in the columns will be discarded
65/// to prevent automatic column creation in GreptimeDB
66#[derive(Debug, Clone)]
67pub struct GreptimeTransformer {
68    transforms: Transforms,
69    schema: Vec<ColumnSchema>,
70}
71
72/// Parameters that can be used to configure the greptime pipelines.
73#[derive(Debug, Default)]
74pub struct GreptimePipelineParams {
75    /// The original options for configuring the greptime pipelines.
76    /// This should not be used directly, instead, use the parsed shortcut option values.
77    options: HashMap<String, String>,
78
79    /// Whether to skip error when processing the pipeline.
80    pub skip_error: OnceCell<bool>,
81    /// Max nested levels when flattening JSON object. Defaults to
82    /// `DEFAULT_MAX_NESTED_LEVELS_FOR_JSON_FLATTENING` when not provided.
83    pub max_nested_levels: OnceCell<usize>,
84}
85
86impl GreptimePipelineParams {
87    /// Create a `GreptimePipelineParams` from params string which is from the http header with key `x-greptime-pipeline-params`
88    /// The params is in the format of `key1=value1&key2=value2`,for example:
89    /// x-greptime-pipeline-params: max_nested_levels=5
90    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    /// Whether to skip error when processing the pipeline.
125    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    /// Max nested levels for JSON flattening. If not provided or invalid,
132    /// falls back to `DEFAULT_MAX_NESTED_LEVELS_FOR_JSON_FLATTENING`.
133    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    /// Add a default timestamp column to the transforms
146    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    /// Generate the schema for the GreptimeTransformer
164    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        // empty check is done in the caller
176        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                    //Safety unwrap is fine here because we have checked the length of real_fields
199                    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                // compatible with v1, add a default timestamp column
213                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                // let keep us `get` here to be compatible with v1
248                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                        // every transform fields has only one output field
262                        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                    // remove the column from the pipeline_map
284                    // so that the auto-transform can use the rest fields
285                    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/// This is used to record the current state schema information and a sequential cache of field names.
385/// As you traverse the user input JSON, this will change.
386/// It will record a superset of all user input schemas.
387#[derive(Default)]
388pub struct SchemaInfo {
389    /// schema info
390    pub schema: Vec<ColumnMetadata>,
391    /// index of the column name
392    pub index: HashMap<String, usize>,
393    /// Tables already looked up, keyed by their resolved suffix. Missing tables are cached as None.
394    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
566/// Converts VRL values to Greptime rows grouped by their ContextOpt.
567/// # Returns
568/// A HashMap where keys are `ContextOpt` and values are vectors of (row, table_suffix) pairs.
569/// Single object input produces one ContextOpt group with one row.
570/// Array input groups rows by their per-element ContextOpt values.
571///
572/// # Errors
573/// - `ArrayElementMustBeObject` if an array element is not an object
574pub(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        // Single object: extract ContextOpt and table_suffix
585        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        // Extract ContextOpt and table_suffix for this element
616        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
633/// `need_calc_ts` happens in two cases:
634/// 1. full greptime_identity
635/// 2. auto-transform without transformer
636///
637/// if transform is present in custom pipeline in v2 mode
638/// we dont need to calc ts again, nor do we need to check ts column name
639pub(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        // calculate timestamp value based on the channel
652        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    // skip ts column
659    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            // safe unwrap after type matched
703            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            // safe unwrap after type matched
715            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            // TODO(LFC): Default to JSON2 for auto-created tables.
787            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    // set time index column schema first
871    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
930/// Identity pipeline for Greptime
931/// This pipeline will convert the input JSON array to Greptime Rows
932/// params table is used to set the semantic type of the row key column to Tag
933/// 1. The pipeline will add a default timestamp column to the schema
934/// 2. The pipeline not resolve NULL value
935/// 3. The pipeline assumes that the json format is fixed
936/// 4. The pipeline will return an error if the same column datatype is mismatched
937/// 5. The pipeline will analyze the schema of each json record and merge them to get the final schema.
938pub 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
970/// Consumes the JSON object and consumes it into a single-level object.
971///
972/// The `max_nested_levels` parameter is used to limit how deep to flatten nested JSON objects.
973/// When the maximum level is reached, the remaining nested structure is serialized to a JSON
974/// string and stored at the current flattened key.
975pub 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        // it will use recursion to flatten the object.
981        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                    // Reached the maximum level; stringify the remaining object.
1028                    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            // Arrays are stringified to ensure no JSON column types in the result.
1044            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            // Other leaf types are inserted as-is.
1050            _ => {
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            &params,
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            // Basic case.
1357            (
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            // Test the case where the object has more than 3 nested levels.
1394            (
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, &params, 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        // Case 1: skip_error=true, mixed valid/invalid elements
1475        {
1476            let schema_info = &mut SchemaInfo::default();
1477            let input_array = vec![
1478                // Valid object
1479                serde_json::json!({"name": "Alice", "age": 25}).into(),
1480                // Invalid element (string)
1481                VrlValue::Bytes("invalid_string".into()),
1482                // Valid object
1483                serde_json::json!({"name": "Bob", "age": 30}).into(),
1484                // Invalid element (number)
1485                VrlValue::Integer(42),
1486                // Valid object
1487                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                &params,
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            // Should succeed and only process valid objects
1511            assert!(result.is_ok());
1512            let rows_by_context = result.unwrap();
1513            // Count total rows across all ContextOpt groups
1514            let total_rows: usize = rows_by_context.values().map(|v| v.len()).sum();
1515            assert_eq!(total_rows, 3); // Only 3 valid objects
1516        }
1517
1518        // Case 2: skip_error=false, invalid elements present
1519        {
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()), // This should cause error
1524            ];
1525
1526            let params = GreptimePipelineParams::default(); // skip_error = false
1527
1528            let pipeline_ctx = PipelineContext::new(
1529                &PipelineDefinition::GreptimeIdentityPipeline(None),
1530                &params,
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            // Should fail with ArrayElementMustBeObject error
1544            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 that values_to_rows correctly groups rows by per-element ContextOpt
1551    #[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        // Create array with elements having different TTL values (ContextOpt)
1557        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            &params,
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        // Should have 2 different ContextOpt groups (1h TTL and 24h TTL)
1583        assert_eq!(rows_by_context.len(), 2);
1584
1585        // Count rows per group
1586        let total_rows: usize = rows_by_context.values().map(|v| v.len()).sum();
1587        assert_eq!(total_rows, 3);
1588
1589        // Verify that rows are correctly grouped by TTL
1590        let mut ttl_1h_count = 0;
1591        let mut ttl_24h_count = 0;
1592        for rows in rows_by_context.values() {
1593            // ContextOpt doesn't expose ttl directly, but we can count by group size
1594            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); // Alice and Bob with 1h TTL
1601        assert_eq!(ttl_24h_count, 1); // Charlie with 24h TTL
1602    }
1603}