fix(sqlserver): paginate joins with duplicate result columns

Closes #5284
This commit is contained in:
t8y2 committed 2026-08-04 12:14:49 +08:00
1 parent b6e1d6d9c4
commit f8c71e61dc
3 files changed
+217 -31

No files matched your search

@@ -1,4 +1,6 @@
use dbx_core::db::sqlserver;
use dbx_core::models::connection::DatabaseType;
use dbx_core::query_result_sql::{build_paginated_query_sql, PaginatedQuerySqlOptions};
use std::time::Duration;
async fn connect_sqlserver() -> sqlserver::SqlServerClient {
@@ -71,3 +73,79 @@ async fn sqlserver_single_result_query_drains_later_results() {
.expect("execute query after draining later results");
assert_eq!(follow_up.rows[0][0].as_i64(), Some(2));
}
#[tokio::test]
#[ignore = "requires DBX_TEST_SQLSERVER_HOST and DBX_TEST_SQLSERVER_PASSWORD"]
async fn sqlserver_duplicate_join_columns_page_without_leaking_rowcount() {
let mut client = connect_sqlserver().await;
sqlserver::execute_simple_batch_with_max_rows(
&mut client,
"CREATE TABLE #dbx_page_detail (id INT NOT NULL PRIMARY KEY, parent_id INT NOT NULL); \
CREATE TABLE #dbx_page_main (id INT NOT NULL PRIMARY KEY, label NVARCHAR(20) NOT NULL); \
WITH numbers AS ( \
SELECT TOP (1200) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS value \
FROM sys.all_objects a CROSS JOIN sys.all_objects b \
) \
INSERT INTO #dbx_page_main (id, label) SELECT value, CONCAT(N'label-', value) FROM numbers; \
INSERT INTO #dbx_page_detail (id, parent_id) SELECT id, id FROM #dbx_page_main;",
None,
)
.await
.expect("create pagination fixtures");
let reported_query = "SELECT * FROM #dbx_page_detail d LEFT JOIN #dbx_page_main m ON m.id = d.parent_id";
let reported_page = build_paginated_query_sql(PaginatedQuerySqlOptions {
original_sql: reported_query.to_string(),
database_type: Some(DatabaseType::SqlServer),
limit: 500,
offset: 500,
});
let reported_results = sqlserver::execute_batch_with_max_rows(
&mut client,
reported_page.sql.as_deref().expect("build reported pagination SQL"),
Some(500),
)
.await
.expect("execute reported duplicate-column page");
assert_eq!(reported_results.first().expect("reported page result").rows.len(), 500);
let query = format!("{reported_query} ORDER BY d.id");
let page = build_paginated_query_sql(PaginatedQuerySqlOptions {
original_sql: query,
database_type: Some(DatabaseType::SqlServer),
limit: 500,
offset: 500,
});
let page_sql = page.sql.expect("build duplicate-column pagination SQL");
let results = sqlserver::execute_batch_with_max_rows(&mut client, &page_sql, Some(500))
.await
.expect("execute duplicate-column page");
let result = results.first().expect("page result");
assert_eq!(result.rows.len(), 500);
assert_eq!(result.rows.first().and_then(|row| row[0].as_i64()), Some(501));
assert_eq!(result.rows.last().and_then(|row| row[0].as_i64()), Some(1000));
let failing_page = build_paginated_query_sql(PaginatedQuerySqlOptions {
original_sql:
"SELECT * FROM #dbx_page_detail d LEFT JOIN #dbx_missing_page_table m ON m.id = d.parent_id ORDER BY d.id"
.to_string(),
database_type: Some(DatabaseType::SqlServer),
limit: 1,
offset: 1,
});
let error = sqlserver::execute_batch_with_max_rows(
&mut client,
failing_page.sql.as_deref().expect("build failing pagination SQL"),
Some(1),
)
.await
.expect_err("missing joined table should fail");
assert!(error.to_ascii_lowercase().contains("dbx_missing_page_table"));
let follow_up =
sqlserver::execute_query(&mut client, "SELECT value FROM (VALUES (1), (2)) rows(value) ORDER BY value")
.await
.expect("execute query after ROWCOUNT page");
assert_eq!(follow_up.rows.len(), 2);
}