fix(core): use row_number pagination for sqlserver table transfers

Closes #7356
This commit is contained in:
t8y2 committed 2026-08-27 17:10:10 +08:00
1 parent e8d6370b1e
commit 6a6e5fbad9
1 file changed
+97 -10
+97 -10
View File
@@ -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"
);
}