Skip to main content

risingwave_connector/sink/
sqlserver.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{BTreeMap, HashMap};
16
17use anyhow::anyhow;
18use async_trait::async_trait;
19use phf::{Set, phf_set};
20use risingwave_common::array::{Op, RowRef, StreamChunk};
21use risingwave_common::catalog::Schema;
22use risingwave_common::row::{OwnedRow, Row};
23use risingwave_common::types::{DataType, Decimal};
24use serde::Deserialize;
25use serde_with::{DisplayFromStr, serde_as};
26use simd_json::prelude::ArrayTrait;
27use tiberius::numeric::Numeric;
28use tiberius::{AuthMethod, Client, ColumnData, Config, Query};
29use tokio::net::TcpStream;
30use with_options::WithOptions;
31
32use super::{
33    SINK_TYPE_APPEND_ONLY, SINK_TYPE_OPTION, SINK_TYPE_UPSERT, SinkError, SinkWriterMetrics,
34};
35use crate::connector_common::sql_server::create_mssql_client_with_config;
36use crate::enforce_secret::EnforceSecret;
37use crate::sink::writer::{LogSinkerOf, SinkWriter, SinkWriterExt};
38use crate::sink::{Result, Sink, SinkParam, SinkWriterParam};
39
40pub const SQLSERVER_SINK: &str = "sqlserver";
41
42fn default_max_batch_rows() -> usize {
43    1024
44}
45
46#[serde_as]
47#[derive(Clone, Debug, Deserialize, WithOptions)]
48pub struct SqlServerConfig {
49    #[serde(rename = "sqlserver.host")]
50    pub host: String,
51    #[serde(rename = "sqlserver.port")]
52    #[serde_as(as = "DisplayFromStr")]
53    pub port: u16,
54    #[serde(rename = "sqlserver.user")]
55    pub user: String,
56    #[serde(rename = "sqlserver.password")]
57    pub password: String,
58    #[serde(rename = "sqlserver.database")]
59    pub database: String,
60    #[serde(rename = "sqlserver.schema", default = "sql_server_default_schema")]
61    pub schema: String,
62    #[serde(rename = "sqlserver.table")]
63    pub table: String,
64    #[serde(
65        rename = "sqlserver.max_batch_rows",
66        default = "default_max_batch_rows"
67    )]
68    #[serde_as(as = "DisplayFromStr")]
69    pub max_batch_rows: usize,
70    pub r#type: String, // accept "append-only" or "upsert"
71
72    #[serde(flatten)]
73    pub unknown_fields: std::collections::HashMap<String, String>,
74}
75
76crate::impl_sink_unknown_fields!(SqlServerConfig);
77
78pub fn sql_server_default_schema() -> String {
79    "dbo".to_owned()
80}
81
82impl SqlServerConfig {
83    pub fn from_btreemap(properties: BTreeMap<String, String>) -> Result<Self> {
84        let config =
85            serde_json::from_value::<SqlServerConfig>(serde_json::to_value(properties).unwrap())
86                .map_err(|e| SinkError::Config(anyhow!(e)))?;
87        if config.r#type != SINK_TYPE_APPEND_ONLY && config.r#type != SINK_TYPE_UPSERT {
88            return Err(SinkError::Config(anyhow!(
89                "`{}` must be {}, or {}",
90                SINK_TYPE_OPTION,
91                SINK_TYPE_APPEND_ONLY,
92                SINK_TYPE_UPSERT
93            )));
94        }
95        Ok(config)
96    }
97
98    pub fn full_object_path(&self) -> String {
99        format!("[{}].[{}].[{}]", self.database, self.schema, self.table)
100    }
101}
102
103impl EnforceSecret for SqlServerConfig {
104    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
105        "sqlserver.password"
106    };
107}
108#[derive(Debug)]
109pub struct SqlServerSink {
110    pub config: SqlServerConfig,
111    schema: Schema,
112    pk_indices: Vec<usize>,
113    is_append_only: bool,
114}
115
116struct SqlServerColumnMetadata {
117    name: String,
118    is_pk: bool,
119    data_type: String,
120}
121
122impl EnforceSecret for SqlServerSink {
123    fn enforce_secret<'a>(
124        prop_iter: impl Iterator<Item = &'a str>,
125    ) -> crate::sink::ConnectorResult<()> {
126        for prop in prop_iter {
127            SqlServerConfig::enforce_one(prop)?;
128        }
129        Ok(())
130    }
131}
132impl SqlServerSink {
133    pub fn new(
134        mut config: SqlServerConfig,
135        schema: Schema,
136        pk_indices: Vec<usize>,
137        is_append_only: bool,
138    ) -> Result<Self> {
139        // Rewrite config because tiberius allows a maximum of 2100 params in one query request.
140        const TIBERIUS_PARAM_MAX: usize = 2000;
141        let params_per_op = schema.fields().len();
142        let tiberius_max_batch_rows = if params_per_op == 0 {
143            config.max_batch_rows
144        } else {
145            ((TIBERIUS_PARAM_MAX as f64 / params_per_op as f64).floor()) as usize
146        };
147        if tiberius_max_batch_rows == 0 {
148            return Err(SinkError::SqlServer(anyhow!(format!(
149                "too many column {}",
150                params_per_op
151            ))));
152        }
153        config.max_batch_rows = std::cmp::min(config.max_batch_rows, tiberius_max_batch_rows);
154        Ok(Self {
155            config,
156            schema,
157            pk_indices,
158            is_append_only,
159        })
160    }
161}
162
163impl TryFrom<SinkParam> for SqlServerSink {
164    type Error = SinkError;
165
166    fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
167        let schema = param.schema();
168        let pk_indices = param.downstream_pk_or_empty();
169        let config = SqlServerConfig::from_btreemap(param.properties)?;
170        SqlServerSink::new(config, schema, pk_indices, param.sink_type.is_append_only())
171    }
172}
173
174impl Sink for SqlServerSink {
175    type LogSinker = LogSinkerOf<SqlServerSinkWriter>;
176
177    const SINK_NAME: &'static str = SQLSERVER_SINK;
178
179    crate::impl_validate_sink_unknown_fields!();
180
181    async fn validate(&self) -> Result<()> {
182        risingwave_common::license::Feature::SqlServerSink
183            .check_available()
184            .map_err(|e| anyhow::anyhow!(e))?;
185
186        if !self.is_append_only && self.pk_indices.is_empty() {
187            return Err(SinkError::Config(anyhow!(
188                "Primary key not defined for upsert SQL Server sink (please define in `primary_key` field)"
189            )));
190        }
191
192        for f in self.schema.fields() {
193            check_data_type_compatibility(&f.data_type)?;
194        }
195
196        let mut sql_client = SqlServerClient::new(&self.config).await?;
197        validate_sql_server_write_permission(&mut sql_client, &self.config, self.is_append_only)
198            .await?;
199        let sql_server_table_metadata =
200            query_sql_server_table_metadata(&mut sql_client, &self.config).await?;
201        let sql_server_pk_count = sql_server_table_metadata
202            .iter()
203            .filter(|metadata| metadata.is_pk)
204            .count();
205        let sql_server_table_metadata = sql_server_table_metadata
206            .into_iter()
207            .map(|metadata| (metadata.name.clone(), metadata))
208            .collect::<HashMap<_, _>>();
209
210        // Validate Column name, Primary Key and data type.
211        for (idx, col) in self.schema.fields().iter().enumerate() {
212            let rw_is_pk = self.pk_indices.contains(&idx);
213            match sql_server_table_metadata.get(&normalize_sql_server_column_name(&col.name)) {
214                None => {
215                    return Err(SinkError::SqlServer(anyhow!(format!(
216                        "column {} not found in the downstream SQL Server table {}",
217                        col.name,
218                        self.config.full_object_path()
219                    ))));
220                }
221                Some(sql_server_col) => {
222                    validate_data_type_compatibility(
223                        &col.name,
224                        &col.data_type,
225                        &sql_server_col.data_type,
226                    )?;
227                    if self.is_append_only {
228                        continue;
229                    }
230                    if rw_is_pk && !sql_server_col.is_pk {
231                        return Err(SinkError::SqlServer(anyhow!(format!(
232                            "column {} specified in primary_key mismatches with the downstream SQL Server table {} PK",
233                            col.name,
234                            self.config.full_object_path(),
235                        ))));
236                    }
237                    if !rw_is_pk && sql_server_col.is_pk {
238                        return Err(SinkError::SqlServer(anyhow!(format!(
239                            "column {} unspecified in primary_key mismatches with the downstream SQL Server table {} PK",
240                            col.name,
241                            self.config.full_object_path(),
242                        ))));
243                    }
244                }
245            }
246        }
247
248        if !self.is_append_only && sql_server_pk_count != self.pk_indices.len() {
249            let sql_server_pk_columns = sql_server_table_metadata
250                .values()
251                .filter(|metadata| metadata.is_pk)
252                .map(|metadata| metadata.name.as_str())
253                .collect::<Vec<_>>()
254                .join(",");
255            let rw_pk_columns = self
256                .pk_indices
257                .iter()
258                .map(|idx| self.schema[*idx].name.as_str())
259                .collect::<Vec<_>>()
260                .join(",");
261            return Err(SinkError::SqlServer(anyhow!(format!(
262                "primary key does not match between RisingWave sink ({}: [{}]) and SQL Server table {} ({}: [{}])",
263                self.pk_indices.len(),
264                rw_pk_columns,
265                self.config.full_object_path(),
266                sql_server_pk_count,
267                sql_server_pk_columns,
268            ))));
269        }
270
271        Ok(())
272    }
273
274    async fn new_log_sinker(&self, writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
275        Ok(SqlServerSinkWriter::new(
276            self.config.clone(),
277            self.schema.clone(),
278            self.pk_indices.clone(),
279            self.is_append_only,
280        )
281        .await?
282        .into_log_sinker(SinkWriterMetrics::new(&writer_param)))
283    }
284}
285
286enum SqlOp {
287    Insert(OwnedRow),
288    Merge(OwnedRow),
289    Delete(OwnedRow),
290}
291
292pub struct SqlServerSinkWriter {
293    config: SqlServerConfig,
294    schema: Schema,
295    pk_indices: Vec<usize>,
296    is_append_only: bool,
297    downstream_column_data_types: Vec<String>,
298    sql_client: SqlServerClient,
299    ops: Vec<SqlOp>,
300}
301
302impl SqlServerSinkWriter {
303    async fn new(
304        config: SqlServerConfig,
305        schema: Schema,
306        pk_indices: Vec<usize>,
307        is_append_only: bool,
308    ) -> Result<Self> {
309        let mut sql_client = SqlServerClient::new(&config).await?;
310        let downstream_column_data_types =
311            query_downstream_column_metadata(&mut sql_client, &config, &schema)
312                .await?
313                .into_iter()
314                .map(|metadata| metadata.data_type)
315                .collect();
316        let writer = Self {
317            config,
318            schema,
319            pk_indices,
320            is_append_only,
321            downstream_column_data_types,
322            sql_client,
323            ops: vec![],
324        };
325        Ok(writer)
326    }
327
328    async fn delete_one(&mut self, row: RowRef<'_>) -> Result<()> {
329        if self.ops.len() + 1 >= self.config.max_batch_rows {
330            self.flush().await?;
331        }
332        self.ops.push(SqlOp::Delete(row.into_owned_row()));
333        Ok(())
334    }
335
336    async fn upsert_one(&mut self, row: RowRef<'_>) -> Result<()> {
337        if self.ops.len() + 1 >= self.config.max_batch_rows {
338            self.flush().await?;
339        }
340        self.ops.push(SqlOp::Merge(row.into_owned_row()));
341        Ok(())
342    }
343
344    async fn insert_one(&mut self, row: RowRef<'_>) -> Result<()> {
345        if self.ops.len() + 1 >= self.config.max_batch_rows {
346            self.flush().await?;
347        }
348        self.ops.push(SqlOp::Insert(row.into_owned_row()));
349        Ok(())
350    }
351
352    async fn flush(&mut self) -> Result<()> {
353        use std::fmt::Write;
354        if self.ops.is_empty() {
355            return Ok(());
356        }
357        let mut query_str = String::new();
358        let col_num = self.schema.fields.len();
359        let mut next_param_id = 1;
360        let non_pk_col_indices = (0..col_num)
361            .filter(|idx| !self.pk_indices.contains(idx))
362            .collect::<Vec<usize>>();
363        let all_col_names = self
364            .schema
365            .fields
366            .iter()
367            .map(|f| format!("[{}]", f.name))
368            .collect::<Vec<_>>()
369            .join(",");
370        let all_source_col_names = self
371            .schema
372            .fields
373            .iter()
374            .map(|f| format!("[SOURCE].[{}]", f.name))
375            .collect::<Vec<_>>()
376            .join(",");
377        let pk_match = self
378            .pk_indices
379            .iter()
380            .map(|idx| {
381                format!(
382                    "[SOURCE].[{}]=[TARGET].[{}]",
383                    self.schema[*idx].name, self.schema[*idx].name
384                )
385            })
386            .collect::<Vec<_>>()
387            .join(" AND ");
388        let param_placeholders = |param_id: &mut usize| {
389            (0..col_num)
390                .map(|_| param_placeholder(param_id))
391                .collect::<Vec<_>>()
392                .join(",")
393        };
394        let set_all_source_col = non_pk_col_indices
395            .iter()
396            .map(|idx| {
397                format!(
398                    "[{}]=[SOURCE].[{}]",
399                    self.schema[*idx].name, self.schema[*idx].name
400                )
401            })
402            .collect::<Vec<_>>()
403            .join(",");
404        // TODO: avoid repeating the SQL
405        for op in &self.ops {
406            match op {
407                SqlOp::Insert(_) => {
408                    write!(
409                        &mut query_str,
410                        "INSERT INTO {} ({}) VALUES ({});",
411                        self.config.full_object_path(),
412                        all_col_names,
413                        param_placeholders(&mut next_param_id),
414                    )
415                    .unwrap();
416                }
417                SqlOp::Merge(_) => {
418                    write!(
419                        &mut query_str,
420                        r#"MERGE {} WITH (HOLDLOCK) AS [TARGET]
421                        USING (VALUES ({})) AS [SOURCE] ({})
422                        ON {}
423                        WHEN MATCHED THEN UPDATE SET {}
424                        WHEN NOT MATCHED THEN INSERT ({}) VALUES ({});"#,
425                        self.config.full_object_path(),
426                        param_placeholders(&mut next_param_id),
427                        all_col_names,
428                        pk_match,
429                        set_all_source_col,
430                        all_col_names,
431                        all_source_col_names,
432                    )
433                    .unwrap();
434                }
435                SqlOp::Delete(_) => {
436                    write!(
437                        &mut query_str,
438                        r#"DELETE FROM {} WHERE {};"#,
439                        self.config.full_object_path(),
440                        self.pk_indices
441                            .iter()
442                            .map(|idx| {
443                                let condition = format!(
444                                    "[{}]={}",
445                                    self.schema[*idx].name,
446                                    param_placeholder(&mut next_param_id)
447                                );
448                                condition
449                            })
450                            .collect::<Vec<_>>()
451                            .join(" AND "),
452                    )
453                    .unwrap();
454                }
455            }
456        }
457
458        let mut query = Query::new(query_str);
459        for op in self.ops.drain(..) {
460            match op {
461                SqlOp::Insert(row) => {
462                    bind_params(
463                        &mut query,
464                        row,
465                        &self.schema,
466                        &self.downstream_column_data_types,
467                        0..col_num,
468                    )?;
469                }
470                SqlOp::Merge(row) => {
471                    bind_params(
472                        &mut query,
473                        row,
474                        &self.schema,
475                        &self.downstream_column_data_types,
476                        0..col_num,
477                    )?;
478                }
479                SqlOp::Delete(row) => {
480                    bind_params(
481                        &mut query,
482                        row,
483                        &self.schema,
484                        &self.downstream_column_data_types,
485                        self.pk_indices.iter().copied(),
486                    )?;
487                }
488            }
489        }
490        query.execute(&mut self.sql_client.inner_client).await?;
491        Ok(())
492    }
493}
494
495#[async_trait]
496impl SinkWriter for SqlServerSinkWriter {
497    async fn begin_epoch(&mut self, _epoch: u64) -> Result<()> {
498        Ok(())
499    }
500
501    async fn write_batch(&mut self, chunk: StreamChunk) -> Result<()> {
502        for (op, row) in chunk.rows() {
503            match op {
504                Op::Insert => {
505                    if self.is_append_only {
506                        self.insert_one(row).await?;
507                    } else {
508                        self.upsert_one(row).await?;
509                    }
510                }
511                Op::UpdateInsert => {
512                    debug_assert!(!self.is_append_only);
513                    self.upsert_one(row).await?;
514                }
515                Op::Delete => {
516                    debug_assert!(!self.is_append_only);
517                    self.delete_one(row).await?;
518                }
519                Op::UpdateDelete => {}
520            }
521        }
522        Ok(())
523    }
524
525    async fn barrier(&mut self, is_checkpoint: bool) -> Result<Self::CommitMetadata> {
526        if is_checkpoint {
527            self.flush().await?;
528        }
529        Ok(())
530    }
531}
532
533#[derive(Debug)]
534pub struct SqlServerClient {
535    pub inner_client: Client<tokio_util::compat::Compat<TcpStream>>,
536}
537
538impl SqlServerClient {
539    async fn new(msconfig: &SqlServerConfig) -> Result<Self> {
540        let mut config = Config::new();
541        config.host(&msconfig.host);
542        config.port(msconfig.port);
543        config.authentication(AuthMethod::sql_server(&msconfig.user, &msconfig.password));
544        config.database(&msconfig.database);
545        config.trust_cert();
546        Self::new_with_config(config).await
547    }
548
549    pub async fn new_with_config(config: Config) -> Result<Self> {
550        let inner_client = create_mssql_client_with_config(config)
551            .await
552            .map_err(SinkError::SqlServer)?;
553        Ok(Self { inner_client })
554    }
555}
556
557async fn query_sql_server_table_metadata(
558    sql_client: &mut SqlServerClient,
559    config: &SqlServerConfig,
560) -> Result<Vec<SqlServerColumnMetadata>> {
561    let mut sql_server_table_metadata = Vec::new();
562    let query_table_metadata_error = || {
563        SinkError::SqlServer(anyhow!(format!(
564            "SQL Server table {} metadata error",
565            config.full_object_path()
566        )))
567    };
568    // Query primary-key membership through a subquery filtered by `pk.is_primary_key = 1`.
569    // A column can appear in both the primary-key index and secondary indexes, and a naive
570    // join from `sys.columns` to all `sys.index_columns` would emit extra index rows or mark
571    // secondary-index-only columns as PK columns. Keep the PK filter inside the subquery so
572    // each table column is returned once with `IsPk` set only by the primary-key index.
573    static QUERY_TABLE_METADATA: &str = r#"
574SELECT
575    col.name AS ColumnName,
576    CAST(CASE WHEN pk_col.column_id IS NULL THEN 0 ELSE 1 END AS int) AS IsPk,
577    typ.name AS DataType
578FROM
579    sys.columns col
580JOIN
581    sys.types typ ON typ.user_type_id = col.user_type_id
582LEFT JOIN
583    (
584        SELECT ic.object_id, ic.column_id
585        FROM sys.indexes pk
586        JOIN sys.index_columns ic ON ic.object_id = pk.object_id AND ic.index_id = pk.index_id
587        WHERE pk.is_primary_key = 1
588    ) pk_col ON pk_col.object_id = col.object_id AND pk_col.column_id = col.column_id
589WHERE
590    col.object_id = OBJECT_ID(@P1)
591ORDER BY
592    col.column_id;"#;
593    let rows = sql_client
594        .inner_client
595        .query(QUERY_TABLE_METADATA, &[&config.full_object_path()])
596        .await?
597        .into_results()
598        .await?;
599    for row in rows.into_iter().flatten() {
600        let mut iter = row.into_iter();
601        let ColumnData::String(Some(col_name)) =
602            iter.next().ok_or_else(query_table_metadata_error)?
603        else {
604            return Err(query_table_metadata_error());
605        };
606        let ColumnData::I32(Some(col_is_pk)) =
607            iter.next().ok_or_else(query_table_metadata_error)?
608        else {
609            return Err(query_table_metadata_error());
610        };
611        let ColumnData::String(Some(data_type)) =
612            iter.next().ok_or_else(query_table_metadata_error)?
613        else {
614            return Err(query_table_metadata_error());
615        };
616        sql_server_table_metadata.push(SqlServerColumnMetadata {
617            name: normalize_sql_server_column_name(&col_name),
618            is_pk: col_is_pk != 0,
619            data_type: data_type.into_owned(),
620        });
621    }
622    Ok(sql_server_table_metadata)
623}
624
625async fn validate_sql_server_write_permission(
626    sql_client: &mut SqlServerClient,
627    config: &SqlServerConfig,
628    is_append_only: bool,
629) -> Result<()> {
630    let permission_query_error = || {
631        SinkError::SqlServer(anyhow!(format!(
632            "SQL Server table {} permission metadata error",
633            config.full_object_path()
634        )))
635    };
636    static QUERY_WRITE_PERMISSION: &str = r#"
637SELECT
638    CAST(HAS_PERMS_BY_NAME(@P1, 'OBJECT', 'INSERT') AS int) AS CanInsert,
639    CAST(HAS_PERMS_BY_NAME(@P1, 'OBJECT', 'UPDATE') AS int) AS CanUpdate,
640    CAST(HAS_PERMS_BY_NAME(@P1, 'OBJECT', 'DELETE') AS int) AS CanDelete;"#;
641    let rows = sql_client
642        .inner_client
643        .query(QUERY_WRITE_PERMISSION, &[&config.full_object_path()])
644        .await?
645        .into_results()
646        .await?;
647    let mut rows = rows.into_iter().flatten();
648    let row = rows.next().ok_or_else(permission_query_error)?;
649    let mut iter = row.into_iter();
650    let ColumnData::I32(can_insert) = iter.next().ok_or_else(permission_query_error)? else {
651        return Err(permission_query_error());
652    };
653    let ColumnData::I32(can_update) = iter.next().ok_or_else(permission_query_error)? else {
654        return Err(permission_query_error());
655    };
656    let ColumnData::I32(can_delete) = iter.next().ok_or_else(permission_query_error)? else {
657        return Err(permission_query_error());
658    };
659
660    let missing_permissions = missing_sql_server_write_permissions(
661        is_append_only,
662        permission_is_granted(can_insert),
663        permission_is_granted(can_update),
664        permission_is_granted(can_delete),
665    );
666    if missing_permissions.is_empty() {
667        return Ok(());
668    }
669
670    Err(SinkError::SqlServer(anyhow!(format!(
671        "SQL Server user {} lacks required write permission(s) {} on table {}",
672        config.user,
673        missing_permissions.join(", "),
674        config.full_object_path()
675    ))))
676}
677
678fn permission_is_granted(permission_value: Option<i32>) -> bool {
679    permission_value == Some(1)
680}
681
682fn missing_sql_server_write_permissions(
683    is_append_only: bool,
684    can_insert: bool,
685    can_update: bool,
686    can_delete: bool,
687) -> Vec<&'static str> {
688    let mut missing_permissions = vec![];
689    if !can_insert {
690        missing_permissions.push("INSERT");
691    }
692    if !is_append_only {
693        if !can_update {
694            missing_permissions.push("UPDATE");
695        }
696        if !can_delete {
697            missing_permissions.push("DELETE");
698        }
699    }
700    missing_permissions
701}
702
703async fn query_downstream_column_metadata(
704    sql_client: &mut SqlServerClient,
705    config: &SqlServerConfig,
706    schema: &Schema,
707) -> Result<Vec<SqlServerColumnMetadata>> {
708    let sql_server_table_metadata = query_sql_server_table_metadata(sql_client, config)
709        .await?
710        .into_iter()
711        .map(|metadata| (metadata.name.clone(), metadata))
712        .collect::<HashMap<_, _>>();
713    schema
714        .fields()
715        .iter()
716        .map(|col| {
717            sql_server_table_metadata
718                .get(&normalize_sql_server_column_name(&col.name))
719                .map(|metadata| SqlServerColumnMetadata {
720                    name: metadata.name.clone(),
721                    is_pk: metadata.is_pk,
722                    data_type: metadata.data_type.clone(),
723                })
724                .ok_or_else(|| {
725                    SinkError::SqlServer(anyhow!(format!(
726                        "column {} not found in the downstream SQL Server table {}",
727                        col.name,
728                        config.full_object_path()
729                    )))
730                })
731        })
732        .collect()
733}
734
735fn param_placeholder(param_id: &mut usize) -> String {
736    let placeholder = format!("@P{}", *param_id);
737    *param_id += 1;
738    placeholder
739}
740
741fn bind_params(
742    query: &mut Query<'_>,
743    row: impl Row,
744    schema: &Schema,
745    downstream_column_data_types: &[String],
746    col_indices: impl Iterator<Item = usize>,
747) -> Result<()> {
748    use risingwave_common::types::ScalarRefImpl;
749    for col_idx in col_indices {
750        match row.datum_at(col_idx) {
751            Some(data_ref) => match data_ref {
752                ScalarRefImpl::Int16(v) => query.bind(v),
753                ScalarRefImpl::Int32(v) => query.bind(v),
754                ScalarRefImpl::Int64(v) => query.bind(v),
755                ScalarRefImpl::Float32(v) => query.bind(v.into_inner()),
756                ScalarRefImpl::Float64(v) => query.bind(v.into_inner()),
757                ScalarRefImpl::Utf8(v) => query.bind(v.to_owned()),
758                ScalarRefImpl::Bool(v) => query.bind(v),
759                ScalarRefImpl::Decimal(v) => match v {
760                    Decimal::Normalized(d) => {
761                        query.bind(decimal_to_sql(&d));
762                    }
763                    Decimal::NaN | Decimal::PositiveInf | Decimal::NegativeInf => {
764                        tracing::warn!(
765                            "Inf, -Inf, Nan in RisingWave decimal is converted into SQL Server null!"
766                        );
767                        query.bind(None as Option<Numeric>);
768                    }
769                },
770                ScalarRefImpl::Date(v) => query.bind(v.0),
771                ScalarRefImpl::Timestamp(v) => query.bind(v.0),
772                ScalarRefImpl::Timestamptz(v) => {
773                    let downstream_data_type = &downstream_column_data_types[col_idx];
774                    match downstream_data_type.as_str() {
775                        "bigint" | "int" | "smallint" | "tinyint" => {
776                            query.bind(v.timestamp_micros());
777                        }
778                        "datetimeoffset" => {
779                            query.bind(v.to_datetime_utc().fixed_offset());
780                        }
781                        "datetime" | "datetime2" | "smalldatetime" => {
782                            query.bind(v.to_datetime_utc().naive_utc());
783                        }
784                        _ => {
785                            return Err(unexpected_downstream_timestamptz_type(
786                                downstream_data_type,
787                            ));
788                        }
789                    };
790                }
791                ScalarRefImpl::Time(v) => query.bind(v.0),
792                ScalarRefImpl::Bytea(v) => query.bind(v.to_vec()),
793                ScalarRefImpl::Interval(_) => return Err(data_type_not_supported("Interval")),
794                ScalarRefImpl::Jsonb(_) => return Err(data_type_not_supported("Jsonb")),
795                ScalarRefImpl::Variant(_) => return Err(data_type_not_supported("Variant")),
796                ScalarRefImpl::Struct(_) => return Err(data_type_not_supported("Struct")),
797                ScalarRefImpl::List(_) => return Err(data_type_not_supported("List")),
798                ScalarRefImpl::Int256(_) => return Err(data_type_not_supported("Int256")),
799                ScalarRefImpl::Serial(_) => return Err(data_type_not_supported("Serial")),
800                ScalarRefImpl::Map(_) => return Err(data_type_not_supported("Map")),
801                ScalarRefImpl::Vector(_) => return Err(data_type_not_supported("Vector")),
802            },
803            None => match schema[col_idx].data_type {
804                DataType::Boolean => {
805                    query.bind(None as Option<bool>);
806                }
807                DataType::Int16 => {
808                    query.bind(None as Option<i16>);
809                }
810                DataType::Int32 => {
811                    query.bind(None as Option<i32>);
812                }
813                DataType::Int64 => {
814                    query.bind(None as Option<i64>);
815                }
816                DataType::Float32 => {
817                    query.bind(None as Option<f32>);
818                }
819                DataType::Float64 => {
820                    query.bind(None as Option<f64>);
821                }
822                DataType::Decimal => {
823                    query.bind(None as Option<Numeric>);
824                }
825                DataType::Date => {
826                    query.bind(None as Option<chrono::NaiveDate>);
827                }
828                DataType::Time => {
829                    query.bind(None as Option<chrono::NaiveTime>);
830                }
831                DataType::Timestamp => {
832                    query.bind(None as Option<chrono::NaiveDateTime>);
833                }
834                DataType::Timestamptz => {
835                    let downstream_data_type = &downstream_column_data_types[col_idx];
836                    match downstream_data_type.as_str() {
837                        "bigint" | "int" | "smallint" | "tinyint" => {
838                            query.bind(None as Option<i64>);
839                        }
840                        "datetimeoffset" => {
841                            query.bind(None as Option<chrono::DateTime<chrono::FixedOffset>>);
842                        }
843                        "datetime" | "datetime2" | "smalldatetime" => {
844                            query.bind(None as Option<chrono::NaiveDateTime>);
845                        }
846                        _ => {
847                            return Err(unexpected_downstream_timestamptz_type(
848                                downstream_data_type,
849                            ));
850                        }
851                    };
852                }
853                DataType::Varchar => {
854                    query.bind(None as Option<String>);
855                }
856                DataType::Bytea => {
857                    query.bind(None as Option<Vec<u8>>);
858                }
859                DataType::Interval => return Err(data_type_not_supported("Interval")),
860                DataType::Struct(_) => return Err(data_type_not_supported("Struct")),
861                DataType::List(_) => return Err(data_type_not_supported("List")),
862                DataType::Jsonb => return Err(data_type_not_supported("Jsonb")),
863                DataType::Variant => return Err(data_type_not_supported("Variant")),
864                DataType::Serial => return Err(data_type_not_supported("Serial")),
865                DataType::Int256 => return Err(data_type_not_supported("Int256")),
866                DataType::Map(_) => return Err(data_type_not_supported("Map")),
867                DataType::Vector(_) => return Err(data_type_not_supported("Vector")),
868            },
869        };
870    }
871    Ok(())
872}
873
874fn data_type_not_supported(data_type_name: &str) -> SinkError {
875    SinkError::SqlServer(anyhow!(format!(
876        "{data_type_name} is not supported in SQL Server"
877    )))
878}
879
880fn unexpected_downstream_timestamptz_type(sql_server_data_type: &str) -> SinkError {
881    SinkError::SqlServer(anyhow!(format!(
882        "unexpected downstream SQL Server type {sql_server_data_type} for Timestamptz"
883    )))
884}
885
886fn check_data_type_compatibility(data_type: &DataType) -> Result<()> {
887    match data_type {
888        DataType::Boolean
889        | DataType::Int16
890        | DataType::Int32
891        | DataType::Int64
892        | DataType::Float32
893        | DataType::Float64
894        | DataType::Decimal
895        | DataType::Date
896        | DataType::Varchar
897        | DataType::Time
898        | DataType::Timestamp
899        | DataType::Timestamptz
900        | DataType::Bytea => Ok(()),
901        DataType::Interval => Err(data_type_not_supported("Interval")),
902        DataType::Struct(_) => Err(data_type_not_supported("Struct")),
903        DataType::List(_) => Err(data_type_not_supported("List")),
904        DataType::Jsonb => Err(data_type_not_supported("Jsonb")),
905        DataType::Variant => Err(data_type_not_supported("Variant")),
906        DataType::Serial => Err(data_type_not_supported("Serial")),
907        DataType::Int256 => Err(data_type_not_supported("Int256")),
908        DataType::Map(_) => Err(data_type_not_supported("Map")),
909        DataType::Vector(_) => Err(data_type_not_supported("Vector")),
910    }
911}
912
913fn normalize_sql_server_column_name(column_name: &str) -> String {
914    // SQL Server identifiers are usually case-insensitive depending on database collation.
915    // Match metadata by a case-insensitive key so validation follows that common behavior.
916    column_name.to_lowercase()
917}
918
919fn validate_data_type_compatibility(
920    column_name: &str,
921    rw_data_type: &DataType,
922    sql_server_data_type: &str,
923) -> Result<()> {
924    if sql_server_data_type_is_compatible(rw_data_type, sql_server_data_type) {
925        return Ok(());
926    }
927
928    Err(SinkError::SqlServer(anyhow!(format!(
929        "column {} data type {:?} is incompatible with downstream SQL Server type {}",
930        column_name, rw_data_type, sql_server_data_type
931    ))))
932}
933
934fn sql_server_data_type_is_compatible(rw_data_type: &DataType, sql_server_data_type: &str) -> bool {
935    match rw_data_type {
936        DataType::Boolean => sql_server_data_type == "bit",
937        DataType::Int16 => matches!(sql_server_data_type, "smallint" | "int" | "bigint"),
938        DataType::Int32 => matches!(sql_server_data_type, "int" | "bigint"),
939        DataType::Int64 => sql_server_data_type == "bigint",
940        DataType::Float32 => matches!(sql_server_data_type, "real" | "float"),
941        DataType::Float64 => sql_server_data_type == "float",
942        DataType::Decimal => matches!(sql_server_data_type, "decimal" | "numeric"),
943        DataType::Date => sql_server_data_type == "date",
944        DataType::Varchar => matches!(
945            sql_server_data_type,
946            "char" | "nchar" | "varchar" | "nvarchar" | "text" | "ntext"
947        ),
948        DataType::Time => sql_server_data_type == "time",
949        DataType::Timestamp => {
950            matches!(
951                sql_server_data_type,
952                "datetime" | "datetime2" | "smalldatetime"
953            )
954        }
955        DataType::Timestamptz => matches!(
956            sql_server_data_type,
957            "datetimeoffset"
958                | "datetime"
959                | "datetime2"
960                | "smalldatetime"
961                | "bigint"
962                | "int"
963                | "smallint"
964                | "tinyint"
965        ),
966        DataType::Bytea => matches!(sql_server_data_type, "binary" | "varbinary" | "image"),
967        DataType::Interval
968        | DataType::Struct(_)
969        | DataType::List(_)
970        | DataType::Jsonb
971        | DataType::Variant
972        | DataType::Serial
973        | DataType::Int256
974        | DataType::Map(_)
975        | DataType::Vector(_) => false,
976    }
977}
978
979/// The implementation is copied from tiberius crate.
980fn decimal_to_sql(decimal: &rust_decimal::Decimal) -> Numeric {
981    let unpacked = decimal.unpack();
982
983    let mut value = (((unpacked.hi as u128) << 64)
984        + ((unpacked.mid as u128) << 32)
985        + unpacked.lo as u128) as i128;
986
987    if decimal.is_sign_negative() {
988        value = -value;
989    }
990
991    Numeric::new_with_scale(value, decimal.scale() as u8)
992}
993
994#[cfg(test)]
995mod tests {
996    use super::*;
997
998    #[test]
999    fn test_normalize_sql_server_column_name() {
1000        assert_eq!(normalize_sql_server_column_name("EventDate"), "eventdate");
1001    }
1002
1003    #[test]
1004    fn test_sql_server_data_type_compatibility() {
1005        assert!(sql_server_data_type_is_compatible(
1006            &DataType::Int16,
1007            "smallint"
1008        ));
1009        assert!(sql_server_data_type_is_compatible(&DataType::Int16, "int"));
1010        assert!(!sql_server_data_type_is_compatible(
1011            &DataType::Int32,
1012            "smallint"
1013        ));
1014
1015        assert!(sql_server_data_type_is_compatible(
1016            &DataType::Timestamp,
1017            "datetime2"
1018        ));
1019        assert!(sql_server_data_type_is_compatible(
1020            &DataType::Timestamptz,
1021            "datetimeoffset"
1022        ));
1023        assert!(sql_server_data_type_is_compatible(
1024            &DataType::Timestamptz,
1025            "datetime2"
1026        ));
1027        assert!(sql_server_data_type_is_compatible(
1028            &DataType::Timestamptz,
1029            "bigint"
1030        ));
1031        assert!(sql_server_data_type_is_compatible(
1032            &DataType::Timestamptz,
1033            "int"
1034        ));
1035        assert!(!sql_server_data_type_is_compatible(
1036            &DataType::Timestamp,
1037            "datetimeoffset"
1038        ));
1039
1040        assert!(sql_server_data_type_is_compatible(
1041            &DataType::Varchar,
1042            "nvarchar"
1043        ));
1044        assert!(!sql_server_data_type_is_compatible(
1045            &DataType::Varchar,
1046            "uniqueidentifier"
1047        ));
1048    }
1049
1050    #[test]
1051    fn test_missing_sql_server_write_permissions() {
1052        assert_eq!(
1053            missing_sql_server_write_permissions(true, false, false, false),
1054            vec!["INSERT"]
1055        );
1056        assert!(missing_sql_server_write_permissions(true, true, false, false).is_empty());
1057        assert_eq!(
1058            missing_sql_server_write_permissions(false, false, false, false),
1059            vec!["INSERT", "UPDATE", "DELETE"]
1060        );
1061        assert_eq!(
1062            missing_sql_server_write_permissions(false, true, false, true),
1063            vec!["UPDATE"]
1064        );
1065        assert!(missing_sql_server_write_permissions(false, true, true, true).is_empty());
1066    }
1067}