risingwave_connector/source/cdc/external/
sql_server.rs1use std::cmp::Ordering;
16
17use anyhow::{Context, anyhow};
18use futures::stream::BoxStream;
19use futures::{StreamExt, TryStreamExt, pin_mut, stream};
20use futures_async_stream::try_stream;
21use itertools::Itertools;
22use risingwave_common::bail;
23use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, Schema};
24use risingwave_common::row::OwnedRow;
25use risingwave_common::types::{DataType, ScalarImpl};
26use serde::{Deserialize, Serialize};
27use tiberius::{Config, Query, QueryItem};
28
29use crate::error::{ConnectorError, ConnectorResult};
30use crate::parser::{ScalarImplTiberiusWrapper, sql_server_row_to_owned_row_with_strict_pk};
31use crate::sink::sqlserver::SqlServerClient;
32use crate::source::CdcTableSnapshotSplit;
33use crate::source::cdc::external::{
34 CdcOffset, CdcOffsetParseFunc, CdcTableSnapshotSplitOption, DebeziumOffset,
35 ExternalTableConfig, ExternalTableReader, SchemaTableName,
36};
37
38const MAX_COMMIT_LSN: &str = "ffffffff:ffffffff:ffff";
40
41#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
42pub struct SqlServerOffset {
43 pub change_lsn: String,
45 pub commit_lsn: String,
46}
47
48impl PartialOrd for SqlServerOffset {
50 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
51 match self.change_lsn.partial_cmp(&other.change_lsn) {
52 Some(Ordering::Equal) => self.commit_lsn.partial_cmp(&other.commit_lsn),
53 other => other,
54 }
55 }
56}
57
58impl SqlServerOffset {
59 pub fn parse_debezium_offset(offset: &str) -> ConnectorResult<Self> {
60 let dbz_offset: DebeziumOffset = serde_json::from_str(offset)
61 .with_context(|| format!("invalid upstream offset: {}", offset))?;
62
63 Ok(Self {
64 change_lsn: dbz_offset
65 .source_offset
66 .change_lsn
67 .context("invalid sql server change_lsn")?,
68 commit_lsn: dbz_offset
69 .source_offset
70 .commit_lsn
71 .context("invalid sql server commit_lsn")?,
72 })
73 }
74}
75
76pub struct SqlServerExternalTable {
77 column_descs: Vec<ColumnDesc>,
78 pk_names: Vec<String>,
79}
80
81impl SqlServerExternalTable {
82 pub async fn connect(config: ExternalTableConfig) -> ConnectorResult<Self> {
83 tracing::debug!("connect to sql server");
84
85 let mut client_config = Config::new();
86
87 client_config.host(&config.host);
88 client_config.database(&config.database);
89 client_config.port(config.port.parse::<u16>().unwrap());
90 client_config.authentication(tiberius::AuthMethod::sql_server(
91 &config.username,
92 &config.password,
93 ));
94 if config.encrypt == "true" {
96 client_config.encryption(tiberius::EncryptionLevel::Required);
97 }
98 client_config.trust_cert();
99
100 let mut client = SqlServerClient::new_with_config(client_config).await?;
101
102 let mut column_descs = vec![];
103 let mut pk_names = vec![];
104 {
105 let sql = Query::new(format!(
106 "SELECT
107 COLUMN_NAME,
108 DATA_TYPE
109 FROM
110 INFORMATION_SCHEMA.COLUMNS
111 WHERE
112 TABLE_SCHEMA = '{}'
113 AND TABLE_NAME = '{}'",
114 config.schema.clone(),
115 config.table.clone(),
116 ));
117
118 let mut stream = sql.query(&mut client.inner_client).await?;
119 while let Some(item) = stream.try_next().await? {
120 match item {
121 QueryItem::Metadata(_) => {}
122 QueryItem::Row(row) => {
123 let col_name: &str = row.try_get(0)?.unwrap();
124 let col_type: &str = row.try_get(1)?.unwrap();
125 column_descs.push(ColumnDesc::named(
126 col_name,
127 ColumnId::placeholder(),
128 mssql_type_to_rw_type(col_type, col_name)?,
129 ));
130 }
131 }
132 }
133 }
134 {
135 let sql = Query::new(format!(
136 "SELECT kcu.COLUMN_NAME
137 FROM
138 INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
139 JOIN
140 INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS kcu
141 ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND
142 tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND
143 tc.TABLE_NAME = kcu.TABLE_NAME
144 WHERE
145 tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND
146 tc.TABLE_SCHEMA = '{}' AND tc.TABLE_NAME = '{}'",
147 config.schema, config.table,
148 ));
149
150 let mut stream = sql.query(&mut client.inner_client).await?;
151 while let Some(item) = stream.try_next().await? {
152 match item {
153 QueryItem::Metadata(_) => {}
154 QueryItem::Row(row) => {
155 let pk_name: &str = row.try_get(0)?.unwrap();
156 pk_names.push(pk_name.to_owned());
157 }
158 }
159 }
160 }
161
162 if column_descs.is_empty() {
164 bail!(
165 "Sql Server table '{}'.'{}' not found in '{}'",
166 config.schema,
167 config.table,
168 config.database
169 );
170 }
171
172 Ok(Self {
173 column_descs,
174 pk_names,
175 })
176 }
177
178 pub fn column_descs(&self) -> &Vec<ColumnDesc> {
179 &self.column_descs
180 }
181
182 pub fn pk_names(&self) -> &Vec<String> {
183 &self.pk_names
184 }
185}
186
187fn mssql_type_to_rw_type(col_type: &str, col_name: &str) -> ConnectorResult<DataType> {
188 let dtype = match col_type.to_lowercase().as_str() {
189 "bit" => DataType::Boolean,
190 "binary" | "varbinary" => DataType::Bytea,
191 "tinyint" | "smallint" => DataType::Int16,
192 "integer" | "int" => DataType::Int32,
193 "bigint" => DataType::Int64,
194 "real" => DataType::Float32,
195 "float" => DataType::Float64,
196 "decimal" | "numeric" => DataType::Decimal,
197 "date" => DataType::Date,
198 "time" => DataType::Time,
199 "datetime" | "datetime2" | "smalldatetime" => DataType::Timestamp,
200 "datetimeoffset" => DataType::Timestamptz,
201 "char" | "nchar" | "varchar" | "nvarchar" | "text" | "ntext" | "xml"
202 | "uniqueidentifier" => DataType::Varchar,
203 "money" => DataType::Decimal,
204 mssql_type => {
205 return Err(anyhow!(
206 "Unsupported Sql Server data type: {:?}, column name: {}",
207 mssql_type,
208 col_name
209 )
210 .into());
211 }
212 };
213 Ok(dtype)
214}
215
216#[derive(Debug)]
217pub struct SqlServerExternalTableReader {
218 rw_schema: Schema,
219 pk_indices: Vec<usize>,
220 field_names: String,
221 client: tokio::sync::Mutex<SqlServerClient>,
222}
223
224impl ExternalTableReader for SqlServerExternalTableReader {
225 async fn current_cdc_offset(&self) -> ConnectorResult<CdcOffset> {
226 let mut client = self.client.lock().await;
227 let row = client
229 .inner_client
230 .simple_query(String::from("SELECT sys.fn_cdc_get_max_lsn()"))
231 .await?
232 .into_row()
233 .await?
234 .expect("No result returned by `SELECT sys.fn_cdc_get_max_lsn()`");
235 let max_lsn = match row.try_get::<&[u8], usize>(0)? {
238 Some(bytes) => {
239 let mut hex_string = String::with_capacity(bytes.len() * 2 + 2);
240 assert_eq!(
241 bytes.len(),
242 10,
243 "sys.fn_cdc_get_max_lsn() should return a 10 bytes array."
244 );
245 for byte in &bytes[0..4] {
246 hex_string.push_str(&format!("{:02x}", byte));
247 }
248 hex_string.push(':');
249 for byte in &bytes[4..8] {
250 hex_string.push_str(&format!("{:02x}", byte));
251 }
252 hex_string.push(':');
253 for byte in &bytes[8..10] {
254 hex_string.push_str(&format!("{:02x}", byte));
255 }
256 hex_string
257 }
258 None => bail!(
259 "None is returned by `SELECT sys.fn_cdc_get_max_lsn()`, please ensure Sql Server Agent is running."
260 ),
261 };
262
263 tracing::debug!("current max_lsn: {}", max_lsn);
264
265 Ok(CdcOffset::SqlServer(SqlServerOffset {
266 change_lsn: max_lsn,
267 commit_lsn: MAX_COMMIT_LSN.into(),
268 }))
269 }
270
271 fn snapshot_read(
272 &self,
273 table_name: SchemaTableName,
274 start_pk: Option<OwnedRow>,
275 primary_keys: Vec<String>,
276 limit: u32,
277 ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
278 self.snapshot_read_inner(table_name, start_pk, primary_keys, limit)
279 }
280
281 fn get_parallel_cdc_splits(
282 &self,
283 _options: CdcTableSnapshotSplitOption,
284 ) -> BoxStream<'_, ConnectorResult<CdcTableSnapshotSplit>> {
285 stream::empty::<ConnectorResult<CdcTableSnapshotSplit>>().boxed()
287 }
288
289 fn split_snapshot_read(
290 &self,
291 _table_name: SchemaTableName,
292 _left: OwnedRow,
293 _right: OwnedRow,
294 _split_columns: Vec<Field>,
295 ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
296 todo!("implement SqlServer CDC parallelized backfill")
297 }
298}
299
300impl SqlServerExternalTableReader {
301 pub async fn new(
302 config: ExternalTableConfig,
303 rw_schema: Schema,
304 pk_indices: Vec<usize>,
305 ) -> ConnectorResult<Self> {
306 tracing::info!(
307 ?rw_schema,
308 ?pk_indices,
309 "create sql server external table reader"
310 );
311 let mut client_config = Config::new();
312
313 client_config.host(&config.host);
314 client_config.database(&config.database);
315 client_config.port(config.port.parse::<u16>().unwrap());
316 client_config.authentication(tiberius::AuthMethod::sql_server(
317 &config.username,
318 &config.password,
319 ));
320 if config.encrypt == "true" {
322 client_config.encryption(tiberius::EncryptionLevel::Required);
323 }
324 client_config.trust_cert();
325
326 let client = SqlServerClient::new_with_config(client_config).await?;
327
328 let field_names = rw_schema
329 .fields
330 .iter()
331 .map(|f| Self::quote_column(&f.name))
332 .join(",");
333
334 Ok(Self {
335 rw_schema,
336 pk_indices,
337 field_names,
338 client: tokio::sync::Mutex::new(client),
339 })
340 }
341
342 pub fn get_cdc_offset_parser() -> CdcOffsetParseFunc {
343 Box::new(move |offset| {
344 Ok(CdcOffset::SqlServer(
345 SqlServerOffset::parse_debezium_offset(offset)?,
346 ))
347 })
348 }
349
350 #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
351 async fn snapshot_read_inner(
352 &self,
353 table_name: SchemaTableName,
354 start_pk_row: Option<OwnedRow>,
355 primary_keys: Vec<String>,
356 limit: u32,
357 ) {
358 let order_key = primary_keys
359 .iter()
360 .map(|col| Self::quote_column(col))
361 .join(",");
362 let mut sql = Query::new(if start_pk_row.is_none() {
363 format!(
364 "SELECT {} FROM {} ORDER BY {} OFFSET 0 ROWS FETCH NEXT {limit} ROWS ONLY",
365 self.field_names,
366 Self::get_normalized_table_name(&table_name),
367 order_key,
368 )
369 } else {
370 let filter_expr = Self::filter_expression(&primary_keys);
371 format!(
372 "SELECT {} FROM {} WHERE {} ORDER BY {} OFFSET 0 ROWS FETCH NEXT {limit} ROWS ONLY",
373 self.field_names,
374 Self::get_normalized_table_name(&table_name),
375 filter_expr,
376 order_key,
377 )
378 });
379
380 let mut client = self.client.lock().await;
381
382 if let Some(pk_row) = start_pk_row {
384 let params: Vec<Option<ScalarImpl>> = pk_row.into_iter().collect();
385 for (index, param) in params.into_iter().enumerate() {
386 let param = param.with_context(|| {
387 format!(
388 "SQL Server snapshot primary-key position at index {index} cannot be NULL"
389 )
390 })?;
391 sql.bind(ScalarImplTiberiusWrapper::from(param));
392 }
393 }
394
395 let stream = sql.query(&mut client.inner_client).await?.into_row_stream();
396
397 let row_stream = stream.map(|res| {
398 let mut row = res?;
400 sql_server_row_to_owned_row_with_strict_pk(&mut row, &self.rw_schema, &self.pk_indices)
401 .map_err(ConnectorError::from)
402 });
403
404 pin_mut!(row_stream);
405
406 #[for_await]
407 for row in row_stream {
408 let row = row?;
409 yield row;
410 }
411 }
412
413 pub fn get_normalized_table_name(table_name: &SchemaTableName) -> String {
414 format!(
415 "\"{}\".\"{}\"",
416 table_name.schema_name, table_name.table_name
417 )
418 }
419
420 fn filter_expression(columns: &[String]) -> String {
424 let mut conditions = vec![];
425 conditions.push(format!("({} > @P{})", Self::quote_column(&columns[0]), 1));
427 for i in 2..=columns.len() {
428 let mut condition = String::new();
430 for (j, col) in columns.iter().enumerate().take(i - 1) {
431 if j == 0 {
432 condition.push_str(&format!("({} = @P{})", Self::quote_column(col), j + 1));
433 } else {
434 condition.push_str(&format!(
435 " AND ({} = @P{})",
436 Self::quote_column(col),
437 j + 1
438 ));
439 }
440 }
441 condition.push_str(&format!(
443 " AND ({} > @P{})",
444 Self::quote_column(&columns[i - 1]),
445 i
446 ));
447 conditions.push(format!("({})", condition));
448 }
449 if columns.len() > 1 {
450 conditions.join(" OR ")
451 } else {
452 conditions.join("")
453 }
454 }
455
456 fn quote_column(column: &str) -> String {
457 format!("\"{}\"", column)
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use crate::source::cdc::external::SqlServerExternalTableReader;
464
465 #[test]
466 fn test_sql_server_filter_expr() {
467 let cols = vec!["id".to_owned()];
468 let expr = SqlServerExternalTableReader::filter_expression(&cols);
469 assert_eq!(expr, "(\"id\" > @P1)");
470
471 let cols = vec!["aa".to_owned(), "bb".to_owned(), "cc".to_owned()];
472 let expr = SqlServerExternalTableReader::filter_expression(&cols);
473 assert_eq!(
474 expr,
475 "(\"aa\" > @P1) OR ((\"aa\" = @P1) AND (\"bb\" > @P2)) OR ((\"aa\" = @P1) AND (\"bb\" = @P2) AND (\"cc\" > @P3))"
476 );
477 }
478}