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(
745                String::from_utf8_lossy(&v).into_owned(),
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                inverted_index: false,
1105            }],
1106            None,
1107        )
1108        .unwrap();
1109        let mut schema_info = SchemaInfo::default();
1110        schema_info.set_table(Some(table(
1111            "events",
1112            int_settings,
1113            serde_json::json!({"age": 42}),
1114        )));
1115        let string_settings = JsonSettings::try_new(
1116            vec![JsonTypeHint {
1117                path: vec!["age".to_string()],
1118                data_type: ConcreteDataType::string_datatype(),
1119                inverted_index: false,
1120            }],
1121            None,
1122        )
1123        .unwrap();
1124        schema_info.set_table_for_suffix(
1125            "_mobile".to_string(),
1126            Some(table(
1127                "events_mobile",
1128                string_settings,
1129                serde_json::json!({"age": "42"}),
1130            )),
1131        );
1132
1133        let pipeline = crate::parse(&crate::Content::Yaml(
1134            r#"
1135transform:
1136  - field: source, payload
1137    type: json2
1138table_suffix: _${device}
1139"#,
1140        ))
1141        .unwrap();
1142        let (pipeline, _, pipeline_definition, pipeline_params) = crate::setup_pipeline!(pipeline);
1143        let pipeline_context =
1144            PipelineContext::new(&pipeline_definition, &pipeline_params, Channel::Unknown);
1145
1146        let error = pipeline
1147            .exec_mut(
1148                serde_json::json!({"source": {"age": "42"}}).into(),
1149                &pipeline_context,
1150                &mut schema_info,
1151            )
1152            .unwrap_err();
1153        assert!(
1154            error.to_string().contains("does not match JSON2 type hint"),
1155            "{error:?}"
1156        );
1157
1158        let mut rows = pipeline
1159            .exec_mut(
1160                serde_json::json!({"source": {"age": "42"}, "device": "mobile"}).into(),
1161                &pipeline_context,
1162                &mut schema_info,
1163            )
1164            .unwrap()
1165            .into_transformed()
1166            .unwrap();
1167        let (row, table_suffix) = rows.swap_remove(0);
1168        assert_eq!(table_suffix.as_deref(), Some("_mobile"));
1169        assert!(matches!(
1170            &row.values[0].value_data,
1171            Some(ValueData::JsonValue(_))
1172        ));
1173    }
1174
1175    #[test]
1176    fn test_identify_pipeline() {
1177        let params = GreptimePipelineParams::default();
1178        let pipeline_ctx = PipelineContext::new(
1179            &PipelineDefinition::GreptimeIdentityPipeline(None),
1180            &params,
1181            Channel::Unknown,
1182        );
1183        {
1184            let array = [
1185                serde_json::json!({
1186                    "woshinull": null,
1187                    "name": "Alice",
1188                    "age": 20,
1189                    "is_student": true,
1190                    "score": 99.5,
1191                    "hobbies": "reading",
1192                    "address": "Beijing",
1193                }),
1194                serde_json::json!({
1195                    "name": "Bob",
1196                    "age": 21,
1197                    "is_student": false,
1198                    "score": "88.5",
1199                    "hobbies": "swimming",
1200                    "address": "Shanghai",
1201                    "gaga": "gaga"
1202                }),
1203            ];
1204            let array = array.iter().map(|v| v.into()).collect();
1205            let rows = identity_pipeline(array, None, &pipeline_ctx);
1206            assert!(rows.is_err());
1207            assert_eq!(
1208                rows.err().unwrap().to_string(),
1209                "Column datatype mismatch. For column: score, expected datatype: Float64, actual datatype: String".to_string(),
1210            );
1211        }
1212        {
1213            let array = [
1214                serde_json::json!({
1215                    "woshinull": null,
1216                    "name": "Alice",
1217                    "age": 20,
1218                    "is_student": true,
1219                    "score": 99.5,
1220                    "hobbies": "reading",
1221                    "address": "Beijing",
1222                }),
1223                serde_json::json!({
1224                    "name": "Bob",
1225                    "age": 21,
1226                    "is_student": false,
1227                    "score": 88,
1228                    "hobbies": "swimming",
1229                    "address": "Shanghai",
1230                    "gaga": "gaga"
1231                }),
1232            ];
1233            let array = array.iter().map(|v| v.into()).collect();
1234            let rows = identity_pipeline(array, None, &pipeline_ctx);
1235            assert!(rows.is_err());
1236            assert_eq!(
1237                rows.err().unwrap().to_string(),
1238                "Column datatype mismatch. For column: score, expected datatype: Float64, actual datatype: Int64".to_string(),
1239            );
1240        }
1241        {
1242            let array = [
1243                serde_json::json!({
1244                    "woshinull": null,
1245                    "name": "Alice",
1246                    "age": 20,
1247                    "is_student": true,
1248                    "score": 99.5,
1249                    "hobbies": "reading",
1250                    "address": "Beijing",
1251                }),
1252                serde_json::json!({
1253                    "name": "Bob",
1254                    "age": 21,
1255                    "is_student": false,
1256                    "score": 88.5,
1257                    "hobbies": "swimming",
1258                    "address": "Shanghai",
1259                    "gaga": "gaga"
1260                }),
1261            ];
1262            let array = array.iter().map(|v| v.into()).collect();
1263            let rows = identity_pipeline(array, None, &pipeline_ctx);
1264            assert!(rows.is_ok());
1265            let mut rows = rows.unwrap();
1266            assert!(rows.len() == 1);
1267            let rows = rows.remove(&ContextOpt::default()).unwrap();
1268            assert_eq!(rows.schema.len(), 8);
1269            assert_eq!(rows.rows.len(), 2);
1270            assert_eq!(8, rows.rows[0].values.len());
1271            assert_eq!(8, rows.rows[1].values.len());
1272        }
1273        {
1274            let array = [
1275                serde_json::json!({
1276                    "woshinull": null,
1277                    "name": "Alice",
1278                    "age": 20,
1279                    "is_student": true,
1280                    "score": 99.5,
1281                    "hobbies": "reading",
1282                    "address": "Beijing",
1283                }),
1284                serde_json::json!({
1285                    "name": "Bob",
1286                    "age": 21,
1287                    "is_student": false,
1288                    "score": 88.5,
1289                    "hobbies": "swimming",
1290                    "address": "Shanghai",
1291                    "gaga": "gaga"
1292                }),
1293            ];
1294            let tag_column_names = ["name".to_string(), "address".to_string()];
1295
1296            let rows = identity_pipeline_inner(
1297                array.iter().map(|v| v.into()).collect(),
1298                &pipeline_ctx,
1299                pipeline_ctx.pipeline_param.max_nested_levels(),
1300            )
1301            .map(|(mut schema, mut rows)| {
1302                for name in tag_column_names {
1303                    if let Some(index) = schema.index.get(&name) {
1304                        schema.schema[*index].semantic_type = SemanticType::Tag;
1305                    }
1306                }
1307
1308                assert!(rows.len() == 1);
1309                let rows = rows.remove(&ContextOpt::default()).unwrap();
1310
1311                Rows {
1312                    schema: schema.column_schemas().unwrap(),
1313                    rows,
1314                }
1315            });
1316
1317            assert!(rows.is_ok());
1318            let rows = rows.unwrap();
1319            assert_eq!(rows.schema.len(), 8);
1320            assert_eq!(rows.rows.len(), 2);
1321            assert_eq!(8, rows.rows[0].values.len());
1322            assert_eq!(8, rows.rows[1].values.len());
1323            assert_eq!(
1324                rows.schema
1325                    .iter()
1326                    .find(|x| x.column_name == "name")
1327                    .unwrap()
1328                    .semantic_type,
1329                SemanticType::Tag as i32
1330            );
1331            assert_eq!(
1332                rows.schema
1333                    .iter()
1334                    .find(|x| x.column_name == "address")
1335                    .unwrap()
1336                    .semantic_type,
1337                SemanticType::Tag as i32
1338            );
1339            assert_eq!(
1340                rows.schema
1341                    .iter()
1342                    .filter(|x| x.semantic_type == SemanticType::Tag as i32)
1343                    .count(),
1344                2
1345            );
1346        }
1347    }
1348
1349    #[test]
1350    fn test_flatten() {
1351        let test_cases = vec![
1352            // Basic case.
1353            (
1354                serde_json::json!(
1355                    {
1356                        "a": {
1357                            "b": {
1358                                "c": [1, 2, 3]
1359                            }
1360                        },
1361                        "d": [
1362                            "foo",
1363                            "bar"
1364                        ],
1365                        "e": {
1366                            "f": [7, 8, 9],
1367                            "g": {
1368                                "h": 123,
1369                                "i": "hello",
1370                                "j": {
1371                                    "k": true
1372                                }
1373                            }
1374                        }
1375                    }
1376                ),
1377                10,
1378                Some(serde_json::json!(
1379                    {
1380                        "a.b.c": "[1,2,3]",
1381                        "d": "[\"foo\",\"bar\"]",
1382                        "e.f": "[7,8,9]",
1383                        "e.g.h": 123,
1384                        "e.g.i": "hello",
1385                        "e.g.j.k": true
1386                    }
1387                )),
1388            ),
1389            // Test the case where the object has more than 3 nested levels.
1390            (
1391                serde_json::json!(
1392                    {
1393                        "a": {
1394                            "b": {
1395                                "c": {
1396                                    "d": [1, 2, 3]
1397                                }
1398                            }
1399                        },
1400                        "e": [
1401                            "foo",
1402                            "bar"
1403                        ]
1404                    }
1405                ),
1406                3,
1407                Some(serde_json::json!(
1408                    {
1409                        "a.b.c": "{\"d\":[1,2,3]}",
1410                        "e": "[\"foo\",\"bar\"]"
1411                    }
1412                )),
1413            ),
1414        ];
1415
1416        for (input, max_depth, expected) in test_cases {
1417            let input = input.into();
1418            let expected = expected.map(|e| e.into());
1419
1420            let flattened_object = flatten_object(input, max_depth).ok();
1421            assert_eq!(flattened_object, expected);
1422        }
1423    }
1424
1425    #[test]
1426    fn test_identity_pipeline_skip_error_flattens_valid_rows() {
1427        let params = GreptimePipelineParams::from_map(ahash::HashMap::from_iter([(
1428            "skip_error".to_string(),
1429            "true".to_string(),
1430        )]));
1431        let pipeline_def = PipelineDefinition::GreptimeIdentityPipeline(None);
1432        let pipeline_ctx = PipelineContext::new(&pipeline_def, &params, Channel::Unknown);
1433        let array = vec![
1434            serde_json::json!({
1435                "service": "frontend",
1436                "nested": {
1437                    "status": 200,
1438                    "path": "/v1/ingest"
1439                },
1440                "labels": ["pipeline", "identity"]
1441            })
1442            .into(),
1443            VrlValue::Bytes("invalid_string".into()),
1444            serde_json::json!({
1445                "service": "frontend",
1446                "nested": {
1447                    "status": 201,
1448                    "path": "/v1/ingest"
1449                },
1450                "labels": ["pipeline", "identity"]
1451            })
1452            .into(),
1453        ];
1454
1455        let mut rows_by_opt = identity_pipeline(array, None, &pipeline_ctx).unwrap();
1456        let rows = rows_by_opt.remove(&ContextOpt::default()).unwrap();
1457
1458        assert_eq!(rows.rows.len(), 2);
1459        assert_eq!(rows.schema.len(), rows.rows[0].values.len());
1460        assert!(rows.schema.iter().any(|s| s.column_name == "nested.status"));
1461        assert!(rows.schema.iter().any(|s| s.column_name == "nested.path"));
1462        assert!(rows.schema.iter().any(|s| s.column_name == "labels"));
1463    }
1464
1465    use ahash::HashMap as AHashMap;
1466    #[test]
1467    fn test_values_to_rows_skip_error_handling() {
1468        let table_suffix_template: Option<crate::tablesuffix::TableSuffixTemplate> = None;
1469
1470        // Case 1: skip_error=true, mixed valid/invalid elements
1471        {
1472            let schema_info = &mut SchemaInfo::default();
1473            let input_array = vec![
1474                // Valid object
1475                serde_json::json!({"name": "Alice", "age": 25}).into(),
1476                // Invalid element (string)
1477                VrlValue::Bytes("invalid_string".into()),
1478                // Valid object
1479                serde_json::json!({"name": "Bob", "age": 30}).into(),
1480                // Invalid element (number)
1481                VrlValue::Integer(42),
1482                // Valid object
1483                serde_json::json!({"name": "Charlie", "age": 35}).into(),
1484            ];
1485
1486            let params = GreptimePipelineParams::from_map(AHashMap::from_iter([(
1487                "skip_error".to_string(),
1488                "true".to_string(),
1489            )]));
1490
1491            let pipeline_ctx = PipelineContext::new(
1492                &PipelineDefinition::GreptimeIdentityPipeline(None),
1493                &params,
1494                Channel::Unknown,
1495            );
1496
1497            let result = values_to_rows(
1498                schema_info,
1499                VrlValue::Array(input_array),
1500                &pipeline_ctx,
1501                None,
1502                true,
1503                table_suffix_template.as_ref(),
1504            );
1505
1506            // Should succeed and only process valid objects
1507            assert!(result.is_ok());
1508            let rows_by_context = result.unwrap();
1509            // Count total rows across all ContextOpt groups
1510            let total_rows: usize = rows_by_context.values().map(|v| v.len()).sum();
1511            assert_eq!(total_rows, 3); // Only 3 valid objects
1512        }
1513
1514        // Case 2: skip_error=false, invalid elements present
1515        {
1516            let schema_info = &mut SchemaInfo::default();
1517            let input_array = vec![
1518                serde_json::json!({"name": "Alice", "age": 25}).into(),
1519                VrlValue::Bytes("invalid_string".into()), // This should cause error
1520            ];
1521
1522            let params = GreptimePipelineParams::default(); // skip_error = false
1523
1524            let pipeline_ctx = PipelineContext::new(
1525                &PipelineDefinition::GreptimeIdentityPipeline(None),
1526                &params,
1527                Channel::Unknown,
1528            );
1529
1530            let result = values_to_rows(
1531                schema_info,
1532                VrlValue::Array(input_array),
1533                &pipeline_ctx,
1534                None,
1535                true,
1536                table_suffix_template.as_ref(),
1537            );
1538
1539            // Should fail with ArrayElementMustBeObject error
1540            assert!(result.is_err());
1541            let error_msg = result.unwrap_err().to_string();
1542            assert!(error_msg.contains("Array element at index 1 must be an object for one-to-many transformation, got string"));
1543        }
1544    }
1545
1546    /// Test that values_to_rows correctly groups rows by per-element ContextOpt
1547    #[test]
1548    fn test_values_to_rows_per_element_context_opt() {
1549        let table_suffix_template: Option<crate::tablesuffix::TableSuffixTemplate> = None;
1550        let schema_info = &mut SchemaInfo::default();
1551
1552        // Create array with elements having different TTL values (ContextOpt)
1553        let input_array = vec![
1554            serde_json::json!({"name": "Alice", "greptime_ttl": "1h"}).into(),
1555            serde_json::json!({"name": "Bob", "greptime_ttl": "1h"}).into(),
1556            serde_json::json!({"name": "Charlie", "greptime_ttl": "24h"}).into(),
1557        ];
1558
1559        let params = GreptimePipelineParams::default();
1560        let pipeline_ctx = PipelineContext::new(
1561            &PipelineDefinition::GreptimeIdentityPipeline(None),
1562            &params,
1563            Channel::Unknown,
1564        );
1565
1566        let result = values_to_rows(
1567            schema_info,
1568            VrlValue::Array(input_array),
1569            &pipeline_ctx,
1570            None,
1571            true,
1572            table_suffix_template.as_ref(),
1573        );
1574
1575        assert!(result.is_ok());
1576        let rows_by_context = result.unwrap();
1577
1578        // Should have 2 different ContextOpt groups (1h TTL and 24h TTL)
1579        assert_eq!(rows_by_context.len(), 2);
1580
1581        // Count rows per group
1582        let total_rows: usize = rows_by_context.values().map(|v| v.len()).sum();
1583        assert_eq!(total_rows, 3);
1584
1585        // Verify that rows are correctly grouped by TTL
1586        let mut ttl_1h_count = 0;
1587        let mut ttl_24h_count = 0;
1588        for rows in rows_by_context.values() {
1589            // ContextOpt doesn't expose ttl directly, but we can count by group size
1590            if rows.len() == 2 {
1591                ttl_1h_count = rows.len();
1592            } else if rows.len() == 1 {
1593                ttl_24h_count = rows.len();
1594            }
1595        }
1596        assert_eq!(ttl_1h_count, 2); // Alice and Bob with 1h TTL
1597        assert_eq!(ttl_24h_count, 1); // Charlie with 24h TTL
1598    }
1599}