1use 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
30mod 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
46fn 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
116async 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
166pub 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 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
291pub 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 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 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 return Err(AccessError::CdcAutoSchemaChangeError {
394 ty: type_name,
395 table_name: format!("{}.{}", source_name, table_name),
396 });
397 }
398 }
399 } else {
400 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 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 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 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 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 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 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 pub(crate) fn transaction_control(
618 &self,
619 connector_props: &ConnectorProperties,
620 ) -> Option<TransactionControl> {
621 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 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 ChangeEventOperation::Upsert => {
658 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 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
734pub 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
797pub 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 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 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 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 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 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 let millis = match datum {
1202 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 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 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 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 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 Err(AccessError::Undefined {
1383 name: field.to_string(),
1384 path: path[0].to_owned(),
1385 })?
1386 }
1387 }
1388
1389 ["_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 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}