diff --git a/prqlc/prqlc/src/sql/gen_query.rs b/prqlc/prqlc/src/sql/gen_query.rs index 5f8ec2cfc21f..e2262af4127e 100644 --- a/prqlc/prqlc/src/sql/gen_query.rs +++ b/prqlc/prqlc/src/sql/gen_query.rs @@ -218,10 +218,19 @@ fn translate_select_pipeline( }) } if order_by.is_empty() { + // When DISTINCT is used, MSSQL requires ORDER BY items to appear + // in the SELECT list. Use the first column from the projection + // instead of (SELECT NULL). + let order_expr = is_distinct + .then(|| first_expr_from_projection(&projection)) + .flatten() + .unwrap_or_else(|| { + sql_ast::Expr::Value( + sql_ast::Value::Placeholder("(SELECT NULL)".to_string()).into(), + ) + }); order_by.push(sql_ast::OrderByExpr { - expr: sql_ast::Expr::Value( - sql_ast::Value::Placeholder("(SELECT NULL)".to_string()).into(), - ), + expr: order_expr, options: sqlparser::ast::OrderByOptions { asc: None, nulls_first: None, @@ -726,6 +735,22 @@ fn count_tables(transforms: &[Transform]) -> usize { count } + +/// Extract the first expression from a projection for use in ORDER BY. +/// Returns None if the projection is empty or only contains wildcards. +fn first_expr_from_projection(projection: &[SelectItem]) -> Option { + for item in projection { + match item { + SelectItem::UnnamedExpr(expr) => return Some(expr.clone()), + SelectItem::ExprWithAlias { alias, .. } => { + return Some(sql_ast::Expr::Identifier(alias.clone())); + } + SelectItem::Wildcard(_) | SelectItem::QualifiedWildcard(_, _) => continue, + } + } + None +} + #[cfg(test)] mod test { use insta::assert_snapshot; diff --git a/prqlc/prqlc/tests/integration/sql.rs b/prqlc/prqlc/tests/integration/sql.rs index e88045f4e173..1cf7ce2b5738 100644 --- a/prqlc/prqlc/tests/integration/sql.rs +++ b/prqlc/prqlc/tests/integration/sql.rs @@ -2365,6 +2365,29 @@ fn test_take_mssql() { "); } +#[test] +fn test_mssql_distinct_fetch() { + // Issue #5628: MSSQL requires ORDER BY items to appear in SELECT list when DISTINCT is used. + // Using (SELECT NULL) for ORDER BY with DISTINCT is invalid in MSSQL. + assert_snapshot!((compile(r#" + prql target:sql.mssql + + from t + take 100 + group {this.`District`} (take 1) + select {this.`District`} + "#).unwrap()), @r###" + SELECT + DISTINCT "District" + FROM + t + ORDER BY + "District" OFFSET 0 ROWS + FETCH FIRST + 100 ROWS ONLY + "###); +} + #[test] fn test_distinct_01() { // window functions cannot materialize into where statement: CTE is needed