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