Skip to main content

risingwave_connector/sink/encoder/
avro.rs

1// Copyright 2023 RisingWave Labs
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
15use std::collections::HashMap;
16use std::sync::Arc;
17
18use apache_avro::schema::{Name, RecordSchema, Schema as AvroSchema};
19use apache_avro::types::{Record, Value};
20use risingwave_common::array::VECTOR_AS_LIST_TYPE;
21use risingwave_common::catalog::Schema;
22use risingwave_common::row::Row;
23use risingwave_common::types::{DataType, DatumRef, ListType, ScalarRefImpl, StructType};
24use risingwave_common::util::iter_util::{ZipEqDebug, ZipEqFast};
25use risingwave_connector_codec::decoder::utils::rust_decimal_to_scaled_bigint;
26use thiserror_ext::AsReport;
27
28use super::{FieldEncodeError, Result as SinkResult, RowEncoder, SerTo};
29
30type Result<T> = std::result::Result<T, FieldEncodeError>;
31struct NamesRef(HashMap<Name, AvroSchema>);
32
33pub struct AvroEncoder {
34    schema: Schema,
35    col_indices: Option<Vec<usize>>,
36    avro_schema: Arc<AvroSchema>,
37    refs: NamesRef,
38    header: AvroHeader,
39}
40
41#[derive(Debug, Clone, Copy)]
42pub enum AvroHeader {
43    None,
44    /// <https://avro.apache.org/docs/1.11.1/specification/#single-object-encoding>
45    ///
46    /// * C3 01
47    /// * 8-byte little-endian CRC-64-AVRO fingerprint
48    SingleObject,
49    /// <https://avro.apache.org/docs/1.11.1/specification/#object-container-files>
50    ///
51    /// * 4F 62 6A 01
52    /// * schema
53    /// * 16-byte random sync marker
54    ContainerFile,
55    /// <https://docs.confluent.io/platform/7.5/schema-registry/fundamentals/serdes-develop/index.html#messages-wire-format>
56    ///
57    /// * 00
58    /// * 4-byte big-endian schema ID
59    ConfluentSchemaRegistry(i32),
60    /// <https://github.com/awslabs/aws-glue-schema-registry/blob/v1.1.20/common/src/main/java/com/amazonaws/services/schemaregistry/utils/AWSSchemaRegistryConstants.java#L59-L61>
61    ///
62    /// * 03
63    /// * 00
64    /// * 16-byte UUID identifying a specific schema version
65    GlueSchemaRegistry(uuid::Uuid),
66}
67
68impl AvroEncoder {
69    pub fn new(
70        schema: Schema,
71        col_indices: Option<Vec<usize>>,
72        avro_schema: Arc<AvroSchema>,
73        header: AvroHeader,
74    ) -> SinkResult<Self> {
75        let refs = NamesRef::new(&avro_schema)?;
76        match &col_indices {
77            Some(col_indices) => validate_fields(
78                col_indices.iter().map(|idx| {
79                    let f = &schema[*idx];
80                    (f.name.as_str(), &f.data_type)
81                }),
82                &avro_schema,
83                &refs,
84            )?,
85            None => validate_fields(
86                schema
87                    .fields
88                    .iter()
89                    .map(|f| (f.name.as_str(), &f.data_type)),
90                &avro_schema,
91                &refs,
92            )?,
93        };
94
95        Ok(Self {
96            schema,
97            col_indices,
98            avro_schema,
99            refs,
100            header,
101        })
102    }
103}
104
105impl NamesRef {
106    fn new(root: &AvroSchema) -> std::result::Result<Self, apache_avro::Error> {
107        let resolved = apache_avro::schema::ResolvedSchema::try_from(root)?;
108        let refs = resolved
109            .get_names()
110            .iter()
111            .map(|(k, v)| (k.to_owned(), (*v).to_owned()))
112            .collect();
113        Ok(Self(refs))
114    }
115
116    fn lookup<'a>(&'a self, avro: &'a AvroSchema) -> &'a AvroSchema {
117        match avro {
118            AvroSchema::Ref { name } => &self.0[name],
119            _ => avro,
120        }
121    }
122}
123
124pub struct AvroEncoded {
125    value: Value,
126    schema: Arc<AvroSchema>,
127    header: AvroHeader,
128}
129
130impl RowEncoder for AvroEncoder {
131    type Output = AvroEncoded;
132
133    fn schema(&self) -> &Schema {
134        &self.schema
135    }
136
137    fn col_indices(&self) -> Option<&[usize]> {
138        self.col_indices.as_deref()
139    }
140
141    fn encode_cols(
142        &self,
143        row: impl Row,
144        col_indices: impl Iterator<Item = usize>,
145    ) -> SinkResult<Self::Output> {
146        let record = encode_fields(
147            col_indices.map(|idx| {
148                let f = &self.schema[idx];
149                ((f.name.as_str(), &f.data_type), row.datum_at(idx))
150            }),
151            &self.avro_schema,
152            &self.refs,
153        )?;
154        Ok(AvroEncoded {
155            value: record.into(),
156            schema: self.avro_schema.clone(),
157            header: self.header,
158        })
159    }
160}
161
162impl SerTo<Vec<u8>> for AvroEncoded {
163    fn ser_to(self) -> SinkResult<Vec<u8>> {
164        use bytes::BufMut as _;
165
166        let header = match self.header {
167            AvroHeader::ConfluentSchemaRegistry(schema_id) => {
168                let mut buf = Vec::with_capacity(1 + 4);
169                buf.put_u8(0);
170                buf.put_i32(schema_id);
171                buf
172            }
173            AvroHeader::GlueSchemaRegistry(schema_version_id) => {
174                let mut buf = Vec::with_capacity(1 + 1 + 16);
175                buf.put_u8(3);
176                buf.put_u8(0);
177                buf.put_slice(schema_version_id.as_bytes());
178                buf
179            }
180            AvroHeader::None | AvroHeader::SingleObject | AvroHeader::ContainerFile => {
181                return Err(crate::sink::SinkError::Encode(format!(
182                    "{:?} unsupported yet",
183                    self.header
184                )));
185            }
186        };
187
188        let raw = apache_avro::to_avro_datum(&self.schema, self.value)
189            .map_err(|e| crate::sink::SinkError::Encode(e.to_report_string()))?;
190        let mut buf = Vec::with_capacity(header.len() + raw.len());
191        buf.put_slice(&header);
192        buf.put_slice(&raw);
193
194        Ok(buf)
195    }
196}
197
198enum OptIdx {
199    /// `T`
200    NotUnion,
201    /// `[T]`
202    Single,
203    /// `[null, T]`
204    NullLeft,
205    /// `[T, null]`
206    NullRight,
207}
208
209/// A trait that assists code reuse between `validate` and `encode`.
210/// * For `validate`, the inputs are (RisingWave type, ProtoBuf type).
211/// * For `encode`, the inputs are (RisingWave type, RisingWave data, ProtoBuf type).
212///
213/// Thus we impl [`MaybeData`] for both `()` and [`DatumRef`].
214trait MaybeData: std::fmt::Debug {
215    type Out;
216
217    fn on_base(self, f: impl FnOnce(ScalarRefImpl<'_>) -> Result<Value>) -> Result<Self::Out>;
218
219    /// Switch to `RecordSchema` after #12562
220    fn on_struct(self, st: &StructType, avro: &AvroSchema, refs: &NamesRef) -> Result<Self::Out>;
221
222    fn on_list(self, lt: &ListType, avro: &AvroSchema, refs: &NamesRef) -> Result<Self::Out>;
223
224    fn on_map(
225        self,
226        value_type: &DataType,
227        avro_value_schema: &AvroSchema,
228        refs: &NamesRef,
229    ) -> Result<Self::Out>;
230
231    fn handle_nullable_union(out: Self::Out, opt_idx: OptIdx) -> Result<Self::Out>;
232}
233
234impl MaybeData for () {
235    type Out = ();
236
237    fn on_base(self, _: impl FnOnce(ScalarRefImpl<'_>) -> Result<Value>) -> Result<Self::Out> {
238        Ok(self)
239    }
240
241    fn on_struct(self, st: &StructType, avro: &AvroSchema, refs: &NamesRef) -> Result<Self::Out> {
242        validate_fields(st.iter(), avro, refs)
243    }
244
245    fn on_list(self, lt: &ListType, avro: &AvroSchema, refs: &NamesRef) -> Result<Self::Out> {
246        on_field(lt.elem(), (), avro, refs)
247    }
248
249    fn on_map(self, elem: &DataType, avro: &AvroSchema, refs: &NamesRef) -> Result<Self::Out> {
250        on_field(elem, (), avro, refs)
251    }
252
253    fn handle_nullable_union(out: Self::Out, _: OptIdx) -> Result<Self::Out> {
254        Ok(out)
255    }
256}
257
258impl MaybeData for DatumRef<'_> {
259    type Out = Value;
260
261    fn on_base(self, f: impl FnOnce(ScalarRefImpl<'_>) -> Result<Value>) -> Result<Self::Out> {
262        match self {
263            Some(s) => f(s),
264            None => Ok(Value::Null),
265        }
266    }
267
268    fn on_struct(self, st: &StructType, avro: &AvroSchema, refs: &NamesRef) -> Result<Self::Out> {
269        let d = match self {
270            Some(s) => s.into_struct(),
271            None => return Ok(Value::Null),
272        };
273        let record = encode_fields(st.iter().zip_eq_debug(d.iter_fields_ref()), avro, refs)?;
274        Ok(record.into())
275    }
276
277    fn on_list(self, lt: &ListType, avro: &AvroSchema, refs: &NamesRef) -> Result<Self::Out> {
278        let d = match self {
279            Some(s) => s.into_list(),
280            None => return Ok(Value::Null),
281        };
282        let vs = d
283            .iter()
284            .map(|d| on_field(lt.elem(), d, avro, refs))
285            .try_collect()?;
286        Ok(Value::Array(vs))
287    }
288
289    fn on_map(self, elem: &DataType, avro: &AvroSchema, refs: &NamesRef) -> Result<Self::Out> {
290        let d = match self {
291            Some(s) => s.into_map(),
292            None => return Ok(Value::Null),
293        };
294        let vs = d
295            .iter()
296            .map(|(k, v)| {
297                let k = k.into_utf8().to_owned();
298                let v = on_field(elem, v, avro, refs)?;
299                Ok((k, v))
300            })
301            .try_collect()?;
302        Ok(Value::Map(vs))
303    }
304
305    fn handle_nullable_union(out: Self::Out, opt_idx: OptIdx) -> Result<Self::Out> {
306        use OptIdx::*;
307
308        match out == Value::Null {
309            true => {
310                let ni = match opt_idx {
311                    NotUnion | Single => {
312                        return Err(FieldEncodeError::new("found null but required"));
313                    }
314                    NullLeft => 0,
315                    NullRight => 1,
316                };
317                Ok(Value::Union(ni, out.into()))
318            }
319            false => {
320                let vi = match opt_idx {
321                    NotUnion => return Ok(out),
322                    NullLeft => 1,
323                    Single | NullRight => 0,
324                };
325                Ok(Value::Union(vi, out.into()))
326            }
327        }
328    }
329}
330
331fn validate_fields<'rw>(
332    rw_fields: impl Iterator<Item = (&'rw str, &'rw DataType)>,
333    avro: &AvroSchema,
334    refs: &NamesRef,
335) -> Result<()> {
336    let avro = refs.lookup(avro);
337    let AvroSchema::Record(RecordSchema { fields, lookup, .. }) = avro else {
338        return Err(FieldEncodeError::new(format!(
339            "expect avro record but got {}",
340            avro.canonical_form(),
341        )));
342    };
343    let mut present = vec![false; fields.len()];
344    for (name, t) in rw_fields {
345        let Some(&idx) = lookup.get(name) else {
346            return Err(FieldEncodeError::new("field not in avro").with_name(name));
347        };
348        present[idx] = true;
349        let avro_field = &fields[idx];
350        on_field(t, (), &avro_field.schema, refs).map_err(|e| e.with_name(name))?;
351    }
352    for (p, avro_field) in present.into_iter().zip_eq_fast(fields) {
353        if p {
354            continue;
355        }
356        if !avro_field.is_nullable() {
357            return Err(
358                FieldEncodeError::new("field not present but required").with_name(&avro_field.name)
359            );
360        }
361    }
362    Ok(())
363}
364
365fn encode_fields<'avro, 'rw>(
366    fields_with_datums: impl Iterator<Item = ((&'rw str, &'rw DataType), DatumRef<'rw>)>,
367    schema: &'avro AvroSchema,
368    refs: &'avro NamesRef,
369) -> Result<Record<'avro>> {
370    let schema = refs.lookup(schema);
371    let mut record = Record::new(schema).unwrap();
372    let AvroSchema::Record(RecordSchema { fields, lookup, .. }) = schema else {
373        unreachable!()
374    };
375    let mut present = vec![false; fields.len()];
376    for ((name, t), d) in fields_with_datums {
377        let idx = lookup[name];
378        present[idx] = true;
379        let avro_field = &fields[idx];
380        let value = on_field(t, d, &avro_field.schema, refs).map_err(|e| e.with_name(name))?;
381        record.put(name, value);
382    }
383    // Unfortunately, the upstream `apache_avro` does not handle missing fields as nullable correctly.
384    // The correct encoding is `Value::Union(null_index, Value::Null)` but it simply writes `Value::Null`.
385    // See [`tests::test_encode_avro_lib_bug`].
386    for (p, avro_field) in present.into_iter().zip_eq_fast(fields) {
387        if p {
388            continue;
389        }
390        let AvroSchema::Union(u) = &avro_field.schema else {
391            unreachable!()
392        };
393        // We could have saved null index of each field during [`validate_fields`] to avoid repeated lookup.
394        // But in most cases it is the 0th.
395        // Alternatively, we can simplify by enforcing the best practice of `null at 0th`.
396        let ni = u
397            .variants()
398            .iter()
399            .position(|a| a == &AvroSchema::Null)
400            .unwrap();
401        record.put(
402            &avro_field.name,
403            Value::Union(ni.try_into().unwrap(), Value::Null.into()),
404        );
405    }
406    Ok(record)
407}
408
409/// Handles both `validate` (without actual data) and `encode`.
410/// See [`MaybeData`] for more info.
411fn on_field<D: MaybeData>(
412    data_type: &DataType,
413    maybe: D,
414    expected: &AvroSchema,
415    refs: &NamesRef,
416) -> Result<D::Out> {
417    use risingwave_common::types::Interval;
418
419    let no_match_err = || {
420        Err(FieldEncodeError::new(format!(
421            "cannot encode {} column as {} field",
422            data_type,
423            expected.canonical_form()
424        )))
425    };
426
427    // For now, we only support optional single type, rather than general union.
428    // For example, how do we encode int16 into avro `["int", "long"]`?
429    let (inner, opt_idx) = match expected {
430        AvroSchema::Union(union) => match union.variants() {
431            [] => return no_match_err(),
432            [one] => (one, OptIdx::Single),
433            [AvroSchema::Null, r] => (r, OptIdx::NullLeft),
434            [l, AvroSchema::Null] => (l, OptIdx::NullRight),
435            _ => return no_match_err(),
436        },
437        _ => (expected, OptIdx::NotUnion),
438    };
439
440    let inner = refs.lookup(inner);
441
442    let value = match &data_type {
443        // Group A: perfect match between RisingWave types and Avro types
444        DataType::Boolean => match inner {
445            AvroSchema::Boolean => maybe.on_base(|s| Ok(Value::Boolean(s.into_bool())))?,
446            _ => return no_match_err(),
447        },
448        DataType::Varchar => match inner {
449            AvroSchema::String => maybe.on_base(|s| Ok(Value::String(s.into_utf8().into())))?,
450
451            // Add enum support
452            AvroSchema::Enum(enum_schema) => maybe.on_base(|s| {
453                let str_value = s.into_utf8();
454
455                if let Some(position) = enum_schema
456                    .symbols
457                    .iter()
458                    .position(|symbol| symbol == str_value)
459                {
460                    Ok(Value::Enum(position as u32, str_value.to_owned()))
461                } else {
462                    Err(FieldEncodeError::new(format!(
463                        "Value '{}' is not a valid enum symbol. Valid symbols are: {:?}",
464                        str_value, enum_schema.symbols
465                    )))
466                }
467            })?,
468
469            _ => return no_match_err(),
470        },
471        DataType::Bytea => match inner {
472            AvroSchema::Bytes => maybe.on_base(|s| Ok(Value::Bytes(s.into_bytea().into())))?,
473            _ => return no_match_err(),
474        },
475        DataType::Float32 => match inner {
476            AvroSchema::Float => maybe.on_base(|s| Ok(Value::Float(s.into_float32().into())))?,
477            _ => return no_match_err(),
478        },
479        DataType::Float64 => match inner {
480            AvroSchema::Double => maybe.on_base(|s| Ok(Value::Double(s.into_float64().into())))?,
481            _ => return no_match_err(),
482        },
483        DataType::Int32 => match inner {
484            AvroSchema::Int => maybe.on_base(|s| Ok(Value::Int(s.into_int32())))?,
485            _ => return no_match_err(),
486        },
487        DataType::Int64 => match inner {
488            AvroSchema::Long => maybe.on_base(|s| Ok(Value::Long(s.into_int64())))?,
489            _ => return no_match_err(),
490        },
491        DataType::Serial => match inner {
492            AvroSchema::Long => maybe.on_base(|s| Ok(Value::Long(s.into_serial().into_inner())))?,
493            _ => return no_match_err(),
494        },
495        DataType::Struct(st) => match inner {
496            AvroSchema::Record { .. } => maybe.on_struct(st, inner, refs)?,
497            _ => return no_match_err(),
498        },
499        DataType::List(lt) => match inner {
500            AvroSchema::Array(avro_arr) => maybe.on_list(lt, &avro_arr.items, refs)?,
501            _ => return no_match_err(),
502        },
503        DataType::Map(m) => {
504            if *m.key() != DataType::Varchar {
505                return no_match_err();
506            }
507            match inner {
508                AvroSchema::Map(avro_map) => maybe.on_map(m.value(), &avro_map.types, refs)?,
509                _ => return no_match_err(),
510            }
511        }
512
513        // Group B: match between RisingWave types and Avro logical types
514        DataType::Timestamptz => match inner {
515            AvroSchema::TimestampMicros => maybe.on_base(|s| {
516                Ok(Value::TimestampMicros(
517                    s.into_timestamptz().timestamp_micros(),
518                ))
519            })?,
520            AvroSchema::TimestampMillis => maybe.on_base(|s| {
521                Ok(Value::TimestampMillis(
522                    s.into_timestamptz().timestamp_millis(),
523                ))
524            })?,
525            _ => return no_match_err(),
526        },
527        DataType::Timestamp => return no_match_err(),
528        DataType::Date => match inner {
529            AvroSchema::Date => {
530                maybe.on_base(|s| Ok(Value::Date(s.into_date().get_nums_days_unix_epoch())))?
531            }
532            _ => return no_match_err(),
533        },
534        DataType::Time => match inner {
535            AvroSchema::TimeMicros => {
536                maybe.on_base(|s| Ok(Value::TimeMicros(Interval::from(s.into_time()).usecs())))?
537            }
538            AvroSchema::TimeMillis => maybe.on_base(|s| {
539                Ok(Value::TimeMillis(
540                    (Interval::from(s.into_time()).usecs() / 1000)
541                        .try_into()
542                        .unwrap(),
543                ))
544            })?,
545            _ => return no_match_err(),
546        },
547        DataType::Interval => match inner {
548            AvroSchema::Duration => maybe.on_base(|s| {
549                use apache_avro::{Days, Duration, Millis, Months};
550                let iv = s.into_interval();
551
552                let overflow = |_| FieldEncodeError::new(format!("{iv} overflows avro duration"));
553
554                Ok(Value::Duration(Duration::new(
555                    Months::new(iv.months().try_into().map_err(overflow)?),
556                    Days::new(iv.days().try_into().map_err(overflow)?),
557                    Millis::new((iv.usecs() / 1000).try_into().map_err(overflow)?),
558                )))
559            })?,
560            _ => return no_match_err(),
561        },
562        // Group C: experimental
563        DataType::Int16 => match inner {
564            AvroSchema::Int => maybe.on_base(|s| Ok(Value::Int(s.into_int16() as i32)))?,
565            _ => return no_match_err(),
566        },
567        DataType::Decimal => match inner {
568            AvroSchema::Decimal(decimal_schema) => {
569                maybe.on_base(|s| {
570                    match s.into_decimal() {
571                        risingwave_common::types::Decimal::Normalized(decimal) => {
572                            // convert to bigint with scale
573                            // rescale the rust_decimal to the scale of the avro decimal
574                            //
575                            // From bigdecimal::BigDecimal::with_scale:
576                            // If the new_scale is lower than the current value (indicating a larger
577                            // power of 10), digits will be dropped (as precision is lower)
578                            let signed_bigint_bytes =
579                                rust_decimal_to_scaled_bigint(decimal, decimal_schema.scale)
580                                    .map_err(FieldEncodeError::new)?;
581                            Ok(Value::Decimal(apache_avro::Decimal::from(
582                                &signed_bigint_bytes,
583                            )))
584                        }
585                        d @ risingwave_common::types::Decimal::NaN
586                        | d @ risingwave_common::types::Decimal::NegativeInf
587                        | d @ risingwave_common::types::Decimal::PositiveInf => {
588                            Err(FieldEncodeError::new(format!(
589                                "Avro Decimal does not support NaN or Inf, but got {}",
590                                d
591                            )))
592                        }
593                    }
594                })?
595            }
596            _ => return no_match_err(),
597        },
598        DataType::Jsonb => match inner {
599            AvroSchema::String => {
600                maybe.on_base(|s| Ok(Value::String(s.into_jsonb().to_string())))?
601            }
602            _ => return no_match_err(),
603        },
604        DataType::Variant => {
605            return no_match_err();
606        }
607        DataType::Vector(_) => match inner {
608            AvroSchema::Array(avro_arr) => {
609                maybe.on_list(&VECTOR_AS_LIST_TYPE, &avro_arr.items, refs)?
610            }
611            _ => return no_match_err(),
612        },
613        // Group D: unsupported
614        DataType::Int256 => {
615            return no_match_err();
616        }
617    };
618
619    D::handle_nullable_union(value, opt_idx)
620}
621
622#[cfg(test)]
623mod tests {
624    use std::collections::HashMap;
625    use std::str::FromStr;
626
627    use expect_test::expect;
628    use itertools::Itertools;
629    use risingwave_common::array::{ArrayBuilder, MapArrayBuilder};
630    use risingwave_common::catalog::Field;
631    use risingwave_common::row::OwnedRow;
632    use risingwave_common::types::{
633        Date, Datum, Interval, JsonbVal, ListValue, MapType, MapValue, Scalar, ScalarImpl,
634        StructValue, Time, Timestamptz, ToDatumRef,
635    };
636
637    use super::*;
638
639    #[track_caller]
640    fn test_ok(rw_type: &DataType, rw_datum: Datum, avro_type: &str, expected: Value) {
641        let avro_schema = AvroSchema::parse_str(avro_type).unwrap();
642        let refs = NamesRef::new(&avro_schema).unwrap();
643        let actual = on_field(rw_type, rw_datum.to_datum_ref(), &avro_schema, &refs).unwrap();
644        assert_eq!(actual, expected);
645    }
646
647    #[track_caller]
648    fn test_err<D: MaybeData>(t: &DataType, d: D, avro: &str, expected: &str)
649    where
650        D::Out: std::fmt::Debug,
651    {
652        let avro_schema = AvroSchema::parse_str(avro).unwrap();
653        let refs = NamesRef::new(&avro_schema).unwrap();
654        let err = on_field(t, d, &avro_schema, &refs).unwrap_err();
655        assert_eq!(err.to_string(), expected);
656    }
657
658    #[track_caller]
659    fn test_v2(rw_type: &str, rw_scalar: &str, avro_type: &str, expected: expect_test::Expect) {
660        let avro_schema = AvroSchema::parse_str(avro_type).unwrap();
661        let refs = NamesRef::new(&avro_schema).unwrap();
662        let rw_type = DataType::from_str(rw_type).unwrap();
663        let rw_datum = ScalarImpl::from_text_for_test(rw_scalar, &rw_type).unwrap();
664
665        if let Err(validate_err) = on_field(&rw_type, (), &avro_schema, &refs) {
666            expected.assert_debug_eq(&validate_err);
667            return;
668        }
669        let actual = on_field(&rw_type, Some(rw_datum).to_datum_ref(), &avro_schema, &refs);
670        match actual {
671            Ok(v) => expected.assert_eq(&print_avro_value(&v)),
672            Err(e) => expected.assert_debug_eq(&e),
673        }
674    }
675
676    fn print_avro_value(v: &Value) -> String {
677        match v {
678            Value::Map(m) => {
679                let mut res = "Map({".to_owned();
680                for (k, v) in m.iter().sorted_by_key(|x| x.0) {
681                    res.push_str(&format!("{}: {}, ", k, print_avro_value(v)));
682                }
683                res.push_str("})");
684                res
685            }
686            _ => format!("{v:?}"),
687        }
688    }
689
690    #[test]
691    fn test_encode_v2() {
692        test_v2(
693            "boolean",
694            "false",
695            r#""int""#,
696            expect![[r#"
697                FieldEncodeError {
698                    message: "cannot encode boolean column as \"int\" field",
699                    rev_path: [],
700                }
701            "#]],
702        );
703        test_v2("boolean", "true", r#""boolean""#, expect!["Boolean(true)"]);
704
705        test_v2(
706            "map(varchar,varchar)",
707            "{1:1,2:2,3:3}",
708            r#"{"type": "map","values": "string"}"#,
709            expect![[r#"Map({1: String("1"), 2: String("2"), 3: String("3"), })"#]],
710        );
711
712        test_v2(
713            "map(varchar,varchar)",
714            "{1:1,2:NULL,3:3}",
715            r#"{"type": "map","values": "string"}"#,
716            expect![[r#"
717                FieldEncodeError {
718                    message: "found null but required",
719                    rev_path: [],
720                }
721            "#]],
722        );
723
724        test_v2(
725            "map(varchar,varchar)",
726            "{1:1,2:NULL,3:3}",
727            r#"{"type": "map","values": ["null", "string"]}"#,
728            expect![[
729                r#"Map({1: Union(1, String("1")), 2: Union(0, Null), 3: Union(1, String("3")), })"#
730            ]],
731        );
732
733        test_v2(
734            "map(int,varchar)",
735            "{1:1,2:NULL,3:3}",
736            r#"{"type": "map","values": ["null", "string"]}"#,
737            expect![[r#"
738                FieldEncodeError {
739                    message: "cannot encode map(integer,character varying) column as {\"type\":\"map\",\"values\":[\"null\",\"string\"]} field",
740                    rev_path: [],
741                }
742            "#]],
743        );
744    }
745
746    #[test]
747    fn test_encode_avro_ok() {
748        test_ok(
749            &DataType::Boolean,
750            Some(ScalarImpl::Bool(false)),
751            r#""boolean""#,
752            Value::Boolean(false),
753        );
754
755        test_ok(
756            &DataType::Varchar,
757            Some(ScalarImpl::Utf8("RisingWave".into())),
758            r#""string""#,
759            Value::String("RisingWave".into()),
760        );
761
762        test_ok(
763            &DataType::Bytea,
764            Some(ScalarImpl::Bytea([0xbe, 0xef].into())),
765            r#""bytes""#,
766            Value::Bytes([0xbe, 0xef].into()),
767        );
768
769        test_ok(
770            &DataType::Float32,
771            Some(ScalarImpl::Float32(3.5f32.into())),
772            r#""float""#,
773            Value::Float(3.5f32),
774        );
775
776        test_ok(
777            &DataType::Float64,
778            Some(ScalarImpl::Float64(4.25f64.into())),
779            r#""double""#,
780            Value::Double(4.25f64),
781        );
782
783        test_ok(
784            &DataType::Int32,
785            Some(ScalarImpl::Int32(16)),
786            r#""int""#,
787            Value::Int(16),
788        );
789
790        test_ok(
791            &DataType::Int64,
792            Some(ScalarImpl::Int64(i64::MAX)),
793            r#""long""#,
794            Value::Long(i64::MAX),
795        );
796
797        test_ok(
798            &DataType::Serial,
799            Some(ScalarImpl::Serial(i64::MAX.into())),
800            r#""long""#,
801            Value::Long(i64::MAX),
802        );
803
804        let tstz = "2018-01-26T18:30:09.453Z".parse().unwrap();
805        test_ok(
806            &DataType::Timestamptz,
807            Some(ScalarImpl::Timestamptz(tstz)),
808            r#"{"type": "long", "logicalType": "timestamp-micros"}"#,
809            Value::TimestampMicros(tstz.timestamp_micros()),
810        );
811        test_ok(
812            &DataType::Timestamptz,
813            Some(ScalarImpl::Timestamptz(tstz)),
814            r#"{"type": "long", "logicalType": "timestamp-millis"}"#,
815            Value::TimestampMillis(tstz.timestamp_millis()),
816        );
817
818        test_ok(
819            &DataType::Date,
820            Some(ScalarImpl::Date(Date::from_ymd_uncheck(1970, 1, 2))),
821            r#"{"type": "int", "logicalType": "date"}"#,
822            Value::Date(1),
823        );
824
825        let tm = Time::from_num_seconds_from_midnight_uncheck(1000, 0);
826        test_ok(
827            &DataType::Time,
828            Some(ScalarImpl::Time(tm)),
829            r#"{"type": "long", "logicalType": "time-micros"}"#,
830            Value::TimeMicros(1000 * 1_000_000),
831        );
832        test_ok(
833            &DataType::Time,
834            Some(ScalarImpl::Time(tm)),
835            r#"{"type": "int", "logicalType": "time-millis"}"#,
836            Value::TimeMillis(1000 * 1000),
837        );
838
839        test_ok(
840            &DataType::Int16,
841            Some(ScalarImpl::Int16(i16::MAX)),
842            r#""int""#,
843            Value::Int(i16::MAX as i32),
844        );
845
846        test_ok(
847            &DataType::Int16,
848            Some(ScalarImpl::Int16(i16::MIN)),
849            r#""int""#,
850            Value::Int(i16::MIN as i32),
851        );
852
853        test_ok(
854            &DataType::Jsonb,
855            Some(ScalarImpl::Jsonb(
856                JsonbVal::from_str(r#"{"a": 1}"#).unwrap(),
857            )),
858            r#""string""#,
859            Value::String(r#"{"a": 1}"#.into()),
860        );
861
862        test_ok(
863            &DataType::Interval,
864            Some(ScalarImpl::Interval(Interval::from_month_day_usec(
865                13, 2, 1000000,
866            ))),
867            r#"{"type": "fixed", "name": "Duration", "size": 12, "logicalType": "duration"}"#,
868            Value::Duration(apache_avro::Duration::new(
869                apache_avro::Months::new(13),
870                apache_avro::Days::new(2),
871                apache_avro::Millis::new(1000),
872            )),
873        );
874
875        let mut inner_map_array_builder = MapArrayBuilder::with_type(
876            2,
877            DataType::Map(MapType::from_kv(DataType::Varchar, DataType::Int32)),
878        );
879        inner_map_array_builder.append(Some(
880            MapValue::try_from_kv(
881                ListValue::from_iter(["a", "b"]),
882                ListValue::from_iter([1, 2]),
883            )
884            .unwrap()
885            .as_scalar_ref(),
886        ));
887        inner_map_array_builder.append(Some(
888            MapValue::try_from_kv(
889                ListValue::from_iter(["c", "d"]),
890                ListValue::from_iter([3, 4]),
891            )
892            .unwrap()
893            .as_scalar_ref(),
894        ));
895        let inner_map_array = inner_map_array_builder.finish();
896        test_ok(
897            &DataType::Map(MapType::from_kv(
898                DataType::Varchar,
899                DataType::Map(MapType::from_kv(DataType::Varchar, DataType::Int32)),
900            )),
901            Some(ScalarImpl::Map(
902                MapValue::try_from_kv(
903                    ListValue::from_iter(["k1", "k2"]),
904                    ListValue::new(inner_map_array.into()),
905                )
906                .unwrap(),
907            )),
908            r#"{"type": "map","values": {"type": "map","values": "int"}}"#,
909            Value::Map(HashMap::from_iter([
910                (
911                    "k1".into(),
912                    Value::Map(HashMap::from_iter([
913                        ("a".into(), Value::Int(1)),
914                        ("b".into(), Value::Int(2)),
915                    ])),
916                ),
917                (
918                    "k2".into(),
919                    Value::Map(HashMap::from_iter([
920                        ("c".into(), Value::Int(3)),
921                        ("d".into(), Value::Int(4)),
922                    ])),
923                ),
924            ])),
925        );
926
927        test_ok(
928            &DataType::Struct(StructType::new(vec![
929                (
930                    "p",
931                    DataType::Struct(StructType::new(vec![
932                        ("x", DataType::Int32),
933                        ("y", DataType::Int32),
934                    ])),
935                ),
936                (
937                    "q",
938                    DataType::Struct(StructType::new(vec![
939                        ("x", DataType::Int32),
940                        ("y", DataType::Int32),
941                    ])),
942                ),
943            ])),
944            Some(ScalarImpl::Struct(StructValue::new(vec![
945                Some(ScalarImpl::Struct(StructValue::new(vec![
946                    Some(ScalarImpl::Int32(-2)),
947                    Some(ScalarImpl::Int32(-1)),
948                ]))),
949                Some(ScalarImpl::Struct(StructValue::new(vec![
950                    Some(ScalarImpl::Int32(2)),
951                    Some(ScalarImpl::Int32(1)),
952                ]))),
953            ]))),
954            r#"{
955                "type": "record",
956                "name": "Segment",
957                "fields": [
958                    {
959                        "name": "p",
960                        "type": {
961                            "type": "record",
962                            "name": "Point",
963                            "fields": [
964                                {
965                                    "name": "x",
966                                    "type": "int"
967                                },
968                                {
969                                    "name": "y",
970                                    "type": "int"
971                                }
972                            ]
973                        }
974                    },
975                    {
976                        "name": "q",
977                        "type": "Point"
978                    }
979                ]
980            }"#,
981            Value::Record(vec![
982                (
983                    "p".to_owned(),
984                    Value::Record(vec![
985                        ("x".to_owned(), Value::Int(-2)),
986                        ("y".to_owned(), Value::Int(-1)),
987                    ]),
988                ),
989                (
990                    "q".to_owned(),
991                    Value::Record(vec![
992                        ("x".to_owned(), Value::Int(2)),
993                        ("y".to_owned(), Value::Int(1)),
994                    ]),
995                ),
996            ]),
997        );
998
999        // NEW: Varchar to Enum tests
1000        test_ok(
1001            &DataType::Varchar,
1002            Some(ScalarImpl::Utf8("RED".into())),
1003            r#"{"type": "enum", "name": "Color", "symbols": ["RED", "GREEN", "BLUE"]}"#,
1004            Value::Enum(0, "RED".to_owned()),
1005        );
1006
1007        test_ok(
1008            &DataType::Varchar,
1009            Some(ScalarImpl::Utf8("BLUE".into())),
1010            r#"{"type": "enum", "name": "Color", "symbols": ["RED", "GREEN", "BLUE"]}"#,
1011            Value::Enum(2, "BLUE".to_owned()),
1012        );
1013
1014        test_ok(
1015            &DataType::Varchar,
1016            Some(ScalarImpl::Utf8("ACTIVE".into())),
1017            r#"{"type": "enum", "name": "Status", "symbols": ["ACTIVE", "INACTIVE"]}"#,
1018            Value::Enum(0, "ACTIVE".to_owned()),
1019        );
1020
1021        // Test complex JSON with nested structures - using serde_json::Value comparison
1022        let complex_json = r#"{
1023            "person": {
1024                "name": "John Doe",
1025                "age": 30,
1026                "address": {
1027                    "street": "123 Main St.",
1028                    "city": "New York",
1029                    "coordinates": [40.7128, -74.0060]
1030                },
1031                "contacts": [
1032                    {"type": "email", "value": "john@example.com"},
1033                    {"type": "phone", "value": "+1-555-123-4567"}
1034                ],
1035                "active": true,
1036                "preferences": {
1037                    "notifications": true,
1038                    "theme": "dark",
1039                    "languages": ["en", "es"],
1040                    "lastLogin": null
1041                },
1042                "tags": ["premium", "verified"],
1043                "unicode_test": "Hello, δΈ–η•Œ! 🌍"
1044            }
1045        }"#;
1046
1047        let input_json = JsonbVal::from_str(complex_json).unwrap();
1048        let result = on_field(
1049            &DataType::Jsonb,
1050            Some(ScalarImpl::Jsonb(input_json)).to_datum_ref(),
1051            &AvroSchema::parse_str(r#""string""#).unwrap(),
1052            &NamesRef::new(&AvroSchema::parse_str(r#""string""#).unwrap()).unwrap(),
1053        )
1054        .unwrap();
1055
1056        // Compare as parsed JSON values to handle key order randomness
1057        if let Value::String(result_str) = result {
1058            let expected_json: serde_json::Value = serde_json::from_str(complex_json).unwrap();
1059            let actual_json: serde_json::Value = serde_json::from_str(&result_str).unwrap();
1060            assert_eq!(
1061                expected_json, actual_json,
1062                "JSON values should be equivalent regardless of key order"
1063            );
1064        } else {
1065            panic!("Expected String value");
1066        };
1067    }
1068
1069    #[test]
1070    fn test_encode_avro_err() {
1071        test_err(
1072            &DataType::Interval,
1073            Some(ScalarRefImpl::Interval(Interval::from_month_day_usec(
1074                -1,
1075                -1,
1076                i64::MAX,
1077            ))),
1078            r#"{"type": "fixed", "name": "Duration", "size": 12, "logicalType": "duration"}"#,
1079            "encode '' error: -1 mons -1 days +2562047788:00:54.775807 overflows avro duration",
1080        );
1081
1082        let avro_schema = AvroSchema::parse_str(
1083            r#"{"type": "record", "name": "Root", "fields": [
1084                {"name": "f0", "type": "int"}
1085            ]}"#,
1086        )
1087        .unwrap();
1088        let mut record = Record::new(&avro_schema).unwrap();
1089        record.put("f0", Value::String("2".into()));
1090        let res: SinkResult<Vec<u8>> = AvroEncoded {
1091            value: Value::from(record),
1092            schema: Arc::new(avro_schema),
1093            header: AvroHeader::ConfluentSchemaRegistry(42),
1094        }
1095        .ser_to();
1096        assert_eq!(
1097            res.unwrap_err().to_string(),
1098            "Encode error: Value does not match schema"
1099        );
1100    }
1101
1102    #[test]
1103    fn test_encode_avro_record() {
1104        let avro_schema = AvroSchema::parse_str(
1105            r#"{
1106                "type": "record",
1107                "name": "Root",
1108                "fields": [
1109                    {"name": "req", "type": "int"},
1110                    {"name": "opt", "type": ["null", "long"]}
1111                ]
1112            }"#,
1113        )
1114        .unwrap();
1115        let avro_schema = Arc::new(avro_schema);
1116        let header = AvroHeader::None;
1117
1118        let schema = Schema::new(vec![
1119            Field::with_name(DataType::Int64, "opt"),
1120            Field::with_name(DataType::Int32, "req"),
1121        ]);
1122        let row = OwnedRow::new(vec![
1123            Some(ScalarImpl::Int64(31)),
1124            Some(ScalarImpl::Int32(15)),
1125        ]);
1126        let encoder = AvroEncoder::new(schema, None, avro_schema.clone(), header).unwrap();
1127        let actual = encoder.encode(row).unwrap();
1128        assert_eq!(
1129            actual.value,
1130            Value::Record(vec![
1131                ("req".into(), Value::Int(15)),
1132                ("opt".into(), Value::Union(1, Value::Long(31).into())),
1133            ])
1134        );
1135
1136        let schema = Schema::new(vec![Field::with_name(DataType::Int32, "req")]);
1137        let row = OwnedRow::new(vec![Some(ScalarImpl::Int32(15))]);
1138        let encoder = AvroEncoder::new(schema, None, avro_schema.clone(), header).unwrap();
1139        let actual = encoder.encode(row).unwrap();
1140        assert_eq!(
1141            actual.value,
1142            Value::Record(vec![
1143                ("req".into(), Value::Int(15)),
1144                ("opt".into(), Value::Union(0, Value::Null.into())),
1145            ])
1146        );
1147
1148        let schema = Schema::new(vec![Field::with_name(DataType::Int64, "opt")]);
1149        let Err(err) = AvroEncoder::new(schema, None, avro_schema.clone(), header) else {
1150            panic!()
1151        };
1152        assert_eq!(
1153            err.to_string(),
1154            "Encode error: encode 'req' error: field not present but required"
1155        );
1156
1157        let schema = Schema::new(vec![
1158            Field::with_name(DataType::Int64, "opt"),
1159            Field::with_name(DataType::Int32, "req"),
1160            Field::with_name(DataType::Varchar, "extra"),
1161        ]);
1162        let Err(err) = AvroEncoder::new(schema, None, avro_schema, header) else {
1163            panic!()
1164        };
1165        assert_eq!(
1166            err.to_string(),
1167            "Encode error: encode 'extra' error: field not in avro"
1168        );
1169
1170        let avro_schema = AvroSchema::parse_str(r#"["null", "long"]"#).unwrap();
1171        let schema = Schema::new(vec![Field::with_name(DataType::Int64, "opt")]);
1172        let Err(err) = AvroEncoder::new(schema, None, avro_schema.into(), header) else {
1173            panic!()
1174        };
1175        assert_eq!(
1176            err.to_string(),
1177            r#"Encode error: encode '' error: expect avro record but got ["null","long"]"#
1178        );
1179
1180        test_err(
1181            &DataType::Struct(StructType::new(vec![("f0", DataType::Boolean)])),
1182            (),
1183            r#"{"type": "record", "name": "T", "fields": [{"name": "f0", "type": "int"}]}"#,
1184            "encode 'f0' error: cannot encode boolean column as \"int\" field",
1185        );
1186    }
1187
1188    #[test]
1189    fn test_encode_avro_array() {
1190        let avro_schema = r#"{
1191            "type": "array",
1192            "items": "int"
1193        }"#;
1194
1195        test_ok(
1196            &DataType::Int32.list(),
1197            Some(ScalarImpl::List(ListValue::from_iter([4, 5]))),
1198            avro_schema,
1199            Value::Array(vec![Value::Int(4), Value::Int(5)]),
1200        );
1201
1202        test_err(
1203            &DataType::Int32.list(),
1204            Some(ScalarImpl::List(ListValue::from_iter([Some(4), None]))).to_datum_ref(),
1205            avro_schema,
1206            "encode '' error: found null but required",
1207        );
1208
1209        test_ok(
1210            &DataType::Int32.list(),
1211            Some(ScalarImpl::List(ListValue::from_iter([Some(4), None]))),
1212            r#"{
1213                "type": "array",
1214                "items": ["null", "int"]
1215            }"#,
1216            Value::Array(vec![
1217                Value::Union(1, Value::Int(4).into()),
1218                Value::Union(0, Value::Null.into()),
1219            ]),
1220        );
1221
1222        test_ok(
1223            &DataType::Int32.list().list(),
1224            Some(ScalarImpl::List(ListValue::from_iter([
1225                ListValue::from_iter([26, 29]),
1226                ListValue::from_iter([46, 49]),
1227            ]))),
1228            r#"{
1229                "type": "array",
1230                "items": {
1231                    "type": "array",
1232                    "items": "int"
1233                }
1234            }"#,
1235            Value::Array(vec![
1236                Value::Array(vec![Value::Int(26), Value::Int(29)]),
1237                Value::Array(vec![Value::Int(46), Value::Int(49)]),
1238            ]),
1239        );
1240
1241        test_err(
1242            &DataType::Boolean.list(),
1243            (),
1244            r#"{"type": "array", "items": "int"}"#,
1245            "encode '' error: cannot encode boolean column as \"int\" field",
1246        );
1247    }
1248
1249    #[test]
1250    fn test_encode_avro_union() {
1251        let t = &DataType::Timestamptz;
1252        let datum = Some(ScalarImpl::Timestamptz(
1253            Timestamptz::from_micros(1500).unwrap(),
1254        ));
1255        let opt_micros = r#"["null", {"type": "long", "logicalType": "timestamp-micros"}]"#;
1256        let opt_millis = r#"["null", {"type": "long", "logicalType": "timestamp-millis"}]"#;
1257        let both = r#"[{"type": "long", "logicalType": "timestamp-millis"}, {"type": "long", "logicalType": "timestamp-micros"}]"#;
1258        let empty = "[]";
1259        let one = r#"[{"type": "long", "logicalType": "timestamp-millis"}]"#;
1260        let right = r#"[{"type": "long", "logicalType": "timestamp-millis"}, "null"]"#;
1261
1262        test_ok(
1263            t,
1264            datum.clone(),
1265            opt_micros,
1266            Value::Union(1, Value::TimestampMicros(1500).into()),
1267        );
1268        test_ok(t, None, opt_micros, Value::Union(0, Value::Null.into()));
1269        test_ok(
1270            t,
1271            datum.clone(),
1272            opt_millis,
1273            Value::Union(1, Value::TimestampMillis(1).into()),
1274        );
1275        test_ok(t, None, opt_millis, Value::Union(0, Value::Null.into()));
1276
1277        test_err(
1278            t,
1279            datum.to_datum_ref(),
1280            both,
1281            r#"encode '' error: cannot encode timestamp with time zone column as [{"type":"long"},{"type":"long"}] field"#,
1282        );
1283
1284        test_err(
1285            t,
1286            datum.to_datum_ref(),
1287            empty,
1288            "encode '' error: cannot encode timestamp with time zone column as [] field",
1289        );
1290
1291        test_ok(
1292            t,
1293            datum.clone(),
1294            one,
1295            Value::Union(0, Value::TimestampMillis(1).into()),
1296        );
1297        test_err(t, None, one, "encode '' error: found null but required");
1298
1299        test_ok(
1300            t,
1301            datum,
1302            right,
1303            Value::Union(0, Value::TimestampMillis(1).into()),
1304        );
1305        test_ok(t, None, right, Value::Union(1, Value::Null.into()));
1306    }
1307
1308    /// This just demonstrates bugs of the upstream [`apache_avro`], rather than our encoder.
1309    /// The encoder is not using these buggy calls and is already tested above.
1310    #[test]
1311    fn test_encode_avro_lib_bug() {
1312        use apache_avro::{Reader, Writer};
1313
1314        // a record with 2 optional int fields
1315        let avro_schema = AvroSchema::parse_str(
1316            r#"{
1317                "type": "record",
1318                "name": "Root",
1319                "fields": [
1320                    {
1321                        "name": "f0",
1322                        "type": ["null", "int"]
1323                    },
1324                    {
1325                        "name": "f1",
1326                        "type": ["null", "int"]
1327                    }
1328                ]
1329            }"#,
1330        )
1331        .unwrap();
1332
1333        let mut writer = Writer::new(&avro_schema, Vec::new());
1334        let mut record = Record::new(writer.schema()).unwrap();
1335        // f0 omitted, f1 = Int(3)
1336        record.put("f1", Value::Int(3));
1337        writer.append(record).unwrap();
1338        let encoded = writer.into_inner().unwrap();
1339        // writing produced no error, but read fails
1340        let reader = Reader::new(encoded.as_slice()).unwrap();
1341        for value in reader {
1342            assert_eq!(
1343                value.unwrap_err().to_string(),
1344                "Union index 3 out of bounds: 2"
1345            );
1346        }
1347    }
1348}