1use std::collections::{BTreeMap, HashMap};
16
17use anyhow::anyhow;
18use async_trait::async_trait;
19use phf::phf_set;
20use redis::aio::MultiplexedConnection;
21use redis::cluster::{ClusterClient, ClusterConnection, ClusterPipeline};
22use redis::{Client as RedisClient, Pipeline};
23use risingwave_common::array::StreamChunk;
24use risingwave_common::catalog::Schema;
25use risingwave_common::types::DataType;
26use serde::Deserialize;
27use serde_json::Value;
28use serde_with::serde_as;
29use with_options::WithOptions;
30
31use super::catalog::SinkFormatDesc;
32use super::encoder::template::{RedisSinkPayloadWriterInput, TemplateStringEncoder};
33use super::formatter::SinkFormatterImpl;
34use super::writer::FormattedSink;
35use super::{SinkError, SinkParam};
36use crate::dispatch_sink_formatter_str_key_impl;
37use crate::enforce_secret::EnforceSecret;
38use crate::error::ConnectorResult;
39use crate::sink::log_store::DeliveryFutureManagerAddFuture;
40use crate::sink::writer::{
41 AsyncTruncateLogSinkerOf, AsyncTruncateSinkWriter, AsyncTruncateSinkWriterExt,
42};
43use crate::sink::{Result, Sink, SinkWriterParam};
44
45pub const REDIS_SINK: &str = "redis";
46pub const KEY_FORMAT: &str = "key_format";
47pub const VALUE_FORMAT: &str = "value_format";
48pub const REDIS_VALUE_TYPE: &str = "redis_value_type";
49pub const REDIS_VALUE_TYPE_STRING: &str = "string";
50pub const REDIS_VALUE_TYPE_GEO: &str = "geospatial";
51pub const REDIS_VALUE_TYPE_PUBSUB: &str = "pubsub";
52pub const REDIS_VALUE_TYPE_STREAM: &str = "stream";
53pub const LON_NAME: &str = "longitude";
54pub const LAT_NAME: &str = "latitude";
55pub const MEMBER_NAME: &str = "member";
56pub const CHANNEL: &str = "channel";
57pub const CHANNEL_COLUMN: &str = "channel_column";
58pub const STREAM: &str = "stream";
59pub const STREAM_COLUMN: &str = "stream_column";
60
61#[derive(Deserialize, Debug, Clone, WithOptions)]
62pub struct RedisCommon {
63 #[serde(rename = "redis.url")]
64 pub url: String,
65}
66
67impl EnforceSecret for RedisCommon {
68 const ENFORCE_SECRET_PROPERTIES: phf::Set<&'static str> = phf_set! {
69 "redis.url"
70 };
71}
72
73pub enum RedisPipe {
74 Cluster(ClusterPipeline),
75 Single(Pipeline),
76}
77impl RedisPipe {
78 pub async fn query<T: redis::FromRedisValue>(
79 &self,
80 conn: &mut RedisConn,
81 ) -> ConnectorResult<T> {
82 match (self, conn) {
83 (RedisPipe::Cluster(pipe), RedisConn::Cluster(conn)) => Ok(pipe.query(conn)?),
84 (RedisPipe::Single(pipe), RedisConn::Single(conn)) => {
85 Ok(pipe.query_async(conn).await?)
86 }
87 _ => Err(SinkError::Redis("RedisPipe and RedisConn not match".to_owned()).into()),
88 }
89 }
90
91 pub fn clear(&mut self) {
92 match self {
93 RedisPipe::Cluster(pipe) => pipe.clear(),
94 RedisPipe::Single(pipe) => pipe.clear(),
95 }
96 }
97
98 pub fn is_empty(&self) -> bool {
101 match self {
102 RedisPipe::Cluster(pipe) => pipe.cmd_iter().next().is_none(),
103 RedisPipe::Single(pipe) => pipe.is_empty(),
104 }
105 }
106
107 pub fn set(
108 &mut self,
109 k: RedisSinkPayloadWriterInput,
110 v: RedisSinkPayloadWriterInput,
111 ) -> Result<()> {
112 match self {
113 RedisPipe::Cluster(pipe) => match (k, v) {
114 (
115 RedisSinkPayloadWriterInput::String(k),
116 RedisSinkPayloadWriterInput::String(v),
117 ) => {
118 pipe.set(k, v);
119 }
120 (
121 RedisSinkPayloadWriterInput::RedisGeoKey((key, member)),
122 RedisSinkPayloadWriterInput::RedisGeoValue((lat, lon)),
123 ) => {
124 pipe.geo_add(key, (lon, lat, member));
125 }
126 (
127 RedisSinkPayloadWriterInput::RedisPubSubStreamKey(key),
128 RedisSinkPayloadWriterInput::String(v),
129 ) => {
130 pipe.publish(key, v);
131 }
132 (
133 RedisSinkPayloadWriterInput::RedisPubSubStreamKey(key),
134 RedisSinkPayloadWriterInput::RedisStreamValue((field, value)),
135 ) => {
136 pipe.xadd(key, "*", &[(&field, &value)]);
137 }
138 _ => return Err(SinkError::Redis("RedisPipe set not match".to_owned())),
139 },
140 RedisPipe::Single(pipe) => match (k, v) {
141 (
142 RedisSinkPayloadWriterInput::String(k),
143 RedisSinkPayloadWriterInput::String(v),
144 ) => {
145 pipe.set(k, v);
146 }
147 (
148 RedisSinkPayloadWriterInput::RedisGeoKey((key, member)),
149 RedisSinkPayloadWriterInput::RedisGeoValue((lat, lon)),
150 ) => {
151 pipe.geo_add(key, (lon, lat, member));
152 }
153 (
154 RedisSinkPayloadWriterInput::RedisPubSubStreamKey(key),
155 RedisSinkPayloadWriterInput::String(v),
156 ) => {
157 pipe.publish(key, v);
158 }
159 (
160 RedisSinkPayloadWriterInput::RedisPubSubStreamKey(key),
161 RedisSinkPayloadWriterInput::RedisStreamValue((field, value)),
162 ) => {
163 pipe.xadd(key, "*", &[(&field, &value)]);
164 }
165 _ => return Err(SinkError::Redis("RedisPipe set not match".to_owned())),
166 },
167 };
168 Ok(())
169 }
170
171 pub fn del(&mut self, k: RedisSinkPayloadWriterInput) -> Result<()> {
172 match self {
173 RedisPipe::Cluster(pipe) => match k {
174 RedisSinkPayloadWriterInput::String(k) => {
175 pipe.del(k);
176 }
177 RedisSinkPayloadWriterInput::RedisGeoKey((key, member)) => {
178 pipe.zrem(key, member);
179 }
180 _ => return Err(SinkError::Redis("RedisPipe del not match".to_owned())),
181 },
182 RedisPipe::Single(pipe) => match k {
183 RedisSinkPayloadWriterInput::String(k) => {
184 pipe.del(k);
185 }
186 RedisSinkPayloadWriterInput::RedisGeoKey((key, member)) => {
187 pipe.zrem(key, member);
188 }
189 _ => return Err(SinkError::Redis("RedisPipe del not match".to_owned())),
190 },
191 };
192 Ok(())
193 }
194}
195pub enum RedisConn {
196 Cluster(ClusterConnection),
198 Single(MultiplexedConnection),
200}
201
202impl RedisCommon {
203 pub async fn build_conn_and_pipe(&self) -> ConnectorResult<(RedisConn, RedisPipe)> {
204 match serde_json::from_str(&self.url).map_err(|e| SinkError::Config(anyhow!(e))) {
205 Ok(v) => {
206 if let Value::Array(list) = v {
207 let list = list
208 .into_iter()
209 .map(|s| {
210 if let Value::String(s) = s {
211 Ok(s)
212 } else {
213 Err(SinkError::Redis(
214 "redis.url must be array of string".to_owned(),
215 )
216 .into())
217 }
218 })
219 .collect::<ConnectorResult<Vec<String>>>()?;
220
221 let client = ClusterClient::new(list)?;
222 Ok((
223 RedisConn::Cluster(client.get_connection()?),
224 RedisPipe::Cluster(redis::cluster::cluster_pipe()),
225 ))
226 } else {
227 Err(SinkError::Redis("redis.url must be array or string".to_owned()).into())
228 }
229 }
230 Err(_) => {
231 let client = RedisClient::open(self.url.clone())?;
232 Ok((
233 RedisConn::Single(client.get_multiplexed_async_connection().await?),
234 RedisPipe::Single(redis::pipe()),
235 ))
236 }
237 }
238 }
239}
240
241#[serde_as]
242#[derive(Clone, Debug, Deserialize, WithOptions)]
243pub struct RedisConfig {
244 #[serde(flatten)]
245 pub common: RedisCommon,
246
247 #[serde(flatten)]
248 pub unknown_fields: std::collections::HashMap<String, String>,
249}
250
251crate::impl_sink_unknown_fields!(RedisConfig);
252
253impl EnforceSecret for RedisConfig {
254 fn enforce_secret<'a>(prop_iter: impl Iterator<Item = &'a str>) -> ConnectorResult<()> {
255 for prop in prop_iter {
256 RedisCommon::enforce_one(prop)?;
257 }
258 Ok(())
259 }
260}
261
262impl RedisConfig {
263 pub fn from_btreemap(properties: BTreeMap<String, String>) -> Result<Self> {
264 let config =
265 serde_json::from_value::<RedisConfig>(serde_json::to_value(properties).unwrap())
266 .map_err(|e| SinkError::Config(anyhow!(e)))?;
267 Ok(config)
268 }
269}
270
271#[derive(Debug)]
272pub struct RedisSink {
273 config: RedisConfig,
274 schema: Schema,
275 pk_indices: Vec<usize>,
276 format_desc: SinkFormatDesc,
277 db_name: String,
278 sink_from_name: String,
279}
280
281impl EnforceSecret for RedisSink {
282 fn enforce_secret<'a>(prop_iter: impl Iterator<Item = &'a str>) -> ConnectorResult<()> {
283 for prop in prop_iter {
284 RedisConfig::enforce_one(prop)?;
285 }
286 Ok(())
287 }
288}
289
290#[async_trait]
291impl TryFrom<SinkParam> for RedisSink {
292 type Error = SinkError;
293
294 fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
295 let Some(pk_indices) = param.downstream_pk.clone() else {
296 return Err(SinkError::Config(anyhow!(
297 "Redis Sink Primary Key must be specified."
298 )));
299 };
300 let config = RedisConfig::from_btreemap(param.properties.clone())?;
301 Ok(Self {
302 config,
303 schema: param.schema(),
304 pk_indices,
305 format_desc: param
306 .format_desc
307 .ok_or_else(|| SinkError::Config(anyhow!("missing FORMAT ... ENCODE ...")))?,
308 db_name: param.db_name,
309 sink_from_name: param.sink_from_name,
310 })
311 }
312}
313
314impl Sink for RedisSink {
315 type LogSinker = AsyncTruncateLogSinkerOf<RedisSinkWriter>;
316
317 const SINK_NAME: &'static str = "redis";
318
319 crate::impl_validate_sink_unknown_fields!();
320
321 async fn new_log_sinker(&self, _writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
322 Ok(RedisSinkWriter::new(
323 self.config.clone(),
324 self.schema.clone(),
325 self.pk_indices.clone(),
326 &self.format_desc,
327 self.db_name.clone(),
328 self.sink_from_name.clone(),
329 )
330 .await?
331 .into_log_sinker(usize::MAX))
332 }
333
334 async fn validate(&self) -> Result<()> {
335 let all_map: HashMap<String, DataType> = self
336 .schema
337 .fields()
338 .iter()
339 .map(|f| (f.name.clone(), f.data_type.clone()))
340 .collect();
341 let pk_map: HashMap<String, DataType> = self
342 .schema
343 .fields()
344 .iter()
345 .enumerate()
346 .filter(|(k, _)| self.pk_indices.contains(k))
347 .map(|(_, v)| (v.name.clone(), v.data_type.clone()))
348 .collect();
349 if matches!(
350 self.format_desc.encode,
351 super::catalog::SinkEncode::Template
352 ) {
353 match self
354 .format_desc
355 .options
356 .get(REDIS_VALUE_TYPE)
357 .map(|s| s.as_str())
358 {
359 Some(REDIS_VALUE_TYPE_STRING) | None => {
361 let key_format = self.format_desc.options.get(KEY_FORMAT).ok_or_else(|| {
362 SinkError::Config(anyhow!(
363 "Cannot find '{KEY_FORMAT}', please set it or use JSON"
364 ))
365 })?;
366 TemplateStringEncoder::check_string_format(key_format, &pk_map)?;
367 let value_format =
368 self.format_desc.options.get(VALUE_FORMAT).ok_or_else(|| {
369 SinkError::Config(anyhow!(
370 "Cannot find `{VALUE_FORMAT}`, please set it or use JSON"
371 ))
372 })?;
373 TemplateStringEncoder::check_string_format(value_format, &all_map)?;
374 }
375 Some(REDIS_VALUE_TYPE_GEO) => {
376 let key_format = self.format_desc.options.get(KEY_FORMAT).ok_or_else(|| {
377 SinkError::Config(anyhow!(
378 "Cannot find '{KEY_FORMAT}', please set it or use JSON"
379 ))
380 })?;
381 TemplateStringEncoder::check_string_format(key_format, &pk_map)?;
382
383 let lon_name = self.format_desc.options.get(LON_NAME).ok_or_else(|| {
384 SinkError::Config(anyhow!(
385 "Cannot find `{LON_NAME}`, please set it or use JSON or set `{REDIS_VALUE_TYPE}` to `{REDIS_VALUE_TYPE_STRING}`"
386 ))
387 })?;
388 let lat_name = self.format_desc.options.get(LAT_NAME).ok_or_else(|| {
389 SinkError::Config(anyhow!(
390 "Cannot find `{LAT_NAME}`, please set it or use JSON or set `{REDIS_VALUE_TYPE}` to `{REDIS_VALUE_TYPE_STRING}`"
391 ))
392 })?;
393 let member_name = self.format_desc.options.get(MEMBER_NAME).ok_or_else(|| {
394 SinkError::Config(anyhow!(
395 "Cannot find `{MEMBER_NAME}`, please set it or use JSON or set `{REDIS_VALUE_TYPE}` to `{REDIS_VALUE_TYPE_STRING}`"
396 ))
397 })?;
398 if let Some(lon_type) = all_map.get(lon_name)
399 && (lon_type == &DataType::Float64
400 || lon_type == &DataType::Float32
401 || lon_type == &DataType::Varchar)
402 {
403 } else {
405 return Err(SinkError::Config(anyhow!(
406 "`{LON_NAME}` must be set to `float64` or `float32` or `varchar`"
407 )));
408 }
409 if let Some(lat_type) = all_map.get(lat_name)
410 && (lat_type == &DataType::Float64
411 || lat_type == &DataType::Float32
412 || lat_type == &DataType::Varchar)
413 {
414 } else {
416 return Err(SinkError::Config(anyhow!(
417 "`{LAT_NAME}` must be set to `float64` or `float32` or `varchar`"
418 )));
419 }
420 if let Some(member_type) = pk_map.get(member_name)
421 && member_type == &DataType::Varchar
422 {
423 } else {
425 return Err(SinkError::Config(anyhow!(
426 "`{MEMBER_NAME}` must be set to `varchar` and `primary_key`"
427 )));
428 }
429 }
430 Some(REDIS_VALUE_TYPE_PUBSUB) => {
431 let channel = self.format_desc.options.get(CHANNEL);
432 let channel_column = self.format_desc.options.get(CHANNEL_COLUMN);
433 if (channel.is_none() && channel_column.is_none())
434 || (channel.is_some() && channel_column.is_some())
435 {
436 return Err(SinkError::Config(anyhow!(
437 "`{CHANNEL}` and `{CHANNEL_COLUMN}` only one can be set"
438 )));
439 }
440
441 if let Some(channel_column) = channel_column
442 && let Some(channel_column_type) = all_map.get(channel_column)
443 && (channel_column_type != &DataType::Varchar)
444 {
445 return Err(SinkError::Config(anyhow!(
446 "`{CHANNEL_COLUMN}` must be set to `varchar`"
447 )));
448 }
449
450 let value_format =
451 self.format_desc.options.get(VALUE_FORMAT).ok_or_else(|| {
452 SinkError::Config(anyhow!("Cannot find `{VALUE_FORMAT}`"))
453 })?;
454 TemplateStringEncoder::check_string_format(value_format, &all_map)?;
455 }
456 Some(REDIS_VALUE_TYPE_STREAM) => {
457 tracing::error!("test:for bug");
458 risingwave_common::license::Feature::RedisSinkStream
459 .check_available()
460 .map_err(|e| anyhow::anyhow!(e))?;
461 let stream = self.format_desc.options.get(STREAM);
462 let stream_column = self.format_desc.options.get(STREAM_COLUMN);
463 if (stream.is_none() && stream_column.is_none())
464 || (stream.is_some() && stream_column.is_some())
465 {
466 return Err(SinkError::Config(anyhow!(
467 "Please specific either `{STREAM}` or `{STREAM_COLUMN}`. They are mutually exclusive options."
468 )));
469 }
470
471 if let Some(stream_column) = stream_column
472 && let Some(stream_column_type) = all_map.get(stream_column)
473 && (stream_column_type != &DataType::Varchar)
474 {
475 return Err(SinkError::Config(anyhow!(
476 "`{STREAM_COLUMN}` must be set to `varchar`"
477 )));
478 }
479
480 let value_format =
481 self.format_desc.options.get(VALUE_FORMAT).ok_or_else(|| {
482 SinkError::Config(anyhow!("Cannot find `{VALUE_FORMAT}`"))
483 })?;
484 let key_format = self.format_desc.options.get(KEY_FORMAT).ok_or_else(|| {
485 SinkError::Config(anyhow!(
486 "Cannot find '{KEY_FORMAT}', please set it or use JSON"
487 ))
488 })?;
489 TemplateStringEncoder::check_string_format(key_format, &pk_map)?;
490 TemplateStringEncoder::check_string_format(value_format, &all_map)?;
491 }
492 _ => {
493 return Err(SinkError::Config(anyhow!(
494 "`{REDIS_VALUE_TYPE}` must be set to `{REDIS_VALUE_TYPE_STRING}` or `{REDIS_VALUE_TYPE_GEO}` or `{REDIS_VALUE_TYPE_PUBSUB}` or `{REDIS_VALUE_TYPE_STREAM}`"
495 )));
496 }
497 }
498 }
499 self.config.common.build_conn_and_pipe().await?;
500 Ok(())
501 }
502}
503
504pub struct RedisSinkWriter {
505 #[expect(dead_code)]
506 epoch: u64,
507 #[expect(dead_code)]
508 schema: Schema,
509 #[expect(dead_code)]
510 pk_indices: Vec<usize>,
511 formatter: SinkFormatterImpl,
512 payload_writer: RedisSinkPayloadWriter,
513}
514
515struct RedisSinkPayloadWriter {
516 conn: Option<RedisConn>,
518 pipe: RedisPipe,
520}
521
522impl RedisSinkPayloadWriter {
523 pub async fn new(config: RedisConfig) -> Result<Self> {
524 let (conn, pipe) = config.common.build_conn_and_pipe().await?;
525 let conn = Some(conn);
526
527 Ok(Self { conn, pipe })
528 }
529
530 #[cfg(test)]
531 pub fn mock() -> Self {
532 let conn = None;
533 let pipe = RedisPipe::Single(redis::pipe());
534 Self { conn, pipe }
535 }
536
537 pub async fn commit(&mut self) -> Result<()> {
538 if self.pipe.is_empty() {
542 return Ok(());
543 }
544
545 #[cfg(test)]
546 {
547 if self.conn.is_none() {
548 return Ok(());
549 }
550 }
551 self.pipe.query::<()>(self.conn.as_mut().unwrap()).await?;
552 self.pipe.clear();
553 Ok(())
554 }
555}
556
557impl FormattedSink for RedisSinkPayloadWriter {
558 type K = RedisSinkPayloadWriterInput;
559 type V = RedisSinkPayloadWriterInput;
560
561 async fn write_one(&mut self, k: Option<Self::K>, v: Option<Self::V>) -> Result<()> {
562 let k = k.ok_or_else(|| SinkError::Redis("The redis key cannot be null".to_owned()))?;
563 match v {
564 Some(v) => self.pipe.set(k, v)?,
565 None => self.pipe.del(k)?,
566 };
567 Ok(())
568 }
569}
570
571impl RedisSinkWriter {
572 pub async fn new(
573 config: RedisConfig,
574 schema: Schema,
575 pk_indices: Vec<usize>,
576 format_desc: &SinkFormatDesc,
577 db_name: String,
578 sink_from_name: String,
579 ) -> Result<Self> {
580 let payload_writer = RedisSinkPayloadWriter::new(config.clone()).await?;
581 let formatter = SinkFormatterImpl::new(
582 format_desc,
583 schema.clone(),
584 pk_indices.clone(),
585 db_name,
586 sink_from_name,
587 "NO_TOPIC",
588 )
589 .await?;
590
591 Ok(Self {
592 schema,
593 pk_indices,
594 epoch: 0,
595 formatter,
596 payload_writer,
597 })
598 }
599
600 #[cfg(test)]
601 pub async fn mock(
602 schema: Schema,
603 pk_indices: Vec<usize>,
604 format_desc: &SinkFormatDesc,
605 ) -> Result<Self> {
606 let formatter = SinkFormatterImpl::new(
607 format_desc,
608 schema.clone(),
609 pk_indices.clone(),
610 "d1".to_owned(),
611 "t1".to_owned(),
612 "NO_TOPIC",
613 )
614 .await?;
615 Ok(Self {
616 schema,
617 pk_indices,
618 epoch: 0,
619 formatter,
620 payload_writer: RedisSinkPayloadWriter::mock(),
621 })
622 }
623}
624
625impl AsyncTruncateSinkWriter for RedisSinkWriter {
626 async fn write_chunk<'a>(
627 &'a mut self,
628 chunk: StreamChunk,
629 _add_future: DeliveryFutureManagerAddFuture<'a, Self::DeliveryFuture>,
630 ) -> Result<()> {
631 dispatch_sink_formatter_str_key_impl!(&self.formatter, formatter, {
632 self.payload_writer.write_chunk(chunk, formatter).await?;
633 self.payload_writer.commit().await
634 })
635 }
636}
637
638#[cfg(test)]
639mod test {
640 use core::panic;
641
642 use rdkafka::message::FromBytes;
643 use risingwave_common::array::{Array, I32Array, Op, Utf8Array};
644 use risingwave_common::catalog::Field;
645 use risingwave_common::types::DataType;
646 use risingwave_common::util::iter_util::ZipEqDebug;
647
648 use super::*;
649 use crate::sink::catalog::{SinkEncode, SinkFormat};
650 use crate::sink::log_store::DeliveryFutureManager;
651
652 #[test]
653 fn test_pipe_is_empty() {
654 let mut pipe = RedisPipe::Single(redis::pipe());
655 assert!(pipe.is_empty());
656 pipe.set(
657 RedisSinkPayloadWriterInput::String("k".to_owned()),
658 RedisSinkPayloadWriterInput::String("v".to_owned()),
659 )
660 .unwrap();
661 assert!(!pipe.is_empty());
662 pipe.clear();
663 assert!(pipe.is_empty());
664
665 let mut cluster_pipe = RedisPipe::Cluster(redis::cluster::cluster_pipe());
666 assert!(cluster_pipe.is_empty());
667 cluster_pipe
668 .del(RedisSinkPayloadWriterInput::String("k".to_owned()))
669 .unwrap();
670 assert!(!cluster_pipe.is_empty());
671 }
672
673 #[tokio::test]
674 async fn test_write() {
675 let schema = Schema::new(vec![
676 Field {
677 data_type: DataType::Int32,
678 name: "id".to_owned(),
679 },
680 Field {
681 data_type: DataType::Varchar,
682 name: "name".to_owned(),
683 },
684 ]);
685
686 let format_desc = SinkFormatDesc {
687 format: SinkFormat::AppendOnly,
688 encode: SinkEncode::Json,
689 options: BTreeMap::default(),
690 secret_refs: BTreeMap::default(),
691 key_encode: None,
692 connection_id: None,
693 };
694
695 let mut redis_sink_writer = RedisSinkWriter::mock(schema, vec![0], &format_desc)
696 .await
697 .unwrap();
698
699 let chunk_a = StreamChunk::new(
700 vec![Op::Insert, Op::Insert, Op::Insert],
701 vec![
702 I32Array::from_iter(vec![1, 2, 3]).into_ref(),
703 Utf8Array::from_iter(vec!["Alice", "Bob", "Clare"]).into_ref(),
704 ],
705 );
706
707 let mut manager = DeliveryFutureManager::new(0);
708
709 redis_sink_writer
710 .write_chunk(chunk_a, manager.start_write_chunk(0, 0))
711 .await
712 .expect("failed to write batch");
713 let expected_a = vec![
714 (
715 0,
716 "*3\r\n$3\r\nSET\r\n$8\r\n{\"id\":1}\r\n$23\r\n{\"id\":1,\"name\":\"Alice\"}\r\n",
717 ),
718 (
719 1,
720 "*3\r\n$3\r\nSET\r\n$8\r\n{\"id\":2}\r\n$21\r\n{\"id\":2,\"name\":\"Bob\"}\r\n",
721 ),
722 (
723 2,
724 "*3\r\n$3\r\nSET\r\n$8\r\n{\"id\":3}\r\n$23\r\n{\"id\":3,\"name\":\"Clare\"}\r\n",
725 ),
726 ];
727
728 if let RedisPipe::Single(pipe) = &redis_sink_writer.payload_writer.pipe {
729 pipe.cmd_iter()
730 .enumerate()
731 .zip_eq_debug(expected_a.clone())
732 .for_each(|((i, cmd), (exp_i, exp_cmd))| {
733 if exp_i == i {
734 assert_eq!(exp_cmd, str::from_bytes(&cmd.get_packed_command()).unwrap())
735 }
736 });
737 } else {
738 panic!("pipe type not match")
739 }
740 }
741
742 #[tokio::test]
743 async fn test_format_write() {
744 let schema = Schema::new(vec![
745 Field {
746 data_type: DataType::Int32,
747 name: "id".to_owned(),
748 },
749 Field {
750 data_type: DataType::Varchar,
751 name: "name".to_owned(),
752 },
753 ]);
754
755 let mut btree_map = BTreeMap::default();
756 btree_map.insert(KEY_FORMAT.to_owned(), "key-{id}".to_owned());
757 btree_map.insert(
758 VALUE_FORMAT.to_owned(),
759 "values:\\{id:{id},name:{name}\\}".to_owned(),
760 );
761 let format_desc = SinkFormatDesc {
762 format: SinkFormat::AppendOnly,
763 encode: SinkEncode::Template,
764 options: btree_map,
765 secret_refs: Default::default(),
766 key_encode: None,
767 connection_id: None,
768 };
769
770 let mut redis_sink_writer = RedisSinkWriter::mock(schema, vec![0], &format_desc)
771 .await
772 .unwrap();
773
774 let mut future_manager = DeliveryFutureManager::new(0);
775
776 let chunk_a = StreamChunk::new(
777 vec![Op::Insert, Op::Insert, Op::Insert],
778 vec![
779 I32Array::from_iter(vec![1, 2, 3]).into_ref(),
780 Utf8Array::from_iter(vec!["Alice", "Bob", "Clare"]).into_ref(),
781 ],
782 );
783
784 redis_sink_writer
785 .write_chunk(chunk_a, future_manager.start_write_chunk(0, 0))
786 .await
787 .expect("failed to write batch");
788 let expected_a = vec![
789 (
790 0,
791 "*3\r\n$3\r\nSET\r\n$5\r\nkey-1\r\n$24\r\nvalues:{id:1,name:Alice}\r\n",
792 ),
793 (
794 1,
795 "*3\r\n$3\r\nSET\r\n$5\r\nkey-2\r\n$22\r\nvalues:{id:2,name:Bob}\r\n",
796 ),
797 (
798 2,
799 "*3\r\n$3\r\nSET\r\n$5\r\nkey-3\r\n$24\r\nvalues:{id:3,name:Clare}\r\n",
800 ),
801 ];
802
803 if let RedisPipe::Single(pipe) = &redis_sink_writer.payload_writer.pipe {
804 pipe.cmd_iter()
805 .enumerate()
806 .zip_eq_debug(expected_a.clone())
807 .for_each(|((i, cmd), (exp_i, exp_cmd))| {
808 if exp_i == i {
809 assert_eq!(exp_cmd, str::from_bytes(&cmd.get_packed_command()).unwrap())
810 }
811 });
812 } else {
813 panic!("pipe type not match")
814 };
815 }
816}