From 4a4a7603b562d6cefeebbd864498bd8e851e8bfb Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 12 Oct 2026 19:32:53 +0800 Subject: [PATCH] fix(sql): sort Oracle duplicate columns without derived tables --- crates/dbx-sql-data/src/query_result_sql.rs | 81 ++++++++++++++++++--- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/crates/dbx-sql-data/src/query_result_sql.rs b/crates/dbx-sql-data/src/query_result_sql.rs index ac55d46d73..9c198f8b89 100644 --- a/crates/dbx-sql-data/src/query_result_sql.rs +++ b/crates/dbx-sql-data/src/query_result_sql.rs @@ -466,7 +466,19 @@ pub fn build_sorted_query_sql(options: SortedQuerySqlOptions) -> QuerySqlBuildRe return err("not_select"); } - if options.database_type == Some(DatabaseType::OceanbaseOracle) && !rownum_wrapper_projection_is_safe(statement) { + // Oracle expands `SELECT *` over a derived table by name, so a projection + // with duplicate labels fails with ORA-00918 no matter how ORDER BY refers + // to the column. Sort the original statement by position instead. + let oracle_has_duplicate_labels = options.database_type == Some(DatabaseType::Oracle) + && options + .result_columns + .iter() + .enumerate() + .any(|(index, column)| options.result_columns[..index].iter().any(|candidate| candidate == column)); + if oracle_has_duplicate_labels + || (options.database_type == Some(DatabaseType::OceanbaseOracle) + && !rownum_wrapper_projection_is_safe(statement)) + { let Ok(statements) = Parser::parse_sql(&GenericDialect {}, statement) else { return err("unsupported"); }; @@ -540,13 +552,7 @@ pub fn build_sorted_query_sql(options: SortedQuerySqlOptions) -> QuerySqlBuildRe let use_sort_ordinal = !use_derived_column_aliases && matches!( options.database_type, - Some( - DatabaseType::Dameng - | DatabaseType::Oracle - | DatabaseType::OceanbaseOracle - | DatabaseType::SapHana - | DatabaseType::Iris - ) + Some(DatabaseType::Dameng | DatabaseType::OceanbaseOracle | DatabaseType::SapHana | DatabaseType::Iris) ) && options.result_columns.get(options.column_index).is_some_and(|column| { options.result_columns.iter().filter(|candidate| candidate.eq_ignore_ascii_case(column)).count() > 1 @@ -6475,12 +6481,69 @@ WHERE u.id = picked.id; direction: QuerySortDirection::Asc, }); + assert_eq!(result.sql.unwrap(), "SELECT a.id, b.id FROM a JOIN b ON b.a_id = a.id ORDER BY 1 ASC;"); + } + + #[test] + fn oracle_sort_with_duplicate_labels_does_not_wrap_the_query() { + let result = build_sorted_query_sql(SortedQuerySqlOptions { + original_sql: "select c1, t.* from dbx_eval_t t order by c1".to_string(), + database_type: Some(DatabaseType::Oracle), + result_columns: vec!["C1".to_string(), "C1".to_string(), "C2".to_string()], + column_index: 2, + column: "C2".to_string(), + direction: QuerySortDirection::Desc, + }); + + assert_eq!(result.sql.unwrap(), "select c1, t.* from dbx_eval_t t ORDER BY 3 DESC;"); + } + + #[test] + fn oracle_sort_with_duplicate_labels_rejects_row_limiting_clause() { + let result = build_sorted_query_sql(SortedQuerySqlOptions { + original_sql: "SELECT c1, t.* FROM dbx_eval_t t FETCH FIRST 10 ROWS ONLY".to_string(), + database_type: Some(DatabaseType::Oracle), + result_columns: vec!["C1".to_string(), "C1".to_string(), "C2".to_string()], + column_index: 0, + column: "C1".to_string(), + direction: QuerySortDirection::Asc, + }); + + assert!(!result.ok); + assert_eq!(result.reason.as_deref(), Some("unsupported")); + } + + #[test] + fn oracle_sort_with_case_distinct_quoted_labels_keeps_the_derived_table() { + let result = build_sorted_query_sql(SortedQuerySqlOptions { + original_sql: "SELECT id AS \"x\", id AS \"X\" FROM t WHERE ROWNUM <= 10".to_string(), + database_type: Some(DatabaseType::Oracle), + result_columns: vec!["x".to_string(), "X".to_string()], + column_index: 1, + column: "X".to_string(), + direction: QuerySortDirection::Desc, + }); + assert_eq!( result.sql.unwrap(), - "SELECT * FROM (SELECT a.id, b.id FROM a JOIN b ON b.a_id = a.id) t ORDER BY 1 ASC;" + "SELECT * FROM (SELECT id AS \"x\", id AS \"X\" FROM t WHERE ROWNUM <= 10) t ORDER BY \"X\" DESC;" ); } + #[test] + fn oracle_sort_without_duplicate_labels_keeps_the_derived_table() { + let result = build_sorted_query_sql(SortedQuerySqlOptions { + original_sql: "SELECT * FROM dbx_eval_t".to_string(), + database_type: Some(DatabaseType::Oracle), + result_columns: vec!["C1".to_string(), "C2".to_string()], + column_index: 1, + column: "C2".to_string(), + direction: QuerySortDirection::Asc, + }); + + assert_eq!(result.sql.unwrap(), "SELECT * FROM (SELECT * FROM dbx_eval_t) t ORDER BY \"C2\" ASC;"); + } + #[test] fn builds_saphana_sorted_query_by_ordinal_for_duplicate_columns() { let result = build_sorted_query_sql(SortedQuerySqlOptions {