Skip to main content

risingwave_connector/parser/unified/
debezium.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;
16
17use itertools::Itertools;
18use risingwave_common::catalog::{ColumnCatalog, ColumnDesc, ColumnId};
19use risingwave_common::id::SourceId;
20use risingwave_common::types::{
21    DataType, Datum, DatumCow, Int256, ListValue, Scalar, ScalarImpl, ScalarRefImpl, StructValue,
22    Timestamp, Timestamptz, ToDatumRef, ToOwnedDatum,
23};
24use risingwave_connector_codec::decoder::AccessExt;
25use risingwave_pb::plan_common::additional_column::ColumnType;
26use thiserror_ext::AsReport;
27
28use super::{Access, AccessError, AccessResult, ChangeEvent, ChangeEventOperation};
29
30/// JDBC type constants found in Debezium schema change events.
31mod debezium_sql_types {
32    pub const STRUCT: i32 = 2002;
33    pub const ARRAY: i32 = 2003;
34}
35use crate::connector_common::{create_pg_client_from_properties, discover_pgvector_dimensions};
36use crate::parser::TransactionControl;
37use crate::parser::debezium::schema_change::{SchemaChangeEnvelope, TableSchemaChange};
38use crate::parser::schema_change::TableChangeType;
39use crate::source::cdc::build_cdc_table_id;
40use crate::source::cdc::external::mysql::{
41    mysql_type_to_rw_type, timestamp_val_to_timestamptz, type_name_to_mysql_type,
42};
43use crate::source::cdc::external::postgres::{pg_type_to_rw_type, type_name_to_pg_type};
44use crate::source::{ConnectorProperties, SourceColumnDesc};
45
46/// Parse Debezium `tableChanges[].id` into `(schema_name, table_name)`.
47///
48/// Input examples observed in Debezium schema-change events:
49/// - `"public"."orders"` (quoted format)
50/// - `public.orders` (plain dotted format)
51/// - `db.public.orders` (database-prefixed dotted format)
52///
53/// Why two parsing branches:
54/// - Different Debezium versions/connectors may emit quoted or plain identifiers.
55/// - We normalize both forms so downstream lookup can consistently query upstream catalogs.
56///
57/// Output:
58/// - `Some((schema, table))` when both schema and table are successfully extracted.
59/// - `None` when the id is malformed or does not contain enough segments.
60fn parse_schema_table_from_debezium_id(id: &str) -> Option<(String, String)> {
61    let trimmed = id.trim();
62    if trimmed.contains("\".\"") {
63        let parts = trimmed
64            .split("\".\"")
65            .map(|s| s.trim_matches('"').trim())
66            .collect_vec();
67        if parts.len() >= 2 {
68            let schema = parts[parts.len() - 2].to_owned();
69            let table = parts[parts.len() - 1].to_owned();
70            if !schema.is_empty() && !table.is_empty() {
71                return Some((schema, table));
72            }
73        }
74    }
75
76    let cleaned = trimmed.trim_matches('"');
77    let parts = cleaned.split('.').collect_vec();
78    if parts.len() >= 2 {
79        let schema = parts[parts.len() - 2].trim().to_owned();
80        let table = parts[parts.len() - 1].trim().to_owned();
81        if !schema.is_empty() && !table.is_empty() {
82            return Some((schema, table));
83        }
84    }
85    None
86}
87
88async fn fetch_pgvector_dimensions_for_table(
89    connector_props: &ConnectorProperties,
90    schema: &str,
91    table: &str,
92) -> AccessResult<std::collections::HashMap<String, usize>> {
93    let ConnectorProperties::PostgresCdc(cdc_props) = connector_props else {
94        return Ok(std::collections::HashMap::new());
95    };
96
97    let client = create_pg_client_from_properties(&cdc_props.properties, None)
98        .await
99        .map_err(|err| AccessError::Uncategorized {
100            message: format!(
101                "failed to connect upstream postgres for schema change lookup: {}",
102                err.as_report()
103            ),
104        })?;
105
106    discover_pgvector_dimensions(&client, schema, table)
107        .await
108        .map_err(|err| AccessError::Uncategorized {
109            message: format!(
110                "failed to query upstream postgres schema for {schema}.{table}: {}",
111                err.as_report()
112            ),
113        })
114}
115
116/// Decide whether an unknown array column type (one not resolved by
117/// `type_name_to_pg_type`) can be represented as `varchar[]` in RW. This is the
118/// single question this function answers; the criteria behind it may grow over
119/// time.
120///
121/// Right now the answer is "yes iff the array's element type is a user-defined
122/// enum or composite". Debezium does not expose element enum/composite metadata
123/// on the array column (the `enumValues` field is only set for scalar enum
124/// columns, and `jdbcType == STRUCT` is only set for scalar composite columns),
125/// so we ask the upstream catalog directly: follow the array type's `typelem`
126/// to its element type and check `typtype IN ('e', 'c')`. Enum values are plain
127/// text, and composite values are converted to text by our `CustomConverter`,
128/// hence `varchar[]`.
129async fn can_fallback_array_to_varchar(
130    connector_props: &ConnectorProperties,
131    array_type_name: &str,
132) -> AccessResult<bool> {
133    let ConnectorProperties::PostgresCdc(cdc_props) = connector_props else {
134        return Ok(false);
135    };
136
137    let client = create_pg_client_from_properties(&cdc_props.properties, None)
138        .await
139        .map_err(|err| AccessError::Uncategorized {
140            message: format!(
141                "failed to connect upstream postgres for schema change lookup: {}",
142                err.as_report()
143            ),
144        })?;
145
146    let row = client
147        .query_opt(
148            "SELECT t_elem.typtype IN ('e', 'c') \
149             FROM pg_type t \
150             JOIN pg_type t_elem ON t.typelem = t_elem.oid \
151             WHERE t.typname = $1 \
152             LIMIT 1",
153            &[&array_type_name],
154        )
155        .await
156        .map_err(|err| AccessError::Uncategorized {
157            message: format!(
158                "failed to query upstream postgres for array element type of `{array_type_name}`: {}",
159                err.as_report()
160            ),
161        })?;
162
163    Ok(row.map(|r| r.get::<_, bool>(0)).unwrap_or(false))
164}
165
166// Example of Debezium JSON value:
167// {
168//     "payload":
169//     {
170//         "before": null,
171//         "after":
172//         {
173//             "O_ORDERKEY": 5,
174//             "O_CUSTKEY": 44485,
175//             "O_ORDERSTATUS": "F",
176//             "O_TOTALPRICE": "144659.20",
177//             "O_ORDERDATE": "1994-07-30"
178//         },
179//         "source":
180//         {
181//             "version": "1.9.7.Final",
182//             "connector": "mysql",
183//             "name": "RW_CDC_1002",
184//             "ts_ms": 1695277757000,
185//             "db": "mydb",
186//             "sequence": null,
187//             "table": "orders",
188//             "server_id": 0,
189//             "gtid": null,
190//             "file": "binlog.000008",
191//             "pos": 3693,
192//             "row": 0,
193//         },
194//         "op": "r",
195//         "ts_ms": 1695277757017,
196//         "transaction": null
197//     }
198// }
199pub struct DebeziumChangeEvent<A> {
200    value_accessor: Option<A>,
201    key_accessor: Option<A>,
202    is_mongodb: bool,
203}
204
205const BEFORE: &str = "before";
206const AFTER: &str = "after";
207
208const UPSTREAM_DDL: &str = "ddl";
209const SOURCE: &str = "source";
210const SOURCE_TS_MS: &str = "ts_ms";
211const SOURCE_DB: &str = "db";
212const SOURCE_SCHEMA: &str = "schema";
213const SOURCE_TABLE: &str = "table";
214const SOURCE_COLLECTION: &str = "collection";
215
216const OP: &str = "op";
217pub const TRANSACTION_STATUS: &str = "status";
218pub const TRANSACTION_ID: &str = "id";
219
220pub const TABLE_CHANGES: &str = "tableChanges";
221
222pub const DEBEZIUM_READ_OP: &str = "r";
223pub const DEBEZIUM_CREATE_OP: &str = "c";
224pub const DEBEZIUM_UPDATE_OP: &str = "u";
225pub const DEBEZIUM_DELETE_OP: &str = "d";
226
227pub const DEBEZIUM_TRANSACTION_STATUS_BEGIN: &str = "BEGIN";
228pub const DEBEZIUM_TRANSACTION_STATUS_COMMIT: &str = "END";
229
230pub fn parse_transaction_meta(
231    accessor: &impl Access,
232    connector_props: &ConnectorProperties,
233) -> AccessResult<TransactionControl> {
234    if let (Some(ScalarRefImpl::Utf8(status)), Some(ScalarRefImpl::Utf8(id))) = (
235        accessor
236            .access(&[TRANSACTION_STATUS], &DataType::Varchar)?
237            .to_datum_ref(),
238        accessor
239            .access(&[TRANSACTION_ID], &DataType::Varchar)?
240            .to_datum_ref(),
241    ) {
242        // The id field has different meanings for different databases:
243        // PG: txID:LSN
244        // MySQL: source_id:transaction_id (e.g. 3E11FA47-71CA-11E1-9E33-C80AA9429562:23)
245        // SQL Server: commit_lsn (e.g. 00000027:00000ac0:0002)
246        match status {
247            DEBEZIUM_TRANSACTION_STATUS_BEGIN => match *connector_props {
248                ConnectorProperties::PostgresCdc(_) => {
249                    let (tx_id, _) = id.split_once(':').unwrap();
250                    return Ok(TransactionControl::Begin { id: tx_id.into() });
251                }
252                ConnectorProperties::MysqlCdc(_) => {
253                    return Ok(TransactionControl::Begin { id: id.into() });
254                }
255                ConnectorProperties::SqlServerCdc(_) => {
256                    return Ok(TransactionControl::Begin { id: id.into() });
257                }
258                _ => {}
259            },
260            DEBEZIUM_TRANSACTION_STATUS_COMMIT => match *connector_props {
261                ConnectorProperties::PostgresCdc(_) => {
262                    let (tx_id, _) = id.split_once(':').unwrap();
263                    return Ok(TransactionControl::Commit { id: tx_id.into() });
264                }
265                ConnectorProperties::MysqlCdc(_) => {
266                    return Ok(TransactionControl::Commit { id: id.into() });
267                }
268                ConnectorProperties::SqlServerCdc(_) => {
269                    return Ok(TransactionControl::Commit { id: id.into() });
270                }
271                _ => {}
272            },
273            _ => {}
274        }
275    }
276
277    Err(AccessError::Undefined {
278        name: "transaction status".into(),
279        path: TRANSACTION_STATUS.into(),
280    })
281}
282
283macro_rules! jsonb_access_field {
284    ($col:expr, $field:expr, $as_type:tt) => {
285        $crate::paste! {
286            $col.access_object_field($field).unwrap().[<as_ $as_type>]().unwrap()
287        }
288    };
289}
290
291/// Parse the schema change message from Debezium.
292/// The layout of MySQL schema change message can refer to
293/// <https://debezium.io/documentation/reference/2.6/connectors/mysql.html#mysql-schema-change-topic>
294pub async fn parse_schema_change(
295    accessor: &impl Access,
296    source_id: SourceId,
297    source_name: &str,
298    connector_props: &ConnectorProperties,
299) -> AccessResult<SchemaChangeEnvelope> {
300    let mut schema_changes = vec![];
301    let mut pgvector_dims_cache: std::collections::HashMap<
302        (String, String),
303        std::collections::HashMap<String, usize>,
304    > = std::collections::HashMap::new();
305
306    let upstream_ddl: String = accessor
307        .access(&[UPSTREAM_DDL], &DataType::Varchar)?
308        .to_owned_datum()
309        .unwrap()
310        .as_utf8()
311        .to_string();
312
313    if let Some(ScalarRefImpl::List(table_changes)) = accessor
314        .access(&[TABLE_CHANGES], &DataType::Jsonb.list())?
315        .to_datum_ref()
316    {
317        for datum in table_changes.iter() {
318            let jsonb = match datum {
319                Some(ScalarRefImpl::Jsonb(jsonb)) => jsonb,
320                _ => unreachable!(""),
321            };
322            let id: String = jsonb_access_field!(jsonb, "id", string);
323            let ty = jsonb_access_field!(jsonb, "type", string);
324
325            let table_name = id.trim_matches('"').to_owned();
326            let ddl_type: TableChangeType = ty.as_str().into();
327            if matches!(ddl_type, TableChangeType::Create | TableChangeType::Drop) {
328                tracing::debug!("skip table schema change for create/drop command");
329                continue;
330            }
331
332            let mut column_descs: Vec<ColumnDesc> = vec![];
333            if let Some(table) = jsonb.access_object_field("table")
334                && let Some(columns) = table.access_object_field("columns")
335            {
336                for col in columns.array_elements().unwrap() {
337                    let name = jsonb_access_field!(col, "name", string);
338                    let type_name = jsonb_access_field!(col, "typeName", string);
339                    // User-defined types (enum, composite) are not in the
340                    // `type_name_to_pg_type` whitelist because their type names are
341                    // user-chosen. We detect them via Debezium metadata instead:
342                    //  - Enum: has a non-null `enumValues` field in the column descriptor.
343                    //  - Composite (STRUCT): identified by `jdbcType == 2002`.
344                    // Both are mapped to Varchar — enum values are plain strings, and
345                    // composite values are converted to text by our CustomConverter.
346                    let is_enum = matches!(col.access_object_field("enumValues"), Some(val) if !val.is_jsonb_null());
347                    let jdbc_type = col
348                        .access_object_field("jdbcType")
349                        .and_then(|v| v.as_number().ok())
350                        .map(|n| n.0 as i32);
351                    let is_composite = jdbc_type == Some(debezium_sql_types::STRUCT);
352
353                    let data_type = match *connector_props {
354                        ConnectorProperties::PostgresCdc(_) => {
355                            if is_composite || is_enum {
356                                tracing::debug!(target: "auto_schema_change",
357                                    "Convert PostgreSQL user-defined type '{}' ({}) to VARCHAR",
358                                    type_name,
359                                    if is_composite { "composite" } else { "enum" });
360                                DataType::Varchar
361                            } else if type_name.eq_ignore_ascii_case("vector") {
362                                let Some((schema_name, table_name_only)) =
363                                    parse_schema_table_from_debezium_id(id.as_str())
364                                else {
365                                    return Err(AccessError::CdcAutoSchemaChangeError {
366                                        ty: type_name,
367                                        table_name: format!("{}.{}", source_name, table_name),
368                                    });
369                                };
370
371                                // Cache by normalized `(schema, table)` tuple to avoid split/join churn on id text.
372                                let cache_key = (schema_name, table_name_only);
373                                if !pgvector_dims_cache.contains_key(&cache_key) {
374                                    let fetched = fetch_pgvector_dimensions_for_table(
375                                        connector_props,
376                                        &cache_key.0,
377                                        &cache_key.1,
378                                    )
379                                    .await?;
380                                    pgvector_dims_cache.insert(cache_key.clone(), fetched);
381                                }
382
383                                match pgvector_dims_cache
384                                    .get(&cache_key)
385                                    .and_then(|m| m.get(name.as_str()).copied())
386                                {
387                                    Some(dim) if (1..=DataType::VEC_MAX_SIZE).contains(&dim) => {
388                                        DataType::Vector(dim)
389                                    }
390                                    _ => {
391                                        // No fallback to VARCHAR: a dimension-less vector cannot be mapped to
392                                        // RW's required `vector(n)` type safely.
393                                        return Err(AccessError::CdcAutoSchemaChangeError {
394                                            ty: type_name,
395                                            table_name: format!("{}.{}", source_name, table_name),
396                                        });
397                                    }
398                                }
399                            } else {
400                                // Resolve builtin types first, so arrays of builtin element
401                                // types keep their proper element type (e.g. `_int4` -> int[],
402                                // `_text` -> text[]). `type_name_to_pg_type` only covers PG
403                                // builtins (the `PgType` it returns carries a static OID);
404                                // user-defined and extension types are not representable here
405                                // and fall through to the catalog lookup below.
406                                let ty = type_name_to_pg_type(type_name.as_str());
407                                match ty {
408                                    Some(ty) => match pg_type_to_rw_type(&ty) {
409                                        Ok(data_type) => data_type,
410                                        Err(err) => {
411                                            tracing::warn!(error=%err.as_report(), "unsupported postgres type in schema change message");
412                                            return Err(AccessError::CdcAutoSchemaChangeError {
413                                                ty: type_name,
414                                                table_name: format!(
415                                                    "{}.{}",
416                                                    source_name, table_name
417                                                ),
418                                            });
419                                        }
420                                    },
421                                    // An unrecognized ARRAY type may be an array of a
422                                    // user-defined enum (e.g. `_mood_enum`). Debezium carries
423                                    // no enum metadata on the array column, so ask upstream
424                                    // whether the element is an enum; if so, map to
425                                    // `varchar[]` (enum values are plain text).
426                                    None if jdbc_type == Some(debezium_sql_types::ARRAY)
427                                        && can_fallback_array_to_varchar(
428                                            connector_props,
429                                            type_name.as_str(),
430                                        )
431                                        .await? =>
432                                    {
433                                        tracing::debug!(target: "auto_schema_change",
434                                            "Fall back PostgreSQL array type '{}' to VARCHAR[]",
435                                            type_name);
436                                        DataType::Varchar.list()
437                                    }
438                                    None => {
439                                        return Err(AccessError::CdcAutoSchemaChangeError {
440                                            ty: type_name,
441                                            table_name: format!("{}.{}", source_name, table_name),
442                                        });
443                                    }
444                                }
445                            }
446                        }
447                        ConnectorProperties::MysqlCdc(_) => {
448                            let ty = type_name_to_mysql_type(type_name.as_str());
449                            match ty {
450                                Some(ty) => match mysql_type_to_rw_type(&ty) {
451                                    Ok(data_type) => data_type,
452                                    Err(err) => {
453                                        tracing::warn!(error=%err.as_report(), "unsupported mysql type in schema change message");
454                                        return Err(AccessError::CdcAutoSchemaChangeError {
455                                            ty: type_name,
456                                            table_name: format!("{}.{}", source_name, table_name),
457                                        });
458                                    }
459                                },
460                                None => {
461                                    return Err(AccessError::CdcAutoSchemaChangeError {
462                                        ty: type_name,
463                                        table_name: format!("{}.{}", source_name, table_name),
464                                    });
465                                }
466                            }
467                        }
468                        _ => {
469                            unreachable!()
470                        }
471                    };
472
473                    // Handle default value expression. Non-constant defaults (`now()`,
474                    // `gen_random_uuid()`, `nextval('seq'::regclass)` for BIGSERIAL, etc.)
475                    // will fail `ScalarImpl::from_text` and fall through to the fail-open
476                    // branch below — the column is added without a default and a warning is
477                    // logged, rather than aborting the whole auto schema change.
478                    //
479                    // TODO: the schema change event carries the **full** set of columns of
480                    // the table, so here we cannot tell "columns newly added by this ALTER"
481                    // from "columns that already existed". A better approach would be to
482                    // only process the delta columns that actually changed in this event,
483                    // which would let us precisely tell the user: which columns have
484                    // existing rows filled with NULL (needs attention), and which ones are
485                    // pre-existing columns merely surfaced by the event (harmless, can be
486                    // ignored silently).
487                    let column_desc = match col.access_object_field("defaultValueExpression") {
488                        Some(default_val_expr_str) if !default_val_expr_str.is_jsonb_null() => {
489                            let default_val_expr_str = default_val_expr_str.as_str().unwrap();
490                            let value_text: Option<String>;
491                            match *connector_props {
492                                ConnectorProperties::PostgresCdc(_) => {
493                                    // default value of non-number data type will be stored as
494                                    // "'value'::type"
495                                    match default_val_expr_str
496                                        .split("::")
497                                        .map(|s| s.trim_matches('\''))
498                                        .next()
499                                    {
500                                        None => {
501                                            value_text = None;
502                                        }
503                                        Some(val_text) => {
504                                            value_text = Some(val_text.to_owned());
505                                        }
506                                    }
507                                }
508                                ConnectorProperties::MysqlCdc(_) => {
509                                    // mysql timestamp is mapped to timestamptz, we use UTC timezone to
510                                    // interpret its value
511                                    if data_type == DataType::Timestamptz {
512                                        value_text = Some(timestamp_val_to_timestamptz(default_val_expr_str).map_err(|err| {
513                                            tracing::error!(target: "auto_schema_change", error=%err.as_report(), "failed to convert timestamp value to timestamptz");
514                                            AccessError::TypeError {
515                                                expected: "timestamp in YYYY-MM-DD HH:MM:SS".into(),
516                                                got: data_type.to_string(),
517                                                value: default_val_expr_str.to_owned(),
518                                            }
519                                        })?);
520                                    } else {
521                                        value_text = Some(default_val_expr_str.to_owned());
522                                    }
523                                }
524                                _ => {
525                                    unreachable!("connector doesn't support schema change")
526                                }
527                            }
528
529                            let snapshot_value: Datum = value_text.and_then(|value_text| {
530                                ScalarImpl::from_text(value_text.as_str(), &data_type)
531                                    .inspect_err(|err| {
532                                        tracing::warn!(
533                                            target: "auto_schema_change",
534                                            error = %err.as_report(),
535                                            column = %name,
536                                            data_type = %data_type,
537                                            default_value_expression = default_val_expr_str,
538                                            upstream_ddl = %upstream_ddl,
539                                            "non-constant default expression, column added without default. \
540                                             If this column is not newly added by this schema change, it is safe to ignore this warning. \
541                                             If this column is newly added by this schema change, existing rows will be NULL in this column — consider using COALESCE in queries to provide a fallback value."
542                                        );
543                                    })
544                                    .ok()
545                            });
546
547                            if snapshot_value.is_none() {
548                                ColumnDesc::named(name, ColumnId::placeholder(), data_type)
549                            } else {
550                                ColumnDesc::named_with_default_value(
551                                    name,
552                                    ColumnId::placeholder(),
553                                    data_type,
554                                    snapshot_value,
555                                )
556                            }
557                        }
558                        _ => ColumnDesc::named(name, ColumnId::placeholder(), data_type),
559                    };
560                    column_descs.push(column_desc);
561                }
562            }
563
564            // concatenate the source_id to the cdc_table_id
565            let cdc_table_id = build_cdc_table_id(source_id, id.replace('"', "").as_str());
566            schema_changes.push(TableSchemaChange {
567                cdc_table_id,
568                columns: column_descs
569                    .into_iter()
570                    .map(|column_desc| ColumnCatalog {
571                        column_desc,
572                        is_hidden: false,
573                    })
574                    .collect_vec(),
575                change_type: ty.as_str().into(),
576                upstream_ddl: upstream_ddl.clone(),
577            });
578        }
579
580        Ok(SchemaChangeEnvelope {
581            table_changes: schema_changes,
582        })
583    } else {
584        Err(AccessError::Undefined {
585            name: "table schema change".into(),
586            path: TABLE_CHANGES.into(),
587        })
588    }
589}
590
591impl<A> DebeziumChangeEvent<A>
592where
593    A: Access,
594{
595    /// Panic: one of the `key_accessor` or `value_accessor` must be provided.
596    pub fn new(key_accessor: Option<A>, value_accessor: Option<A>) -> Self {
597        assert!(key_accessor.is_some() || value_accessor.is_some());
598        Self {
599            value_accessor,
600            key_accessor,
601            is_mongodb: false,
602        }
603    }
604
605    pub fn new_mongodb_event(key_accessor: Option<A>, value_accessor: Option<A>) -> Self {
606        assert!(key_accessor.is_some() || value_accessor.is_some());
607        Self {
608            value_accessor,
609            key_accessor,
610            is_mongodb: true,
611        }
612    }
613
614    /// Returns the transaction metadata if exists.
615    ///
616    /// See the [doc](https://debezium.io/documentation/reference/2.3/connectors/postgresql.html#postgresql-transaction-metadata) of Debezium for more details.
617    pub(crate) fn transaction_control(
618        &self,
619        connector_props: &ConnectorProperties,
620    ) -> Option<TransactionControl> {
621        // Ignore if `value_accessor` is not provided or there's any error when
622        // trying to parse the transaction metadata.
623        self.value_accessor
624            .as_ref()
625            .and_then(|accessor| parse_transaction_meta(accessor, connector_props).ok())
626    }
627}
628
629impl<A> ChangeEvent for DebeziumChangeEvent<A>
630where
631    A: Access,
632{
633    fn access_field(&self, desc: &SourceColumnDesc) -> super::AccessResult<DatumCow<'_>> {
634        match self.op()? {
635            ChangeEventOperation::Delete => {
636                // For delete events of MongoDB, the "before" and "after" field both are null in the value,
637                // we need to extract the _id field from the key.
638                if self.is_mongodb && desc.name == "_id" {
639                    return self
640                        .key_accessor
641                        .as_ref()
642                        .expect("key_accessor must be provided for delete operation")
643                        .access(&[&desc.name], &desc.data_type);
644                }
645
646                if let Some(va) = self.value_accessor.as_ref() {
647                    va.access(&[BEFORE, &desc.name], &desc.data_type)
648                } else {
649                    self.key_accessor
650                        .as_ref()
651                        .unwrap()
652                        .access(&[&desc.name], &desc.data_type)
653                }
654            }
655
656            // value should not be None.
657            ChangeEventOperation::Upsert => {
658                // For upsert operation, if desc is an additional column, access field in the `SOURCE` field.
659                desc.additional_column.column_type.as_ref().map_or_else(
660                    || {
661                        self.value_accessor
662                            .as_ref()
663                            .expect("value_accessor must be provided for upsert operation")
664                            .access(&[AFTER, &desc.name], &desc.data_type)
665                    },
666                    |additional_column_type| {
667                        match *additional_column_type {
668                            ColumnType::Timestamp(_) => {
669                                // access payload.source.ts_ms
670                                let ts_ms = self
671                                    .value_accessor
672                                    .as_ref()
673                                    .expect("value_accessor must be provided for upsert operation")
674                                    .access_owned(&[SOURCE, SOURCE_TS_MS], &DataType::Int64)?;
675                                Ok(DatumCow::Owned(ts_ms.map(|scalar| {
676                                    Timestamptz::from_millis(scalar.into_int64())
677                                        .expect("source.ts_ms must in millisecond")
678                                        .to_scalar_value()
679                                })))
680                            }
681                            ColumnType::DatabaseName(_) => self
682                                .value_accessor
683                                .as_ref()
684                                .expect("value_accessor must be provided for upsert operation")
685                                .access(&[SOURCE, SOURCE_DB], &desc.data_type),
686                            ColumnType::SchemaName(_) => self
687                                .value_accessor
688                                .as_ref()
689                                .expect("value_accessor must be provided for upsert operation")
690                                .access(&[SOURCE, SOURCE_SCHEMA], &desc.data_type),
691                            ColumnType::TableName(_) => self
692                                .value_accessor
693                                .as_ref()
694                                .expect("value_accessor must be provided for upsert operation")
695                                .access(&[SOURCE, SOURCE_TABLE], &desc.data_type),
696                            ColumnType::CollectionName(_) => self
697                                .value_accessor
698                                .as_ref()
699                                .expect("value_accessor must be provided for upsert operation")
700                                .access(&[SOURCE, SOURCE_COLLECTION], &desc.data_type),
701                            _ => Err(AccessError::UnsupportedAdditionalColumn {
702                                name: desc.name.clone(),
703                            }),
704                        }
705                    },
706                )
707            }
708        }
709    }
710
711    fn op(&self) -> Result<ChangeEventOperation, AccessError> {
712        if let Some(accessor) = &self.value_accessor {
713            if let Some(ScalarRefImpl::Utf8(op)) =
714                accessor.access(&[OP], &DataType::Varchar)?.to_datum_ref()
715            {
716                match op {
717                    DEBEZIUM_READ_OP | DEBEZIUM_CREATE_OP | DEBEZIUM_UPDATE_OP => {
718                        return Ok(ChangeEventOperation::Upsert);
719                    }
720                    DEBEZIUM_DELETE_OP => return Ok(ChangeEventOperation::Delete),
721                    _ => (),
722                }
723            }
724            Err(super::AccessError::Undefined {
725                name: "op".into(),
726                path: Default::default(),
727            })
728        } else {
729            Ok(ChangeEventOperation::Delete)
730        }
731    }
732}
733
734/// Access support for Mongo
735///
736/// For now, we considerate `strong_schema` typed `MongoDB` Debezium event jsons only.
737pub struct MongoJsonAccess<A> {
738    accessor: A,
739    strong_schema: bool,
740}
741
742pub fn extract_bson_id(id_type: &DataType, bson_doc: &serde_json::Value) -> AccessResult {
743    let id_field = if let Some(value) = bson_doc.get("_id") {
744        value
745    } else {
746        bson_doc
747    };
748
749    let type_error = || AccessError::TypeError {
750        expected: id_type.to_string(),
751        got: match id_field {
752            serde_json::Value::Null => "null",
753            serde_json::Value::Bool(_) => "bool",
754            serde_json::Value::Number(_) => "number",
755            serde_json::Value::String(_) => "string",
756            serde_json::Value::Array(_) => "array",
757            serde_json::Value::Object(_) => "object",
758        }
759        .to_owned(),
760        value: id_field.to_string(),
761    };
762
763    let id: Datum = match id_type {
764        DataType::Jsonb => ScalarImpl::Jsonb(id_field.clone().into()).into(),
765        DataType::Varchar => match id_field {
766            serde_json::Value::String(s) => Some(ScalarImpl::Utf8(s.clone().into())),
767            serde_json::Value::Object(obj) if obj.contains_key("$oid") => Some(ScalarImpl::Utf8(
768                obj["$oid"].as_str().unwrap_or_default().into(),
769            )),
770            _ => return Err(type_error()),
771        },
772        DataType::Int32 => {
773            if let serde_json::Value::Object(obj) = id_field
774                && obj.contains_key("$numberInt")
775            {
776                let int_str = obj["$numberInt"].as_str().unwrap_or_default();
777                Some(ScalarImpl::Int32(int_str.parse().unwrap_or_default()))
778            } else {
779                return Err(type_error());
780            }
781        }
782        DataType::Int64 => {
783            if let serde_json::Value::Object(obj) = id_field
784                && obj.contains_key("$numberLong")
785            {
786                let int_str = obj["$numberLong"].as_str().unwrap_or_default();
787                Some(ScalarImpl::Int64(int_str.parse().unwrap_or_default()))
788            } else {
789                return Err(type_error());
790            }
791        }
792        _ => unreachable!("DebeziumMongoJsonParser::new must ensure _id column datatypes."),
793    };
794    Ok(id)
795}
796
797/// Extract the field data from the bson document
798///
799/// BSON document is a JSON object with some special fields, such as:
800/// long integer: {"$numberLong": "1630454400000"}
801/// date time: {"$date": {"$numberLong": "1630454400000"}}
802///
803/// For now, we support only the Canonical format of the date and timestamp.
804///
805/// # NOTE:
806///
807/// - `field` indicates the field name in the bson document, if it is None, the `bson_doc` is the field itself.
808// similar to extract the "_id" field from the message payload
809pub fn extract_bson_field(
810    type_expected: &DataType,
811    bson_doc: &serde_json::Value,
812    field: Option<&str>,
813) -> AccessResult {
814    let type_error = |datum: &serde_json::Value| AccessError::TypeError {
815        expected: type_expected.to_string(),
816        got: match bson_doc {
817            serde_json::Value::Null => "null",
818            serde_json::Value::Bool(_) => "bool",
819            serde_json::Value::Number(_) => "number",
820            serde_json::Value::String(_) => "string",
821            serde_json::Value::Array(_) => "array",
822            serde_json::Value::Object(_) => "object",
823        }
824        .to_owned(),
825        value: datum.to_string(),
826    };
827
828    let datum = if let Some(field) = field {
829        let Some(bson_doc) = bson_doc.get(field) else {
830            return Err(type_error(bson_doc));
831        };
832        bson_doc
833    } else {
834        bson_doc
835    };
836
837    if datum.is_null() {
838        return Ok(None);
839    }
840
841    let field_datum: Datum = match type_expected {
842        DataType::Boolean => {
843            if datum.is_boolean() {
844                Some(ScalarImpl::Bool(datum.as_bool().unwrap()))
845            } else {
846                return Err(type_error(datum));
847            }
848        }
849        DataType::Jsonb => ScalarImpl::Jsonb(datum.clone().into()).into(),
850        DataType::Varchar => match datum {
851            serde_json::Value::String(s) => Some(ScalarImpl::Utf8(s.clone().into())),
852            serde_json::Value::Object(obj) if obj.contains_key("$oid") && field == Some("_id") => {
853                obj["oid"].as_str().map(|s| ScalarImpl::Utf8(s.into()))
854            }
855            _ => return Err(type_error(datum)),
856        },
857        DataType::Int16
858        | DataType::Int32
859        | DataType::Int64
860        | DataType::Int256
861        | DataType::Float32
862        | DataType::Float64 => {
863            if !datum.is_object() {
864                return Err(type_error(datum));
865            };
866
867            bson_extract_number(datum, type_expected)?
868        }
869
870        DataType::Date | DataType::Timestamp | DataType::Timestamptz => {
871            if let serde_json::Value::Object(mp) = datum {
872                if mp.contains_key("$timestamp") && mp["$timestamp"].is_object() {
873                    bson_extract_timestamp(datum, type_expected)?
874                } else if mp.contains_key("$date") {
875                    bson_extract_date(datum, type_expected)?
876                } else {
877                    return Err(type_error(datum));
878                }
879            } else {
880                return Err(type_error(datum));
881            }
882        }
883        DataType::Decimal => {
884            if let serde_json::Value::Object(obj) = datum
885                && obj.contains_key("$numberDecimal")
886                && obj["$numberDecimal"].is_string()
887            {
888                let number = obj["$numberDecimal"].as_str().unwrap();
889
890                let dec = risingwave_common::types::Decimal::from_str(number).map_err(|_| {
891                    AccessError::TypeError {
892                        expected: type_expected.to_string(),
893                        got: "unparsable string".into(),
894                        value: number.to_owned(),
895                    }
896                })?;
897                Some(ScalarImpl::Decimal(dec))
898            } else {
899                return Err(type_error(datum));
900            }
901        }
902
903        DataType::Bytea => {
904            if let serde_json::Value::Object(obj) = datum
905                && obj.contains_key("$binary")
906                && obj["$binary"].is_object()
907            {
908                use base64::Engine;
909
910                let binary = obj["$binary"].as_object().unwrap();
911
912                if !binary.contains_key("$base64")
913                    || !binary["$base64"].is_string()
914                    || !binary.contains_key("$subType")
915                    || !binary["$subType"].is_string()
916                {
917                    return Err(AccessError::TypeError {
918                        expected: type_expected.to_string(),
919                        got: "object".into(),
920                        value: datum.to_string(),
921                    });
922                }
923
924                let b64_str = binary["$base64"]
925                    .as_str()
926                    .ok_or_else(|| AccessError::TypeError {
927                        expected: type_expected.to_string(),
928                        got: "object".into(),
929                        value: datum.to_string(),
930                    })?;
931
932                // type is not used for now
933                let _type_str =
934                    binary["$subType"]
935                        .as_str()
936                        .ok_or_else(|| AccessError::TypeError {
937                            expected: type_expected.to_string(),
938                            got: "object".into(),
939                            value: datum.to_string(),
940                        })?;
941
942                let bytes = base64::prelude::BASE64_STANDARD
943                    .decode(b64_str)
944                    .map_err(|_| AccessError::TypeError {
945                        expected: "$binary object with $base64 string and $subType string field"
946                            .to_owned(),
947                        got: "string".to_owned(),
948                        value: bson_doc.to_string(),
949                    })?;
950                let bytea = ScalarImpl::Bytea(bytes.into());
951                Some(bytea)
952            } else {
953                return Err(type_error(datum));
954            }
955        }
956
957        DataType::Struct(struct_fields) => {
958            let mut datums = vec![];
959            for (field_name, field_type) in struct_fields.iter() {
960                let field_datum = extract_bson_field(field_type, datum, Some(field_name))?;
961                datums.push(field_datum);
962            }
963            let value = StructValue::new(datums);
964
965            Some(ScalarImpl::Struct(value))
966        }
967
968        DataType::List(list_type) => {
969            let elem_type = list_type.elem();
970            let Some(d_array) = datum.as_array() else {
971                return Err(type_error(datum));
972            };
973
974            let mut builder = elem_type.create_array_builder(d_array.len());
975            for item in d_array {
976                builder.append(extract_bson_field(elem_type, item, None)?);
977            }
978            Some(ScalarImpl::from(ListValue::new(builder.finish())))
979        }
980
981        _ => {
982            if let Some(field_name) = field {
983                unreachable!(
984                    "DebeziumMongoJsonParser::new must ensure {field_name} column datatypes."
985                )
986            } else {
987                let type_expected = type_expected.to_string();
988                unreachable!(
989                    "DebeziumMongoJsonParser::new must ensure type of `{type_expected}` matches datum `{datum}`"
990                )
991            }
992        }
993    };
994    Ok(field_datum)
995}
996
997fn bson_extract_number(bson_doc: &serde_json::Value, type_expected: &DataType) -> AccessResult {
998    let field_name = match type_expected {
999        DataType::Int16 => "$numberInt",
1000        DataType::Int32 => "$numberInt",
1001        DataType::Int64 => "$numberLong",
1002        DataType::Int256 => "$numberLong",
1003        DataType::Float32 => "$numberDouble",
1004        DataType::Float64 => "$numberDouble",
1005        _ => unreachable!("DebeziumMongoJsonParser::new must ensure column datatypes."),
1006    };
1007
1008    let datum = bson_doc.get(field_name);
1009    if datum.is_none() {
1010        return Err(AccessError::TypeError {
1011            expected: type_expected.to_string(),
1012            got: "object".into(),
1013            value: bson_doc.to_string(),
1014        });
1015    }
1016
1017    let datum = datum.unwrap();
1018
1019    if datum.is_string() {
1020        let Some(num_str) = datum.as_str() else {
1021            return Err(AccessError::TypeError {
1022                expected: type_expected.to_string(),
1023                got: "string".into(),
1024                value: datum.to_string(),
1025            });
1026        };
1027        // parse to float
1028        if [DataType::Float32, DataType::Float64].contains(type_expected) {
1029            match (num_str, type_expected) {
1030                ("Infinity", DataType::Float64) => {
1031                    return Ok(Some(ScalarImpl::Float64(f64::INFINITY.into())));
1032                }
1033                ("Infinity", DataType::Float32) => {
1034                    return Ok(Some(ScalarImpl::Float32(f32::INFINITY.into())));
1035                }
1036                ("-Infinity", DataType::Float64) => {
1037                    return Ok(Some(ScalarImpl::Float64(f64::NEG_INFINITY.into())));
1038                }
1039                ("-Infinity", DataType::Float32) => {
1040                    return Ok(Some(ScalarImpl::Float32(f32::NEG_INFINITY.into())));
1041                }
1042                ("NaN", DataType::Float64) => {
1043                    return Ok(Some(ScalarImpl::Float64(f64::NAN.into())));
1044                }
1045                ("NaN", DataType::Float32) => {
1046                    return Ok(Some(ScalarImpl::Float32(f32::NAN.into())));
1047                }
1048                _ => {}
1049            }
1050
1051            let parsed_num: f64 = match num_str.parse() {
1052                Ok(n) => n,
1053                Err(_e) => {
1054                    return Err(AccessError::TypeError {
1055                        expected: type_expected.to_string(),
1056                        got: "string".into(),
1057                        value: num_str.to_owned(),
1058                    });
1059                }
1060            };
1061            if *type_expected == DataType::Float64 {
1062                return Ok(Some(ScalarImpl::Float64(parsed_num.into())));
1063            } else {
1064                let parsed_num = parsed_num as f32;
1065                return Ok(Some(ScalarImpl::Float32(parsed_num.into())));
1066            }
1067        }
1068        // parse to large int
1069        if *type_expected == DataType::Int256 {
1070            let parsed_num = match Int256::from_str(num_str) {
1071                Ok(n) => n,
1072                Err(_) => {
1073                    return Err(AccessError::TypeError {
1074                        expected: type_expected.to_string(),
1075                        got: "string".into(),
1076                        value: num_str.to_owned(),
1077                    });
1078                }
1079            };
1080            return Ok(Some(ScalarImpl::Int256(parsed_num)));
1081        }
1082
1083        // parse to integer
1084        let parsed_num: i64 = match num_str.parse() {
1085            Ok(n) => n,
1086            Err(_e) => {
1087                return Err(AccessError::TypeError {
1088                    expected: type_expected.to_string(),
1089                    got: "string".into(),
1090                    value: num_str.to_owned(),
1091                });
1092            }
1093        };
1094        match type_expected {
1095            DataType::Int16 => {
1096                if parsed_num < i16::MIN as i64 || parsed_num > i16::MAX as i64 {
1097                    return Err(AccessError::TypeError {
1098                        expected: type_expected.to_string(),
1099                        got: "string".into(),
1100                        value: num_str.to_owned(),
1101                    });
1102                }
1103                return Ok(Some(ScalarImpl::Int16(parsed_num as i16)));
1104            }
1105            DataType::Int32 => {
1106                if parsed_num < i32::MIN as i64 || parsed_num > i32::MAX as i64 {
1107                    return Err(AccessError::TypeError {
1108                        expected: type_expected.to_string(),
1109                        got: "string".into(),
1110                        value: num_str.to_owned(),
1111                    });
1112                }
1113                return Ok(Some(ScalarImpl::Int32(parsed_num as i32)));
1114            }
1115            DataType::Int64 => {
1116                return Ok(Some(ScalarImpl::Int64(parsed_num)));
1117            }
1118            _ => unreachable!("DebeziumMongoJsonParser::new must ensure column datatypes."),
1119        }
1120    }
1121    if datum.is_null() {
1122        return Err(AccessError::TypeError {
1123            expected: type_expected.to_string(),
1124            got: "null".into(),
1125            value: bson_doc.to_string(),
1126        });
1127    }
1128
1129    if datum.is_array() {
1130        return Err(AccessError::TypeError {
1131            expected: type_expected.to_string(),
1132            got: "array".to_owned(),
1133            value: datum.to_string(),
1134        });
1135    }
1136
1137    if datum.is_object() {
1138        return Err(AccessError::TypeError {
1139            expected: type_expected.to_string(),
1140            got: "object".to_owned(),
1141            value: datum.to_string(),
1142        });
1143    }
1144
1145    if datum.is_boolean() {
1146        return Err(AccessError::TypeError {
1147            expected: type_expected.to_string(),
1148            got: "boolean".into(),
1149            value: bson_doc.to_string(),
1150        });
1151    }
1152
1153    if datum.is_number() {
1154        let got_type = if datum.is_f64() { "f64" } else { "i64" };
1155        return Err(AccessError::TypeError {
1156            expected: type_expected.to_string(),
1157            got: got_type.into(),
1158            value: bson_doc.to_string(),
1159        });
1160    }
1161
1162    Err(AccessError::TypeError {
1163        expected: type_expected.to_string(),
1164        got: "unknown".into(),
1165        value: bson_doc.to_string(),
1166    })
1167}
1168
1169fn bson_extract_date(bson_doc: &serde_json::Value, type_expected: &DataType) -> AccessResult {
1170    // according to mongodb extended json v2
1171    // the date could be:
1172    //
1173    // the timestamp type could be:
1174    //
1175    // both Canonical and Relaxed format:
1176    // {"$timestamp": {"t": 1630454400, "i": 1}}
1177    //
1178    // Canonical: {"$date": {"$numberLong": "1630454400000"}}
1179    // date is encoded as number of milliseconds since the Unix epoch
1180    //
1181    // Relaxed: {"$date": "2021-09-01T00:00:00.000Z"}
1182    // date is encoded as ISO8601 string
1183
1184    let datum = &bson_doc["$date"];
1185
1186    let type_error = || AccessError::TypeError {
1187        expected: type_expected.to_string(),
1188        got: match bson_doc {
1189            serde_json::Value::Null => "null",
1190            serde_json::Value::Bool(_) => "bool",
1191            serde_json::Value::Number(_) => "number",
1192            serde_json::Value::String(_) => "string",
1193            serde_json::Value::Array(_) => "array",
1194            serde_json::Value::Object(_) => "object",
1195        }
1196        .to_owned(),
1197        value: datum.to_string(),
1198    };
1199
1200    // deal with the Canonical format only
1201    let millis = match datum {
1202        // Canonical format {"$date": {"$numberLong": "1630454400000"}}
1203        serde_json::Value::Object(obj)
1204            if obj.contains_key("$numberLong") && obj["$numberLong"].is_string() =>
1205        {
1206            obj["$numberLong"]
1207                .as_str()
1208                .unwrap()
1209                .parse::<i64>()
1210                .map_err(|_| AccessError::TypeError {
1211                    expected: "timestamp".into(),
1212                    got: "object".into(),
1213                    value: datum.to_string(),
1214                })?
1215        }
1216        // Relaxed format {"$date": "2021-09-01T00:00:00.000Z"}
1217        serde_json::Value::String(s) => {
1218            let dt =
1219                chrono::DateTime::parse_from_rfc3339(s).map_err(|_| AccessError::TypeError {
1220                    expected: "valid ISO-8601 date string".into(),
1221                    got: "string".into(),
1222                    value: datum.to_string(),
1223                })?;
1224            dt.timestamp_millis()
1225        }
1226
1227        // jsonv1 format
1228        // {"$date": 1630454400000}
1229        serde_json::Value::Number(num) => num.as_i64().ok_or_else(|| AccessError::TypeError {
1230            expected: "timestamp".into(),
1231            got: "number".into(),
1232            value: datum.to_string(),
1233        })?,
1234
1235        _ => return Err(type_error()),
1236    };
1237
1238    let datetime =
1239        chrono::DateTime::from_timestamp_millis(millis).ok_or_else(|| AccessError::TypeError {
1240            expected: "timestamp".into(),
1241            got: "object".into(),
1242            value: datum.to_string(),
1243        })?;
1244
1245    let res = match type_expected {
1246        DataType::Date => {
1247            let naive = datetime.naive_local();
1248            let dt = naive.date();
1249            Some(ScalarImpl::Date(dt.into()))
1250        }
1251        DataType::Time => {
1252            let naive = datetime.naive_local();
1253            let dt = naive.time();
1254            Some(ScalarImpl::Time(dt.into()))
1255        }
1256        DataType::Timestamp => {
1257            let naive = datetime.naive_local();
1258            let dt = Timestamp::from(naive);
1259            Some(ScalarImpl::Timestamp(dt))
1260        }
1261        DataType::Timestamptz => {
1262            let dt = datetime.into();
1263            Some(ScalarImpl::Timestamptz(dt))
1264        }
1265        _ => unreachable!("DebeziumMongoJsonParser::new must ensure column datatypes."),
1266    };
1267    Ok(res)
1268}
1269
1270fn bson_extract_timestamp(bson_doc: &serde_json::Value, type_expected: &DataType) -> AccessResult {
1271    // according to mongodb extended json v2
1272    // the date could be:
1273    //
1274    // the timestamp type could be:
1275    //
1276    // both Canonical and Relaxed format:
1277    // {"$timestamp": {"t": 1630454400, "i": 1}}
1278    // t is the number of seconds since the Unix epoch
1279    //
1280    // Canonical: {"$date": {"$numberLong": "1630454400000"}}
1281    // date is encoded as number of milliseconds since the Unix epoch
1282    //
1283    // Relaxed: {"$date": "2021-09-01T00:00:00.000Z"}
1284    // date is encoded as ISO8601 string
1285    //
1286    // *For now, we support the Canonical format only.*
1287
1288    let Some(obj) = bson_doc["$timestamp"].as_object() else {
1289        return Err(AccessError::TypeError {
1290            expected: "timestamp".into(),
1291            got: "object".into(),
1292            value: bson_doc.to_string(),
1293        });
1294    };
1295
1296    if !obj.contains_key("t") || !obj["t"].is_u64() || !obj.contains_key("i") || !obj["i"].is_u64()
1297    {
1298        return Err(AccessError::TypeError {
1299            expected: "timestamp with valid seconds since epoch".into(),
1300            got: "object".into(),
1301            value: bson_doc.to_string(),
1302        });
1303    }
1304
1305    let since_epoch = obj["t"].as_i64().ok_or_else(|| AccessError::TypeError {
1306        expected: "timestamp with valid seconds since epoch".into(),
1307        got: "object".into(),
1308        value: bson_doc.to_string(),
1309    })?;
1310
1311    let chrono_datetime =
1312        chrono::DateTime::from_timestamp(since_epoch, 0).ok_or_else(|| AccessError::TypeError {
1313            expected: type_expected.to_string(),
1314            got: "object".to_owned(),
1315            value: bson_doc.to_string(),
1316        })?;
1317
1318    let res = match type_expected {
1319        DataType::Date => {
1320            let naive = chrono_datetime.naive_local();
1321            let dt = naive.date();
1322            Some(ScalarImpl::Date(dt.into()))
1323        }
1324        DataType::Time => {
1325            let naive = chrono_datetime.naive_local();
1326            let dt = naive.time();
1327            Some(ScalarImpl::Time(dt.into()))
1328        }
1329        DataType::Timestamp => {
1330            let naive = chrono_datetime.naive_local();
1331            let dt = Timestamp::from(naive);
1332            Some(ScalarImpl::Timestamp(dt))
1333        }
1334        DataType::Timestamptz => {
1335            let dt = chrono_datetime.into();
1336            Some(ScalarImpl::Timestamptz(dt))
1337        }
1338        _ => unreachable!("DebeziumMongoJsonParser::new must ensure column datatypes."),
1339    };
1340
1341    Ok(res)
1342}
1343
1344impl<A> MongoJsonAccess<A> {
1345    pub fn new(accessor: A, strong_schema: bool) -> Self {
1346        Self {
1347            accessor,
1348            strong_schema,
1349        }
1350    }
1351}
1352
1353impl<A> Access for MongoJsonAccess<A>
1354where
1355    A: Access,
1356{
1357    fn access<'a>(&'a self, path: &[&str], type_expected: &DataType) -> AccessResult<DatumCow<'a>> {
1358        match path {
1359            ["after" | "before", "_id"] => {
1360                let payload = self.access_owned(&[path[0]], &DataType::Jsonb)?;
1361                if let Some(ScalarImpl::Jsonb(bson_doc)) = payload {
1362                    Ok(extract_bson_id(type_expected, &bson_doc.take())?.into())
1363                } else {
1364                    // fail to extract the "_id" field from the message payload
1365                    Err(AccessError::Undefined {
1366                        name: "_id".to_owned(),
1367                        path: path[0].to_owned(),
1368                    })?
1369                }
1370            }
1371
1372            ["after" | "before", "payload"] if !self.strong_schema => {
1373                self.access(&[path[0]], &DataType::Jsonb)
1374            }
1375
1376            ["after" | "before", field] if self.strong_schema => {
1377                let payload = self.access_owned(&[path[0]], &DataType::Jsonb)?;
1378                if let Some(ScalarImpl::Jsonb(bson_doc)) = payload {
1379                    Ok(extract_bson_field(type_expected, &bson_doc.take(), Some(field))?.into())
1380                } else {
1381                    // fail to extract the expected field from the message payload
1382                    Err(AccessError::Undefined {
1383                        name: field.to_string(),
1384                        path: path[0].to_owned(),
1385                    })?
1386                }
1387            }
1388
1389            // To handle a DELETE message, we need to extract the "_id" field from the message key, because it is not in the payload.
1390            // In addition, the "_id" field is named as "id" in the key. An example of message key:
1391            // {"schema":null,"payload":{"id":"{\"$oid\": \"65bc9fb6c485f419a7a877fe\"}"}}
1392            ["_id"] => {
1393                let ret = self.accessor.access(path, type_expected);
1394                if matches!(ret, Err(AccessError::Undefined { .. })) {
1395                    let id_bson = self.accessor.access_owned(&["id"], &DataType::Jsonb)?;
1396                    if let Some(ScalarImpl::Jsonb(bson_doc)) = id_bson {
1397                        Ok(extract_bson_id(type_expected, &bson_doc.take())?.into())
1398                    } else {
1399                        // fail to extract the "_id" field from the message key
1400                        Err(AccessError::Undefined {
1401                            name: "_id".to_owned(),
1402                            path: "id".to_owned(),
1403                        })?
1404                    }
1405                } else {
1406                    ret
1407                }
1408            }
1409            _ => self.accessor.access(path, type_expected),
1410        }
1411    }
1412}