1use std::borrow::Cow;
13use std::fmt::Debug;
14use std::time::Duration;
15
16use mz_dyncfg::ConfigSet;
17use mz_expr::MirScalarExpr;
18use mz_pgcopy::CopyFormatParams;
19use mz_repr::bytes::ByteSize;
20use mz_repr::{CatalogItemId, GlobalId, RelationDesc};
21#[cfg(any(test, feature = "proptest"))]
22use proptest_derive::Arbitrary;
23use serde::{Deserialize, Serialize};
24use timely::PartialOrder;
25use timely::progress::frontier::Antichain;
26
27use crate::AlterCompatible;
28use crate::connections::inline::{
29 ConnectionAccess, ConnectionResolver, InlinedConnection, IntoInlineConnection,
30 ReferencedConnection,
31};
32use crate::connections::{ConnectionContext, KafkaConnection, KafkaTopicOptions};
33use crate::controller::AlterError;
34use crate::wire_format::WireFormat;
35
36pub mod s3_oneshot_sink;
37
38#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
40pub struct StorageSinkDesc<S, T = mz_repr::Timestamp> {
41 pub from: GlobalId,
42 pub from_desc: RelationDesc,
43 pub connection: StorageSinkConnection,
44 pub with_snapshot: bool,
45 pub version: u64,
46 pub envelope: SinkEnvelope,
47 pub as_of: Antichain<T>,
48 pub from_storage_metadata: S,
49 pub to_storage_metadata: S,
50 pub commit_interval: Option<Duration>,
56}
57
58impl<S: Debug + PartialEq, T: Debug + PartialEq + PartialOrder> AlterCompatible
59 for StorageSinkDesc<S, T>
60{
61 fn alter_compatible(
69 &self,
70 id: GlobalId,
71 other: &StorageSinkDesc<S, T>,
72 ) -> Result<(), AlterError> {
73 if self == other {
74 return Ok(());
75 }
76 let StorageSinkDesc {
77 from,
78 from_desc,
79 connection,
80 envelope,
81 version: _,
82 as_of: _,
84 from_storage_metadata,
85 with_snapshot,
86 to_storage_metadata,
87 commit_interval: _,
88 } = self;
89
90 let compatibility_checks = [
91 (from == &other.from, "from"),
92 (from_desc == &other.from_desc, "from_desc"),
93 (
94 connection.alter_compatible(id, &other.connection).is_ok(),
95 "connection",
96 ),
97 (envelope == &other.envelope, "envelope"),
98 (*with_snapshot || !other.with_snapshot, "with_snapshot"),
101 (
102 from_storage_metadata == &other.from_storage_metadata,
103 "from_storage_metadata",
104 ),
105 (
106 to_storage_metadata == &other.to_storage_metadata,
107 "to_storage_metadata",
108 ),
109 ];
110
111 for (compatible, field) in compatibility_checks {
112 if !compatible {
113 tracing::warn!(
114 "StorageSinkDesc incompatible at {field}:\nself:\n{:#?}\n\nother\n{:#?}",
115 self,
116 other
117 );
118
119 return Err(AlterError { id });
120 }
121 }
122
123 Ok(())
124 }
125}
126
127#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
128pub enum SinkEnvelope {
129 Debezium,
131 Upsert,
132 Append,
134}
135
136#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
137pub enum StorageSinkConnection<C: ConnectionAccess = InlinedConnection> {
138 Kafka(KafkaSinkConnection<C>),
139 Iceberg(IcebergSinkConnection<C>),
140}
141
142impl<C: ConnectionAccess> StorageSinkConnection<C> {
143 pub fn alter_compatible(
148 &self,
149 id: GlobalId,
150 other: &StorageSinkConnection<C>,
151 ) -> Result<(), AlterError> {
152 if self == other {
153 return Ok(());
154 }
155 match (self, other) {
156 (StorageSinkConnection::Kafka(s), StorageSinkConnection::Kafka(o)) => {
157 s.alter_compatible(id, o)?
158 }
159 (StorageSinkConnection::Iceberg(s), StorageSinkConnection::Iceberg(o)) => {
160 s.alter_compatible(id, o)?
161 }
162 _ => {
163 tracing::warn!(
164 "StorageSinkConnection incompatible:\nself:\n{:#?}\n\nother\n{:#?}",
165 self,
166 other
167 );
168 return Err(AlterError { id });
169 }
170 }
171
172 Ok(())
173 }
174}
175
176impl<R: ConnectionResolver> IntoInlineConnection<StorageSinkConnection, R>
177 for StorageSinkConnection<ReferencedConnection>
178{
179 fn into_inline_connection(self, r: R) -> StorageSinkConnection {
180 match self {
181 Self::Kafka(conn) => StorageSinkConnection::Kafka(conn.into_inline_connection(r)),
182 Self::Iceberg(conn) => StorageSinkConnection::Iceberg(conn.into_inline_connection(r)),
183 }
184 }
185}
186
187impl<C: ConnectionAccess> StorageSinkConnection<C> {
188 pub fn connection_id(&self) -> Option<CatalogItemId> {
190 use StorageSinkConnection::*;
191 match self {
192 Kafka(KafkaSinkConnection { connection_id, .. }) => Some(*connection_id),
193 Iceberg(IcebergSinkConnection {
194 catalog_connection_id: connection_id,
195 ..
196 }) => Some(*connection_id),
197 }
198 }
199
200 pub fn name(&self) -> &'static str {
202 use StorageSinkConnection::*;
203 match self {
204 Kafka(_) => "kafka",
205 Iceberg(_) => "iceberg",
206 }
207 }
208}
209
210#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
211pub enum KafkaSinkCompressionType {
212 None,
213 Gzip,
214 Snappy,
215 Lz4,
216 Zstd,
217}
218
219impl KafkaSinkCompressionType {
220 pub fn to_librdkafka_option(&self) -> &'static str {
223 match self {
224 KafkaSinkCompressionType::None => "none",
225 KafkaSinkCompressionType::Gzip => "gzip",
226 KafkaSinkCompressionType::Snappy => "snappy",
227 KafkaSinkCompressionType::Lz4 => "lz4",
228 KafkaSinkCompressionType::Zstd => "zstd",
229 }
230 }
231}
232
233#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
234pub struct KafkaSinkConnection<C: ConnectionAccess = InlinedConnection> {
235 pub connection_id: CatalogItemId,
236 pub connection: C::Kafka,
237 pub format: KafkaSinkFormat<C>,
238 pub relation_key_indices: Option<Vec<usize>>,
240 pub key_desc_and_indices: Option<(RelationDesc, Vec<usize>)>,
242 pub headers_index: Option<usize>,
244 pub value_desc: RelationDesc,
245 pub partition_by: Option<MirScalarExpr>,
248 pub topic: String,
249 pub topic_options: KafkaTopicOptions,
251 pub compression_type: KafkaSinkCompressionType,
252 pub progress_group_id: KafkaIdStyle,
253 pub transactional_id: KafkaIdStyle,
254 pub topic_metadata_refresh_interval: Duration,
255}
256
257impl KafkaSinkConnection {
258 pub fn client_id(
263 &self,
264 configs: &ConfigSet,
265 connection_context: &ConnectionContext,
266 sink_id: GlobalId,
267 ) -> String {
268 let mut client_id =
269 KafkaConnection::id_base(connection_context, self.connection_id, sink_id);
270 self.connection.enrich_client_id(configs, &mut client_id);
271 client_id
272 }
273
274 pub fn progress_topic(&self, connection_context: &ConnectionContext) -> Cow<'_, str> {
276 self.connection
277 .progress_topic(connection_context, self.connection_id)
278 }
279
280 pub fn progress_group_id(
286 &self,
287 connection_context: &ConnectionContext,
288 sink_id: GlobalId,
289 ) -> String {
290 match self.progress_group_id {
291 KafkaIdStyle::Prefix(ref prefix) => format!(
292 "{}{}",
293 prefix.as_deref().unwrap_or(""),
294 KafkaConnection::id_base(connection_context, self.connection_id, sink_id),
295 ),
296 KafkaIdStyle::Legacy => format!("materialize-bootstrap-sink-{sink_id}"),
297 }
298 }
299
300 pub fn transactional_id(
305 &self,
306 connection_context: &ConnectionContext,
307 sink_id: GlobalId,
308 ) -> String {
309 match self.transactional_id {
310 KafkaIdStyle::Prefix(ref prefix) => format!(
311 "{}{}",
312 prefix.as_deref().unwrap_or(""),
313 KafkaConnection::id_base(connection_context, self.connection_id, sink_id)
314 ),
315 KafkaIdStyle::Legacy => format!("mz-producer-{sink_id}-0"),
316 }
317 }
318}
319
320impl<C: ConnectionAccess> KafkaSinkConnection<C> {
321 pub fn alter_compatible(
326 &self,
327 id: GlobalId,
328 other: &KafkaSinkConnection<C>,
329 ) -> Result<(), AlterError> {
330 if self == other {
331 return Ok(());
332 }
333 let KafkaSinkConnection {
334 connection_id,
335 connection,
336 format,
337 relation_key_indices,
338 key_desc_and_indices,
339 headers_index,
340 value_desc,
341 partition_by,
342 topic,
343 compression_type,
344 progress_group_id,
345 transactional_id,
346 topic_options,
347 topic_metadata_refresh_interval,
348 } = self;
349
350 let compatibility_checks = [
351 (connection_id == &other.connection_id, "connection_id"),
352 (
353 connection.alter_compatible(id, &other.connection).is_ok(),
354 "connection",
355 ),
356 (format.alter_compatible(id, &other.format).is_ok(), "format"),
357 (
358 relation_key_indices == &other.relation_key_indices,
359 "relation_key_indices",
360 ),
361 (
362 key_desc_and_indices == &other.key_desc_and_indices,
363 "key_desc_and_indices",
364 ),
365 (headers_index == &other.headers_index, "headers_index"),
366 (value_desc == &other.value_desc, "value_desc"),
367 (partition_by == &other.partition_by, "partition_by"),
368 (topic == &other.topic, "topic"),
369 (
370 compression_type == &other.compression_type,
371 "compression_type",
372 ),
373 (
374 progress_group_id == &other.progress_group_id,
375 "progress_group_id",
376 ),
377 (
378 transactional_id == &other.transactional_id,
379 "transactional_id",
380 ),
381 (topic_options == &other.topic_options, "topic_config"),
382 (
383 topic_metadata_refresh_interval == &other.topic_metadata_refresh_interval,
384 "topic_metadata_refresh_interval",
385 ),
386 ];
387 for (compatible, field) in compatibility_checks {
388 if !compatible {
389 tracing::warn!(
390 "KafkaSinkConnection incompatible at {field}:\nself:\n{:#?}\n\nother\n{:#?}",
391 self,
392 other
393 );
394
395 return Err(AlterError { id });
396 }
397 }
398
399 Ok(())
400 }
401}
402
403impl<R: ConnectionResolver> IntoInlineConnection<KafkaSinkConnection, R>
404 for KafkaSinkConnection<ReferencedConnection>
405{
406 fn into_inline_connection(self, r: R) -> KafkaSinkConnection {
407 let KafkaSinkConnection {
408 connection_id,
409 connection,
410 format,
411 relation_key_indices,
412 key_desc_and_indices,
413 headers_index,
414 value_desc,
415 partition_by,
416 topic,
417 compression_type,
418 progress_group_id,
419 transactional_id,
420 topic_options,
421 topic_metadata_refresh_interval,
422 } = self;
423 KafkaSinkConnection {
424 connection_id,
425 connection: r.resolve_connection(connection).unwrap_kafka(),
426 format: format.into_inline_connection(r),
427 relation_key_indices,
428 key_desc_and_indices,
429 headers_index,
430 value_desc,
431 partition_by,
432 topic,
433 compression_type,
434 progress_group_id,
435 transactional_id,
436 topic_options,
437 topic_metadata_refresh_interval,
438 }
439 }
440}
441
442#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
443pub enum KafkaIdStyle {
444 Prefix(Option<String>),
446 Legacy,
448}
449
450#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
451pub struct KafkaSinkFormat<C: ConnectionAccess = InlinedConnection> {
452 pub key_format: Option<KafkaSinkFormatType<C>>,
453 pub value_format: KafkaSinkFormatType<C>,
454}
455
456#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
457pub enum KafkaSinkFormatType<C: ConnectionAccess = InlinedConnection> {
458 Avro {
459 schema: String,
460 compatibility_level: Option<mz_ccsr::CompatibilityLevel>,
461 #[serde(default)]
466 schema_name: Option<String>,
467 wire_format: WireFormat<C>,
470 },
471 Json,
472 Text,
473 Bytes,
474}
475
476impl<C: ConnectionAccess> KafkaSinkFormatType<C> {
477 pub fn get_format_name(&self) -> &str {
478 match self {
479 Self::Avro { .. } => "avro",
480 Self::Json => "json",
481 Self::Text => "text",
482 Self::Bytes => "bytes",
483 }
484 }
485}
486
487impl<C: ConnectionAccess> KafkaSinkFormat<C> {
488 pub fn get_format_name<'a>(&'a self) -> Cow<'a, str> {
489 match &self.key_format {
493 None => self.value_format.get_format_name().into(),
494 Some(key_format) => match (key_format, &self.value_format) {
495 (KafkaSinkFormatType::Avro { .. }, KafkaSinkFormatType::Avro { .. }) => {
496 "avro".into()
497 }
498 (KafkaSinkFormatType::Json, KafkaSinkFormatType::Json) => "json".into(),
499 (keyf, valuef) => format!(
500 "key-{}-value-{}",
501 keyf.get_format_name(),
502 valuef.get_format_name()
503 )
504 .into(),
505 },
506 }
507 }
508
509 fn alter_compatible(&self, id: GlobalId, other: &Self) -> Result<(), AlterError> {
510 if self == other {
511 return Ok(());
512 }
513
514 match (&self.value_format, &other.value_format) {
515 (
516 KafkaSinkFormatType::Avro {
517 schema,
518 compatibility_level: _,
519 schema_name,
520 wire_format,
521 },
522 KafkaSinkFormatType::Avro {
523 schema: other_schema,
524 compatibility_level: _,
525 schema_name: other_schema_name,
526 wire_format: other_wire_format,
527 },
528 ) => {
529 if schema != other_schema
530 || schema_name != other_schema_name
531 || wire_format.alter_compatible(id, other_wire_format).is_err()
532 {
533 tracing::warn!(
534 "KafkaSinkFormat::Avro incompatible at value_format:\nself:\n{:#?}\n\nother\n{:#?}",
535 self,
536 other
537 );
538
539 return Err(AlterError { id });
540 }
541 }
542 (s, o) => {
543 if s != o {
544 tracing::warn!(
545 "KafkaSinkFormat incompatible at value_format:\nself:\n{:#?}\n\nother:{:#?}",
546 s,
547 o
548 );
549 return Err(AlterError { id });
550 }
551 }
552 }
553
554 match (&self.key_format, &other.key_format) {
555 (
556 Some(KafkaSinkFormatType::Avro {
557 schema,
558 compatibility_level: _,
559 schema_name,
560 wire_format,
561 }),
562 Some(KafkaSinkFormatType::Avro {
563 schema: other_schema,
564 compatibility_level: _,
565 schema_name: other_schema_name,
566 wire_format: other_wire_format,
567 }),
568 ) => {
569 if schema != other_schema
570 || schema_name != other_schema_name
571 || wire_format.alter_compatible(id, other_wire_format).is_err()
572 {
573 tracing::warn!(
574 "KafkaSinkFormat::Avro incompatible at key_format:\nself:\n{:#?}\n\nother\n{:#?}",
575 self,
576 other
577 );
578
579 return Err(AlterError { id });
580 }
581 }
582 (s, o) => {
583 if s != o {
584 tracing::warn!(
585 "KafkaSinkFormat incompatible at key_format\nself:\n{:#?}\n\nother:{:#?}",
586 s,
587 o
588 );
589 return Err(AlterError { id });
590 }
591 }
592 }
593
594 Ok(())
595 }
596}
597
598impl<R: ConnectionResolver> IntoInlineConnection<KafkaSinkFormat, R>
599 for KafkaSinkFormat<ReferencedConnection>
600{
601 fn into_inline_connection(self, r: R) -> KafkaSinkFormat {
602 KafkaSinkFormat {
603 key_format: self.key_format.map(|f| f.into_inline_connection(&r)),
604 value_format: self.value_format.into_inline_connection(&r),
605 }
606 }
607}
608
609impl<R: ConnectionResolver> IntoInlineConnection<KafkaSinkFormatType, R>
610 for KafkaSinkFormatType<ReferencedConnection>
611{
612 fn into_inline_connection(self, r: R) -> KafkaSinkFormatType {
613 match self {
614 KafkaSinkFormatType::Avro {
615 schema,
616 compatibility_level,
617 schema_name,
618 wire_format,
619 } => KafkaSinkFormatType::Avro {
620 schema,
621 compatibility_level,
622 schema_name,
623 wire_format: wire_format.into_inline_connection(r),
624 },
625 KafkaSinkFormatType::Json => KafkaSinkFormatType::Json,
626 KafkaSinkFormatType::Text => KafkaSinkFormatType::Text,
627 KafkaSinkFormatType::Bytes => KafkaSinkFormatType::Bytes,
628 }
629 }
630}
631
632#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
633pub enum S3SinkFormat {
634 PgCopy(CopyFormatParams<'static>),
636 Parquet,
638}
639
640#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
642pub struct S3UploadInfo {
643 pub uri: String,
645 pub max_file_size: u64,
647 pub desc: RelationDesc,
649 pub format: S3SinkFormat,
651}
652
653pub const MIN_S3_SINK_FILE_SIZE: ByteSize = ByteSize::mb(16);
654pub const MAX_S3_SINK_FILE_SIZE: ByteSize = ByteSize::gb(4);
655
656pub const ICEBERG_APPEND_DIFF_COLUMN: &str = "_mz_diff";
658pub const ICEBERG_APPEND_TIMESTAMP_COLUMN: &str = "_mz_timestamp";
660
661pub const ICEBERG_UINT64_DECIMAL_PRECISION: u8 = 20;
664
665pub fn iceberg_type_overrides(
680 scalar_type: &mz_repr::SqlScalarType,
681) -> Option<(arrow::datatypes::DataType, String)> {
682 use arrow::datatypes::DataType;
683 use mz_repr::SqlScalarType;
684 match scalar_type {
685 SqlScalarType::UInt16 => Some((DataType::Int32, "uint2".to_string())),
686 SqlScalarType::UInt32 => Some((DataType::Int64, "uint4".to_string())),
687 SqlScalarType::UInt64 => Some((
688 DataType::Decimal128(ICEBERG_UINT64_DECIMAL_PRECISION, 0),
689 "uint8".to_string(),
690 )),
691 SqlScalarType::MzTimestamp => Some((
692 DataType::Decimal128(ICEBERG_UINT64_DECIMAL_PRECISION, 0),
693 "mz_timestamp".to_string(),
694 )),
695 SqlScalarType::Interval => Some((DataType::LargeUtf8, "interval".to_string())),
696 _ => None,
697 }
698}
699
700#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
701#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
702pub struct IcebergSinkConnection<C: ConnectionAccess = InlinedConnection> {
703 pub catalog_connection_id: CatalogItemId,
704 pub catalog_connection: C::IcebergCatalog,
705
706 pub storage_connection_id: Option<CatalogItemId>,
715 pub storage_connection: Option<C::Aws>,
716
717 pub relation_key_indices: Option<Vec<usize>>,
719 pub key_desc_and_indices: Option<(RelationDesc, Vec<usize>)>,
721 pub namespace: String,
722 pub table: String,
723}
724
725impl<C: ConnectionAccess> IcebergSinkConnection<C> {
726 pub fn alter_compatible(&self, id: GlobalId, other: &Self) -> Result<(), AlterError> {
731 if self == other {
732 return Ok(());
733 }
734 let IcebergSinkConnection {
735 catalog_connection_id: connection_id,
736 catalog_connection,
737 storage_connection_id,
738 storage_connection,
739 relation_key_indices,
740 key_desc_and_indices,
741 namespace,
742 table,
743 } = self;
744
745 let compatibility_checks = [
746 (
747 connection_id == &other.catalog_connection_id,
748 "connection_id",
749 ),
750 (
751 catalog_connection
752 .alter_compatible(id, &other.catalog_connection)
753 .is_ok(),
754 "catalog_connection",
755 ),
756 (
759 other.storage_connection_id.is_none()
760 || storage_connection_id == &other.storage_connection_id,
761 "storage_connection_id",
762 ),
763 (
764 match &other.storage_connection {
765 None => true, Some(after) => {
767 match storage_connection {
768 None => false, Some(before) => before.alter_compatible(id, after).is_ok(),
770 }
771 }
772 },
773 "storage_connection",
774 ),
775 (
776 relation_key_indices == &other.relation_key_indices,
777 "relation_key_indices",
778 ),
779 (
780 key_desc_and_indices == &other.key_desc_and_indices,
781 "key_desc_and_indices",
782 ),
783 (namespace == &other.namespace, "namespace"),
784 (table == &other.table, "table"),
785 ];
786 for (compatible, field) in compatibility_checks {
787 if !compatible {
788 tracing::warn!(
789 "IcebergSinkConnection incompatible at {field}:\nself:\n{:#?}\n\nother\n{:#?}",
790 self,
791 other
792 );
793
794 return Err(AlterError { id });
795 }
796 }
797
798 Ok(())
799 }
800}
801
802impl<R: ConnectionResolver> IntoInlineConnection<IcebergSinkConnection, R>
803 for IcebergSinkConnection<ReferencedConnection>
804{
805 fn into_inline_connection(self, r: R) -> IcebergSinkConnection {
806 let IcebergSinkConnection {
807 catalog_connection_id,
808 catalog_connection,
809 storage_connection_id,
810 storage_connection,
811 relation_key_indices,
812 key_desc_and_indices,
813 namespace,
814 table,
815 } = self;
816 IcebergSinkConnection {
817 catalog_connection_id,
818 catalog_connection: r
819 .resolve_connection(catalog_connection)
820 .unwrap_iceberg_catalog(),
821 storage_connection_id,
822 storage_connection: storage_connection.map(|c| r.resolve_connection(c).unwrap_aws()),
823 relation_key_indices,
824 key_desc_and_indices,
825 namespace,
826 table,
827 }
828 }
829}