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