From 6a6e5fbad9e61f0fc01cbb78ad233bec71afb8cb Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 27 Aug 2026 17:10:10 +0800 Subject: [PATCH] fix(core): use row_number pagination for sqlserver table transfers Closes #7356 --- crates/dbx-core/src/transfer.rs | 107 +++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 10 deletions(-) diff --git a/crates/dbx-core/src/transfer.rs b/crates/dbx-core/src/transfer.rs index be35c1b513..1e4aa9d662 100644 --- a/crates/dbx-core/src/transfer.rs +++ b/crates/dbx-core/src/transfer.rs @@ -1895,6 +1895,22 @@ fn oracle_rownum_page_sql(col_list: &str, base_sql: String, offset: u64, limit: ) } +// SQL Server 2008 R2 and older reject `OFFSET ... FETCH` (added in SQL Server +// 2012), so paged reads must use a ROW_NUMBER() subquery that every supported +// SQL Server version accepts (issue #7356). +fn sqlserver_row_number_page_sql( + col_list: &str, + from_clause: &str, + order_by: &str, + offset: u64, + limit: usize, +) -> String { + let end = offset + limit as u64; + format!( + "SELECT {col_list} FROM (SELECT {col_list}, ROW_NUMBER() OVER (ORDER BY {order_by}) AS __dbx_row_num FROM {from_clause}) AS __dbx_page WHERE __dbx_row_num > {offset} AND __dbx_row_num <= {end}" + ) +} + fn postgres_index_column_sql(column: &str) -> String { if is_simple_identifier(column) { quote_identifier(column, &DatabaseType::Postgres) @@ -4055,7 +4071,10 @@ pub fn pagination_sql( format!("SELECT SKIP {offset} FIRST {limit} {col_list} FROM {full_table}") } } - DatabaseType::SqlServer | DatabaseType::Dameng => { + DatabaseType::SqlServer => { + sqlserver_row_number_page_sql(&col_list, &full_table, "(SELECT NULL)", offset, limit) + } + DatabaseType::Dameng => { format!( "SELECT {col_list} FROM {full_table} ORDER BY (SELECT NULL) OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY" ) @@ -4098,7 +4117,11 @@ pub fn pagination_sql_with_order( format!("SELECT SKIP {offset} FIRST {limit} {col_list} FROM {full_table}{order_by}") } } - DatabaseType::SqlServer | DatabaseType::Dameng => { + DatabaseType::SqlServer => { + let order_by = order_expression.unwrap_or_else(|| "(SELECT NULL)".to_string()); + sqlserver_row_number_page_sql(&col_list, &full_table, &order_by, offset, limit) + } + DatabaseType::Dameng => { let order_by = order_expression.unwrap_or_else(|| "(SELECT NULL)".to_string()); format!( "SELECT {col_list} FROM {full_table} ORDER BY {order_by} OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY" @@ -4182,7 +4205,12 @@ pub fn pagination_sql_with_filter_order_and_identifier_quote( format!("SELECT SKIP {offset} FIRST {limit} {col_list} FROM {full_table}{where_clause}{order_by}") } } - DatabaseType::SqlServer | DatabaseType::Dameng => { + DatabaseType::SqlServer => { + let order_by = order_expression.unwrap_or_else(|| "(SELECT NULL)".to_string()); + let from_clause = format!("{full_table}{where_clause}"); + sqlserver_row_number_page_sql(&col_list, &from_clause, &order_by, offset, limit) + } + DatabaseType::Dameng => { let order_by = order_expression.unwrap_or_else(|| "(SELECT NULL)".to_string()); format!( "SELECT {col_list} FROM {full_table}{where_clause} ORDER BY {order_by} OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY" @@ -4282,7 +4310,10 @@ pub fn keyset_pagination_sql_with_identifier_quote( DatabaseType::Informix => { format!("SELECT FIRST {limit} {col_list} FROM {full_table}{where_clause} ORDER BY {order}") } - DatabaseType::SqlServer | DatabaseType::Dameng => { + DatabaseType::SqlServer => { + format!("SELECT TOP ({limit}) {col_list} FROM {full_table}{where_clause} ORDER BY {order}") + } + DatabaseType::Dameng => { format!( "SELECT {col_list} FROM {full_table}{where_clause} ORDER BY {order} OFFSET 0 ROWS FETCH NEXT {limit} ROWS ONLY" ) @@ -10500,6 +10531,65 @@ mod tests { assert!(!sql.contains(" LIMIT ")); } + #[test] + fn sqlserver_export_pagination_uses_row_number_subquery() { + let sql = pagination_sql( + &[String::from("id"), String::from("name")], + "users", + "dbo", + &DatabaseType::SqlServer, + 500, + 100, + ); + + assert_eq!( + sql, + "SELECT [id], [name] FROM (SELECT [id], [name], ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS __dbx_row_num FROM [dbo].[users]) AS __dbx_page WHERE __dbx_row_num > 500 AND __dbx_row_num <= 600" + ); + assert!(!sql.contains("OFFSET")); + assert!(!sql.contains(" FETCH ")); + } + + #[test] + fn sqlserver_ordered_pagination_uses_row_number_subquery() { + let sql = pagination_sql_with_order( + &[String::from("id"), String::from("name")], + "users", + "dbo", + &DatabaseType::SqlServer, + 200, + 100, + &[String::from("id")], + None, + ); + + assert!(sql.contains("ROW_NUMBER() OVER (ORDER BY [id]) AS __dbx_row_num")); + assert!(sql.contains("WHERE __dbx_row_num > 200 AND __dbx_row_num <= 300")); + assert!(!sql.contains("OFFSET")); + assert!(!sql.contains(" FETCH ")); + } + + #[test] + fn sqlserver_filtered_pagination_preserves_filter_in_subquery() { + let sql = pagination_sql_with_filter_order( + &[String::from("id"), String::from("status")], + "users", + "dbo", + &DatabaseType::SqlServer, + 10_000, + 2_000, + Some("WHERE status = 'active'"), + Some("[id] DESC"), + &[String::from("id")], + ); + + assert!(sql.contains("ROW_NUMBER() OVER (ORDER BY [id] DESC) AS __dbx_row_num")); + assert!(sql.contains("FROM [dbo].[users] WHERE (status = 'active')")); + assert!(sql.contains("WHERE __dbx_row_num > 10000 AND __dbx_row_num <= 12000")); + assert!(!sql.contains("OFFSET")); + assert!(!sql.contains(" FETCH ")); + } + #[test] fn filtered_pagination_preserves_where_and_order() { let sql = pagination_sql_with_filter_order( @@ -10572,7 +10662,7 @@ mod tests { } #[test] - fn sqlserver_keyset_pagination_includes_offset_fetch() { + fn sqlserver_keyset_pagination_uses_top() { let sql = keyset_pagination_sql( &[String::from("id"), String::from("name")], "users", @@ -10583,10 +10673,7 @@ mod tests { 100, ); - assert_eq!( - sql, - "SELECT [id], [name] FROM [dbo].[users] ORDER BY [id] ASC OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY" - ); + assert_eq!(sql, "SELECT TOP (100) [id], [name] FROM [dbo].[users] ORDER BY [id] ASC"); } #[test] @@ -10642,7 +10729,7 @@ mod tests { assert_eq!( sql, - "SELECT [tenant_id], [id], [name] FROM [dbo].[users] WHERE ([tenant_id] > 10 OR ([tenant_id] = 10 AND [id] > 25)) ORDER BY [tenant_id] ASC, [id] ASC OFFSET 0 ROWS FETCH NEXT 100 ROWS ONLY" + "SELECT TOP (100) [tenant_id], [id], [name] FROM [dbo].[users] WHERE ([tenant_id] > 10 OR ([tenant_id] = 10 AND [id] > 25)) ORDER BY [tenant_id] ASC, [id] ASC" ); }