Skip to main content

risingwave_connector/parser/unified/
json.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::str::FromStr;
16use std::sync::LazyLock;
17
18use base64::Engine;
19use itertools::Itertools;
20use num_bigint::BigInt;
21use risingwave_common::array::{Finite32, ListValue, StructValue};
22use risingwave_common::cast::{i64_to_timestamp, i64_to_timestamptz, str_to_bytea};
23use risingwave_common::log::LogSuppressor;
24use risingwave_common::types::{
25    DEBEZIUM_UNAVAILABLE_VALUE, DataType, Date, Decimal, Int256, Interval, JsonbVal, ScalarImpl,
26    Time, Timestamp, Timestamptz, ToOwnedDatum, VectorVal, debezium_unavailable_vector,
27};
28use risingwave_connector_codec::decoder::utils::scaled_bigint_to_rust_decimal;
29use simd_json::base::ValueAsObject;
30use simd_json::prelude::{
31    TypedValue, ValueAsArray, ValueAsScalar, ValueObjectAccess, ValueTryAsScalar,
32};
33use simd_json::{BorrowedValue, ValueType};
34use thiserror_ext::AsReport;
35
36use super::{Access, AccessError, AccessResult};
37use crate::parser::DatumCow;
38use crate::schema::{InvalidOptionError, bail_invalid_option_error};
39
40/// Try to parse a Debezium `PostGIS` spatial object.
41///
42/// Debezium represents `PostGIS` `geometry` and `geography` values as an object:
43/// `{"srid": <int>, "wkb": <base64_string>}`.
44/// For our current Postgres CDC ingestion, the `wkb` field is expected to be EWKB bytes (base64-encoded),
45/// and `srid` is redundant. We decode `wkb` into raw bytes and store it as `bytea`.
46///
47/// Note: Debezium provides an SMT to convert between `WKB` and `EWKB`, which may be useful for future
48/// unification across connectors (e.g., MySQL): see `GeometryFormatTransformer`.
49///
50/// Return semantics:
51/// - `Ok(Some(bytes))`: The input matches the Debezium spatial shape (`srid` is numeric AND `wkb` is string),
52///   and we successfully decoded `wkb` into bytes.
53/// - `Ok(None)`: The input does NOT look like a Debezium spatial object. This allows the caller to keep the
54///   match arm focused on dispatching, and avoids misclassifying other JSON objects that might map to `bytea`
55///   in the future.
56/// - `Err(...)`: The input looks like a Debezium spatial object, but decoding/parsing failed (e.g. invalid
57///   base64). This indicates a real data/format error and should not be silently ignored.
58fn try_parse_debezium_postgis_spatial_as_bytea(
59    value: &BorrowedValue<'_>,
60    create_error: impl Fn() -> AccessError,
61) -> AccessResult<Option<Box<[u8]>>> {
62    let obj = match value.as_object() {
63        Some(obj) => obj,
64        None => return Ok(None),
65    };
66
67    // Strictly identify the spatial object by checking both fields and their types.
68    // There may be other objects that map to bytea in the future.
69    let srid = obj.get("srid").and_then(|v| v.as_i64());
70    let wkb = obj.get("wkb").and_then(|v| v.as_str());
71
72    let (Some(_srid), Some(wkb)) = (srid, wkb) else {
73        return Ok(None);
74    };
75
76    let bytes = base64::engine::general_purpose::STANDARD
77        .decode(wkb)
78        .map_err(|_| create_error())?
79        .into_boxed_slice();
80
81    Ok(Some(bytes))
82}
83
84#[derive(Clone, Debug)]
85pub enum ByteaHandling {
86    Standard,
87    // debezium converts postgres bytea to base64 format
88    Base64,
89}
90#[derive(Clone, Debug)]
91pub enum TimeHandling {
92    Milli,
93    Micro,
94}
95
96#[derive(Clone, Copy, Debug)]
97pub enum BigintUnsignedHandlingMode {
98    /// Convert unsigned bigint to signed bigint (default)
99    Long,
100    /// Use base64-encoded decimal for unsigned bigint (Debezium precise mode)
101    Precise,
102}
103
104#[derive(Clone, Debug)]
105pub enum TimestamptzHandling {
106    /// `"2024-04-11T02:00:00.123456Z"`
107    UtcString,
108    /// `"2024-04-11 02:00:00.123456"`
109    UtcWithoutSuffix,
110    /// `1712800800123`
111    Milli,
112    /// `1712800800123456`
113    Micro,
114    /// Both `1712800800123` (ms) and `1712800800123456` (us) maps to `2024-04-11`.
115    ///
116    /// Only works for `[1973-03-03 09:46:40, 5138-11-16 09:46:40)`.
117    ///
118    /// This option is backward compatible.
119    GuessNumberUnit,
120}
121
122impl TimestamptzHandling {
123    pub const OPTION_KEY: &'static str = "timestamptz.handling.mode";
124
125    pub fn from_options(value: &str) -> Result<Self, InvalidOptionError> {
126        let mode = match value {
127            "utc_string" => Self::UtcString,
128            "utc_without_suffix" => Self::UtcWithoutSuffix,
129            "micro" => Self::Micro,
130            "milli" => Self::Milli,
131            "guess_number_unit" => Self::GuessNumberUnit,
132            v => bail_invalid_option_error!("unrecognized {} value {}", Self::OPTION_KEY, v),
133        };
134        Ok(mode)
135    }
136}
137
138#[derive(Clone, Debug)]
139pub enum TimestampHandling {
140    Milli,
141    Micro,
142    GuessNumberUnit,
143}
144
145#[derive(Clone, Debug)]
146pub enum JsonValueHandling {
147    AsValue,
148    AsString,
149}
150#[derive(Clone, Debug)]
151pub enum NumericHandling {
152    Strict,
153    // should integer be parsed to float
154    Relax {
155        // should "3.14" be parsed to 3.14 in float
156        string_parsing: bool,
157    },
158}
159#[derive(Clone, Debug)]
160pub enum BooleanHandling {
161    Strict,
162    // should integer 1,0 be parsed to boolean (debezium)
163    Relax {
164        // should "True" "False" be parsed to true or false in boolean
165        string_parsing: bool,
166        // should string "1" "0" be paesed to boolean (cannal + mysql)
167        string_integer_parsing: bool,
168    },
169}
170
171#[derive(Clone, Debug)]
172pub enum VarcharHandling {
173    // do not allow other types cast to varchar
174    Strict,
175    // allow Json Value (Null, Bool, I64, I128, U64, U128, F64) cast to varchar
176    OnlyPrimaryTypes,
177    // allow all type cast to varchar (inc. Array, Object)
178    AllTypes,
179}
180
181#[derive(Clone, Debug)]
182pub enum StructHandling {
183    // only allow object parsed to struct
184    Strict,
185    // allow string containing a serialized json object (like "{\"a\": 1, \"b\": 2}") parsed to
186    // struct
187    AllowJsonString,
188}
189
190#[derive(Clone, Debug)]
191pub struct JsonParseOptions {
192    pub bytea_handling: ByteaHandling,
193    pub time_handling: TimeHandling,
194    pub timestamp_handling: TimestampHandling,
195    pub timestamptz_handling: TimestamptzHandling,
196    pub json_value_handling: JsonValueHandling,
197    pub numeric_handling: NumericHandling,
198    pub boolean_handling: BooleanHandling,
199    pub varchar_handling: VarcharHandling,
200    pub struct_handling: StructHandling,
201    pub bigint_unsigned_handling: BigintUnsignedHandlingMode,
202    pub ignoring_keycase: bool,
203    pub handle_toast_columns: bool,
204}
205
206impl Default for JsonParseOptions {
207    fn default() -> Self {
208        Self::DEFAULT.clone()
209    }
210}
211
212impl JsonParseOptions {
213    pub const CANAL: JsonParseOptions = JsonParseOptions {
214        bytea_handling: ByteaHandling::Standard,
215        time_handling: TimeHandling::Micro,
216        timestamp_handling: TimestampHandling::GuessNumberUnit, // backward-compatible
217        timestamptz_handling: TimestamptzHandling::GuessNumberUnit, // backward-compatible
218        json_value_handling: JsonValueHandling::AsValue,
219        numeric_handling: NumericHandling::Relax {
220            string_parsing: true,
221        },
222        boolean_handling: BooleanHandling::Relax {
223            string_parsing: true,
224            string_integer_parsing: true,
225        },
226        varchar_handling: VarcharHandling::Strict,
227        struct_handling: StructHandling::Strict,
228        bigint_unsigned_handling: BigintUnsignedHandlingMode::Long, // default to long mode
229        ignoring_keycase: true,
230        handle_toast_columns: false,
231    };
232    pub const DEFAULT: JsonParseOptions = JsonParseOptions {
233        bytea_handling: ByteaHandling::Standard,
234        time_handling: TimeHandling::Micro,
235        timestamp_handling: TimestampHandling::GuessNumberUnit, // backward-compatible
236        timestamptz_handling: TimestamptzHandling::GuessNumberUnit, // backward-compatible
237        json_value_handling: JsonValueHandling::AsValue,
238        numeric_handling: NumericHandling::Relax {
239            string_parsing: true,
240        },
241        boolean_handling: BooleanHandling::Strict,
242        varchar_handling: VarcharHandling::OnlyPrimaryTypes,
243        struct_handling: StructHandling::AllowJsonString,
244        bigint_unsigned_handling: BigintUnsignedHandlingMode::Long, // default to long mode
245        ignoring_keycase: true,
246        handle_toast_columns: false,
247    };
248
249    pub fn new_for_debezium(
250        timestamptz_handling: TimestamptzHandling,
251        timestamp_handling: TimestampHandling,
252        time_handling: TimeHandling,
253        bigint_unsigned_handling: BigintUnsignedHandlingMode,
254        handle_toast_columns: bool,
255    ) -> Self {
256        Self {
257            bytea_handling: ByteaHandling::Base64,
258            time_handling,
259            timestamp_handling,
260            timestamptz_handling,
261            json_value_handling: JsonValueHandling::AsString,
262            numeric_handling: NumericHandling::Relax {
263                string_parsing: false,
264            },
265            boolean_handling: BooleanHandling::Relax {
266                string_parsing: false,
267                string_integer_parsing: false,
268            },
269            varchar_handling: VarcharHandling::Strict,
270            struct_handling: StructHandling::Strict,
271            bigint_unsigned_handling,
272            ignoring_keycase: true,
273            handle_toast_columns,
274        }
275    }
276
277    pub fn parse<'a>(
278        &self,
279        value: &'a BorrowedValue<'a>,
280        type_expected: &DataType,
281    ) -> AccessResult<DatumCow<'a>> {
282        let create_error = || AccessError::TypeError {
283            expected: format!("{:?}", type_expected),
284            got: value.value_type().to_string(),
285            value: value.to_string(),
286        };
287        let v: ScalarImpl = match (type_expected, value.value_type()) {
288            (_, ValueType::Null) => return Ok(DatumCow::NULL),
289            // ---- Boolean -----
290            (DataType::Boolean, ValueType::Bool) => value.as_bool().unwrap().into(),
291
292            (
293                DataType::Boolean,
294                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
295            ) if matches!(self.boolean_handling, BooleanHandling::Relax { .. })
296                && matches!(value.as_i64(), Some(0i64) | Some(1i64)) =>
297            {
298                (value.as_i64() == Some(1i64)).into()
299            }
300
301            (DataType::Boolean, ValueType::String)
302                if matches!(
303                    self.boolean_handling,
304                    BooleanHandling::Relax {
305                        string_parsing: true,
306                        ..
307                    }
308                ) =>
309            {
310                match value.as_str().unwrap().to_lowercase().as_str() {
311                    "true" => true.into(),
312                    "false" => false.into(),
313                    c @ ("1" | "0")
314                        if matches!(
315                            self.boolean_handling,
316                            BooleanHandling::Relax {
317                                string_parsing: true,
318                                string_integer_parsing: true
319                            }
320                        ) =>
321                    {
322                        if c == "1" {
323                            true.into()
324                        } else {
325                            false.into()
326                        }
327                    }
328                    _ => Err(create_error())?,
329                }
330            }
331            // ---- Int16 -----
332            (
333                DataType::Int16,
334                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
335            ) => value.try_as_i16().map_err(|_| create_error())?.into(),
336
337            (DataType::Int16, ValueType::String)
338                if matches!(
339                    self.numeric_handling,
340                    NumericHandling::Relax {
341                        string_parsing: true
342                    }
343                ) =>
344            {
345                value
346                    .as_str()
347                    .unwrap()
348                    .parse::<i16>()
349                    .map_err(|_| create_error())?
350                    .into()
351            }
352            // ---- Int32 -----
353            (
354                DataType::Int32,
355                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
356            ) => value.try_as_i32().map_err(|_| create_error())?.into(),
357
358            (DataType::Int32, ValueType::String)
359                if matches!(
360                    self.numeric_handling,
361                    NumericHandling::Relax {
362                        string_parsing: true
363                    }
364                ) =>
365            {
366                value
367                    .as_str()
368                    .unwrap()
369                    .parse::<i32>()
370                    .map_err(|_| create_error())?
371                    .into()
372            }
373            // ---- Int64 -----
374            (
375                DataType::Int64,
376                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
377            ) => value.try_as_i64().map_err(|_| create_error())?.into(),
378
379            (DataType::Int64, ValueType::String)
380                if matches!(
381                    self.numeric_handling,
382                    NumericHandling::Relax {
383                        string_parsing: true
384                    }
385                ) =>
386            {
387                value
388                    .as_str()
389                    .unwrap()
390                    .parse::<i64>()
391                    .map_err(|_| create_error())?
392                    .into()
393            }
394            // ---- Float32 -----
395            (
396                DataType::Float32,
397                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
398            ) if matches!(self.numeric_handling, NumericHandling::Relax { .. }) => {
399                (value.try_as_i64().map_err(|_| create_error())? as f32).into()
400            }
401            (DataType::Float32, ValueType::String)
402                if matches!(
403                    self.numeric_handling,
404                    NumericHandling::Relax {
405                        string_parsing: true
406                    }
407                ) =>
408            {
409                value
410                    .as_str()
411                    .unwrap()
412                    .parse::<f32>()
413                    .map_err(|_| create_error())?
414                    .into()
415            }
416            (DataType::Float32, ValueType::F64) => {
417                value.try_as_f32().map_err(|_| create_error())?.into()
418            }
419            // ---- Float64 -----
420            (
421                DataType::Float64,
422                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
423            ) if matches!(self.numeric_handling, NumericHandling::Relax { .. }) => {
424                (value.try_as_i64().map_err(|_| create_error())? as f64).into()
425            }
426            (DataType::Float64, ValueType::String)
427                if matches!(
428                    self.numeric_handling,
429                    NumericHandling::Relax {
430                        string_parsing: true
431                    }
432                ) =>
433            {
434                value
435                    .as_str()
436                    .unwrap()
437                    .parse::<f64>()
438                    .map_err(|_| create_error())?
439                    .into()
440            }
441            (DataType::Float64, ValueType::F64) => {
442                value.try_as_f64().map_err(|_| create_error())?.into()
443            }
444            // ---- Decimal -----
445            (DataType::Decimal, ValueType::I128 | ValueType::U128) => {
446                Decimal::from_str(&value.try_as_i128().map_err(|_| create_error())?.to_string())
447                    .map_err(|_| create_error())?
448                    .into()
449            }
450            (DataType::Decimal, ValueType::I64 | ValueType::U64) => {
451                let i64_val = value.try_as_i64().map_err(|_| create_error())?;
452                Decimal::from(i64_val).into()
453            }
454            (DataType::Decimal, ValueType::String) => {
455                let str_val = value.as_str().unwrap();
456                // the following values are special string generated by Debezium and should be handled separately
457                match str_val {
458                    "NAN" => return Ok(DatumCow::Owned(Some(ScalarImpl::Decimal(Decimal::NaN)))),
459                    "POSITIVE_INFINITY" => {
460                        return Ok(DatumCow::Owned(Some(ScalarImpl::Decimal(
461                            Decimal::PositiveInf,
462                        ))));
463                    }
464                    "NEGATIVE_INFINITY" => {
465                        return Ok(DatumCow::Owned(Some(ScalarImpl::Decimal(
466                            Decimal::NegativeInf,
467                        ))));
468                    }
469                    _ => {}
470                }
471
472                Decimal::from_str(str_val)
473                    .or_else(|_err| {
474                        try_base64_decode_decimal(
475                            str_val,
476                            self.bigint_unsigned_handling,
477                            create_error,
478                        )
479                    })?
480                    .into()
481            }
482
483            (DataType::Decimal, ValueType::F64) => {
484                Decimal::try_from(value.try_as_f64().map_err(|_| create_error())?)
485                    .map_err(|_| create_error())?
486                    .into()
487            }
488            (DataType::Decimal, ValueType::Object) => {
489                // ref https://github.com/risingwavelabs/risingwave/issues/10628
490                // handle debezium json (variable scale): {"scale": int, "value": bytes}
491                let scale = value
492                    .get("scale")
493                    .ok_or_else(create_error)?
494                    .as_i32()
495                    .unwrap();
496                let value = value
497                    .get("value")
498                    .ok_or_else(create_error)?
499                    .as_str()
500                    .unwrap()
501                    .as_bytes();
502                let unscaled = BigInt::from_signed_bytes_be(value);
503                let decimal = scaled_bigint_to_rust_decimal(unscaled, scale as _)?;
504                ScalarImpl::Decimal(Decimal::Normalized(decimal))
505            }
506            // ---- Date -----
507            (
508                DataType::Date,
509                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
510            ) => Date::with_days_since_unix_epoch(value.try_as_i32().map_err(|_| create_error())?)
511                .map_err(|_| create_error())?
512                .into(),
513            (DataType::Date, ValueType::String) => value
514                .as_str()
515                .unwrap()
516                .parse::<Date>()
517                .map_err(|_| create_error())?
518                .into(),
519            // ---- Varchar -----
520            (DataType::Varchar, ValueType::String) => {
521                return Ok(DatumCow::Borrowed(Some(value.as_str().unwrap().into())));
522            }
523            (
524                DataType::Varchar,
525                ValueType::Bool
526                | ValueType::I64
527                | ValueType::I128
528                | ValueType::U64
529                | ValueType::U128
530                | ValueType::F64,
531            ) if matches!(self.varchar_handling, VarcharHandling::OnlyPrimaryTypes) => {
532                value.to_string().into()
533            }
534            (
535                DataType::Varchar,
536                ValueType::Bool
537                | ValueType::I64
538                | ValueType::I128
539                | ValueType::U64
540                | ValueType::U128
541                | ValueType::F64
542                | ValueType::Array
543                | ValueType::Object,
544            ) if matches!(self.varchar_handling, VarcharHandling::AllTypes) => {
545                value.to_string().into()
546            }
547            // ---- Time -----
548            (DataType::Time, ValueType::String) => value
549                .as_str()
550                .unwrap()
551                .parse::<Time>()
552                .map_err(|_| create_error())?
553                .into(),
554            (
555                DataType::Time,
556                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
557            ) => value
558                .as_i64()
559                .map(|i| match self.time_handling {
560                    TimeHandling::Milli => Time::with_milli(i as u32),
561                    TimeHandling::Micro => Time::with_micro(i as u64),
562                })
563                .unwrap()
564                .map_err(|_| create_error())?
565                .into(),
566            // ---- Timestamp -----
567            (DataType::Timestamp, ValueType::String) => value
568                .as_str()
569                .unwrap()
570                .parse::<Timestamp>()
571                .map_err(|_| create_error())?
572                .into(),
573            (
574                DataType::Timestamp,
575                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
576            ) => value
577                .as_i64()
578                .map(|num| match self.timestamp_handling {
579                    // Only when user configures debezium.time.precision.mode = 'connect',
580                    // the Milli branch will be executed
581                    TimestampHandling::Milli => {
582                        Timestamp::with_millis(num).map_err(|_| create_error())
583                    }
584                    TimestampHandling::Micro => {
585                        Timestamp::with_micros(num).map_err(|_| create_error())
586                    }
587                    TimestampHandling::GuessNumberUnit => {
588                        i64_to_timestamp(num).map_err(|_| create_error())
589                    }
590                })
591                .ok_or_else(create_error)??
592                .into(),
593            // ---- Timestamptz -----
594            (DataType::Timestamptz, ValueType::String) => match self.timestamptz_handling {
595                TimestamptzHandling::UtcWithoutSuffix => value
596                    .as_str()
597                    .unwrap()
598                    .parse::<Timestamp>()
599                    .map(|naive_utc| {
600                        Timestamptz::from_micros_uncheck(naive_utc.0.and_utc().timestamp_micros())
601                    })
602                    .map_err(|_| create_error())?
603                    .into(),
604                // Unless explicitly requested `utc_without_utc`, we parse string with `YYYY-MM-DDTHH:MM:SSZ`.
605                _ => value
606                    .as_str()
607                    .unwrap()
608                    .parse::<Timestamptz>()
609                    .map_err(|_| create_error())?
610                    .into(),
611            },
612            (
613                DataType::Timestamptz,
614                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
615            ) => value
616                .as_i64()
617                .and_then(|num| match self.timestamptz_handling {
618                    TimestamptzHandling::GuessNumberUnit => i64_to_timestamptz(num).ok(),
619                    TimestamptzHandling::Micro => Timestamptz::from_micros(num),
620                    TimestamptzHandling::Milli => Timestamptz::from_millis(num),
621                    // When explicitly requested string format, number without units are rejected.
622                    TimestamptzHandling::UtcString | TimestamptzHandling::UtcWithoutSuffix => None,
623                })
624                .ok_or_else(create_error)?
625                .into(),
626            // ---- Interval -----
627            (DataType::Interval, ValueType::String) => value
628                .as_str()
629                .unwrap()
630                .parse::<Interval>()
631                .map_err(|_| create_error())?
632                .into(),
633            // ---- Struct -----
634            (DataType::Struct(struct_type_info), ValueType::Object) => {
635                // Collecting into a Result<Vec<_>> doesn't reserve the capacity in advance, so we `Vec::with_capacity` instead.
636                // https://github.com/rust-lang/rust/issues/48994
637                let mut fields = Vec::with_capacity(struct_type_info.len());
638                for (field_name, field_type) in struct_type_info.iter() {
639                    let field_value = json_object_get_case_insensitive(value, field_name)
640                            .unwrap_or_else(|| {
641                                let error = AccessError::Undefined {
642                                    name: field_name.to_owned(),
643                                    path: struct_type_info.to_string(), // TODO: this is not good, we should maintain a path stack
644                                };
645                                // TODO: is it possible to unify the logging with the one in `do_action`?
646                                static LOG_SUPPRESSOR: LazyLock<LogSuppressor> =  LazyLock::new(LogSuppressor::default);
647                                if let Ok(suppressed_count) = LOG_SUPPRESSOR.check() {
648                                    tracing::warn!(error = %error.as_report(), suppressed_count, "undefined nested field, padding with `NULL`");
649                                }
650                                &BorrowedValue::Static(simd_json::StaticNode::Null)
651                            });
652                    fields.push(
653                        self.parse(field_value, field_type)
654                            .map(|d| d.to_owned_datum())?,
655                    );
656                }
657                StructValue::new(fields).into()
658            }
659
660            // String containing json object, e.g. "{\"a\": 1, \"b\": 2}"
661            // Try to parse it as json object.
662            (DataType::Struct(_), ValueType::String)
663                if matches!(self.struct_handling, StructHandling::AllowJsonString) =>
664            {
665                // TODO: avoid copy by accepting `&mut BorrowedValue` in `parse` method.
666                let mut value = value.as_str().unwrap().as_bytes().to_vec();
667                let value =
668                    simd_json::to_borrowed_value(&mut value[..]).map_err(|_| create_error())?;
669                return self
670                    .parse(&value, type_expected)
671                    .map(|d| d.to_owned_datum().into());
672            }
673
674            // ---- List -----
675            (DataType::List(list_type), ValueType::Array) => ListValue::new({
676                let item_type = list_type.elem();
677                let array = value.as_array().unwrap();
678                let mut builder = item_type.create_array_builder(array.len());
679                for v in array {
680                    let value = self.parse(v, item_type)?;
681                    builder.append(value);
682                }
683                builder.finish()
684            })
685            .into(),
686            // ---- Vector -----
687            (DataType::Vector(size), ValueType::Array) => {
688                let array = value.as_array().unwrap();
689                if array.len() != *size {
690                    Err(create_error())?
691                }
692                let mut elems = Vec::with_capacity(array.len());
693                for v in array {
694                    let value = match v.value_type() {
695                        ValueType::I64 | ValueType::I128 => {
696                            let i128_value = v.try_as_i128().map_err(|_| create_error())?;
697                            i128_value
698                                .to_string()
699                                .parse::<f32>()
700                                .map_err(|_| create_error())?
701                        }
702                        ValueType::U64 | ValueType::U128 => {
703                            let u128_value = v.try_as_u128().map_err(|_| create_error())?;
704                            u128_value
705                                .to_string()
706                                .parse::<f32>()
707                                .map_err(|_| create_error())?
708                        }
709                        ValueType::F64 => {
710                            let f64_value = v.try_as_f64().map_err(|_| create_error())?;
711                            if !f64_value.is_finite() {
712                                Err(create_error())?
713                            }
714                            f64_value
715                                .to_string()
716                                .parse::<f32>()
717                                .map_err(|_| create_error())?
718                        }
719                        _ => Err(create_error())?,
720                    };
721                    let finite = Finite32::try_from(value).map_err(|_| create_error())?;
722                    elems.push(finite);
723                }
724                VectorVal::from(elems).into()
725            }
726            // Vector emitted as a string. Reached via the Java `PgVectorToStringConverter`,
727            // which downgrades the pgvector column from `DoubleVector` (ARRAY) to STRING so
728            // that the Debezium unchanged-TOAST placeholder can flow through without tripping
729            // Connect's schema validation. The value is either the pgvector text form
730            // `"[a,b,...]"` or the placeholder; the latter is mapped to a sentinel `VectorVal`
731            // that the materialize executor recognises and replaces with the old row value.
732            (DataType::Vector(size), ValueType::String) => {
733                let s = value.as_str().unwrap();
734                if self.handle_toast_columns
735                    && s.len() == DEBEZIUM_UNAVAILABLE_VALUE.len()
736                    && s == DEBEZIUM_UNAVAILABLE_VALUE
737                {
738                    // Build the sentinel at the declared dimension so it passes
739                    // `check_datum_type` in the chunk builder; the materialize
740                    // executor recognises it by checking that every element
741                    // equals `DEBEZIUM_UNAVAILABLE_VECTOR_ELEM`.
742                    debezium_unavailable_vector(*size).into()
743                } else {
744                    VectorVal::from_text(s, *size)
745                        .map_err(|_| create_error())?
746                        .into()
747                }
748            }
749
750            // ---- Bytea -----
751            (DataType::Bytea, ValueType::String) => {
752                let value_str = value.as_str().unwrap();
753
754                match self.bytea_handling {
755                    ByteaHandling::Standard => {
756                        let mut buf = Vec::new();
757                        str_to_bytea(value_str, &mut buf).map_err(|_| create_error())?;
758                        buf.into()
759                    }
760                    ByteaHandling::Base64 => base64::engine::general_purpose::STANDARD
761                        .decode(value_str)
762                        .map_err(|_| create_error())?
763                        .into_boxed_slice()
764                        .into(),
765                }
766            }
767            // Handle Debezium PostGIS geometry and geography types:
768            // {"srid": <int>, "wkb": <base64_string>}. We extract the wkb field and
769            // decode it as EWKB bytes.
770            (DataType::Bytea, ValueType::Object) => {
771                match try_parse_debezium_postgis_spatial_as_bytea(value, create_error)? {
772                    Some(bytes) => bytes.into(),
773                    None => Err(create_error())?,
774                }
775            }
776            // ---- Jsonb -----
777            (DataType::Jsonb, ValueType::String)
778                if matches!(self.json_value_handling, JsonValueHandling::AsString) =>
779            {
780                // Check if this value is the Debezium unavailable value (TOAST handling for postgres-cdc).
781                // Debezium will base64 encode the bytea type placeholder.
782                // When a placeholder is encountered, it is converted into a jsonb format placeholder to match the original type.
783                match self.handle_toast_columns {
784                    true => JsonbVal::from_debezium_unavailable_value(value.as_str().unwrap())
785                        .map_err(|_| create_error())?
786                        .into(),
787                    false => JsonbVal::from_str(value.as_str().unwrap())
788                        .map_err(|_| create_error())?
789                        .into(),
790                }
791            }
792            (DataType::Jsonb, _)
793                if matches!(self.json_value_handling, JsonValueHandling::AsValue) =>
794            {
795                let value: serde_json::Value =
796                    value.clone().try_into().map_err(|_| create_error())?;
797                JsonbVal::from(value).into()
798            }
799            // ---- Int256 -----
800            (
801                DataType::Int256,
802                ValueType::I64 | ValueType::I128 | ValueType::U64 | ValueType::U128,
803            ) => Int256::from(value.try_as_i64().map_err(|_| create_error())?).into(),
804
805            (DataType::Int256, ValueType::String) => Int256::from_str(value.as_str().unwrap())
806                .map_err(|_| create_error())?
807                .into(),
808
809            (_expected, _got) => Err(create_error())?,
810        };
811        Ok(DatumCow::Owned(Some(v)))
812    }
813}
814
815/// Try to decode a base64-encoded decimal string for unsigned bigint handling in Precise mode.
816///
817/// This is used when processing CDC data from upstream systems with unsigned bigint (e.g., MySQL CDC).
818/// When users configure `debezium.bigint.unsigned.handling.mode='precise'`, Debezium converts
819/// unsigned bigint to base64-encoded decimal.
820///
821/// Reference: <https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-property-bigint-unsigned-handling-mode>.
822fn try_base64_decode_decimal(
823    str_val: &str,
824    bigint_unsigned_handling: BigintUnsignedHandlingMode,
825    create_error: impl Fn() -> AccessError,
826) -> Result<Decimal, AccessError> {
827    match bigint_unsigned_handling {
828        BigintUnsignedHandlingMode::Precise => {
829            // A better approach would be to get bytes + org.apache.kafka.connect.data.Decimal from schema
830            // instead of string, as described in <https://github.com/risingwavelabs/risingwave/issues/16852>.
831            // However, Rust doesn't have a library to parse Kafka Connect metadata, so we'll refactor this
832            // after implementing that functionality.
833            let value = base64::engine::general_purpose::STANDARD
834                .decode(str_val)
835                .map_err(|_| create_error())?;
836            let unscaled = num_bigint::BigInt::from_signed_bytes_be(&value);
837            Decimal::from_str(&unscaled.to_string()).map_err(|_| create_error())
838        }
839        BigintUnsignedHandlingMode::Long => {
840            // In Long mode, don't attempt base64 decoding
841            Err(create_error())
842        }
843    }
844}
845
846pub struct JsonAccess<'a> {
847    value: BorrowedValue<'a>,
848    options: &'a JsonParseOptions,
849}
850
851impl<'a> JsonAccess<'a> {
852    pub fn new_with_options(value: BorrowedValue<'a>, options: &'a JsonParseOptions) -> Self {
853        Self { value, options }
854    }
855
856    pub fn new(value: BorrowedValue<'a>) -> Self {
857        Self::new_with_options(value, &JsonParseOptions::DEFAULT)
858    }
859}
860
861impl Access for JsonAccess<'_> {
862    fn access<'a>(&'a self, path: &[&str], type_expected: &DataType) -> AccessResult<DatumCow<'a>> {
863        let mut value = &self.value;
864
865        for (idx, &key) in path.iter().enumerate() {
866            if let Some(sub_value) = if self.options.ignoring_keycase {
867                json_object_get_case_insensitive(value, key)
868            } else {
869                value.get(key)
870            } {
871                value = sub_value;
872            } else {
873                Err(AccessError::Undefined {
874                    name: key.to_owned(),
875                    path: path.iter().take(idx).join("."),
876                })?;
877            }
878        }
879
880        self.options.parse(value, type_expected)
881    }
882}
883
884/// Get a value from a json object by key, case insensitive.
885///
886/// Returns `None` if the given json value is not an object, or the key is not found.
887fn json_object_get_case_insensitive<'b>(
888    v: &'b simd_json::BorrowedValue<'b>,
889    key: &str,
890) -> Option<&'b simd_json::BorrowedValue<'b>> {
891    let obj = v.as_object()?;
892    let value = obj.get(key);
893    if value.is_some() {
894        return value; // fast path
895    }
896    for (k, v) in obj {
897        if k.eq_ignore_ascii_case(key) {
898            return Some(v);
899        }
900    }
901    None
902}