implement pretty-printing with {:#} (#1847)

This commit is contained in:
Ophir LOJKINE 2025-05-13 15:25:07 +02:00 committed by GitHub
parent 052ad4a759
commit 6120bb59cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 591 additions and 160 deletions

View file

@ -40,8 +40,14 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "visitor")]
use sqlparser_derive::{Visit, VisitMut};
use crate::keywords::Keyword;
use crate::tokenizer::{Span, Token};
use crate::{
display_utils::SpaceOrNewline,
tokenizer::{Span, Token},
};
use crate::{
display_utils::{Indent, NewLine},
keywords::Keyword,
};
pub use self::data_type::{
ArrayElemTypeDef, BinaryLength, CharLengthUnits, CharacterLength, DataType, EnumMember,
@ -134,9 +140,9 @@ where
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut delim = "";
for t in self.slice {
write!(f, "{delim}")?;
f.write_str(delim)?;
delim = self.sep;
write!(f, "{t}")?;
t.fmt(f)?;
}
Ok(())
}
@ -628,7 +634,12 @@ pub struct CaseWhen {
impl fmt::Display for CaseWhen {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "WHEN {} THEN {}", self.condition, self.result)
f.write_str("WHEN ")?;
self.condition.fmt(f)?;
f.write_str(" THEN")?;
SpaceOrNewline.fmt(f)?;
Indent(&self.result).fmt(f)?;
Ok(())
}
}
@ -1662,23 +1673,29 @@ impl fmt::Display for Expr {
write!(f, "{data_type}")?;
write!(f, " {value}")
}
Expr::Function(fun) => write!(f, "{fun}"),
Expr::Function(fun) => fun.fmt(f),
Expr::Case {
operand,
conditions,
else_result,
} => {
write!(f, "CASE")?;
f.write_str("CASE")?;
if let Some(operand) = operand {
write!(f, " {operand}")?;
f.write_str(" ")?;
operand.fmt(f)?;
}
for when in conditions {
write!(f, " {when}")?;
SpaceOrNewline.fmt(f)?;
Indent(when).fmt(f)?;
}
if let Some(else_result) = else_result {
write!(f, " ELSE {else_result}")?;
SpaceOrNewline.fmt(f)?;
Indent("ELSE").fmt(f)?;
SpaceOrNewline.fmt(f)?;
Indent(Indent(else_result)).fmt(f)?;
}
write!(f, " END")
SpaceOrNewline.fmt(f)?;
f.write_str("END")
}
Expr::Exists { subquery, negated } => write!(
f,
@ -1867,8 +1884,14 @@ pub enum WindowType {
impl Display for WindowType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WindowType::WindowSpec(spec) => write!(f, "({})", spec),
WindowType::NamedWindow(name) => write!(f, "{}", name),
WindowType::WindowSpec(spec) => {
f.write_str("(")?;
NewLine.fmt(f)?;
Indent(spec).fmt(f)?;
NewLine.fmt(f)?;
f.write_str(")")
}
WindowType::NamedWindow(name) => name.fmt(f),
}
}
}
@ -1896,14 +1919,19 @@ pub struct WindowSpec {
impl fmt::Display for WindowSpec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut delim = "";
let mut is_first = true;
if let Some(window_name) = &self.window_name {
delim = " ";
if !is_first {
SpaceOrNewline.fmt(f)?;
}
is_first = false;
write!(f, "{window_name}")?;
}
if !self.partition_by.is_empty() {
f.write_str(delim)?;
delim = " ";
if !is_first {
SpaceOrNewline.fmt(f)?;
}
is_first = false;
write!(
f,
"PARTITION BY {}",
@ -1911,12 +1939,16 @@ impl fmt::Display for WindowSpec {
)?;
}
if !self.order_by.is_empty() {
f.write_str(delim)?;
delim = " ";
if !is_first {
SpaceOrNewline.fmt(f)?;
}
is_first = false;
write!(f, "ORDER BY {}", display_comma_separated(&self.order_by))?;
}
if let Some(window_frame) = &self.window_frame {
f.write_str(delim)?;
if !is_first {
SpaceOrNewline.fmt(f)?;
}
if let Some(end_bound) = &window_frame.end_bound {
write!(
f,
@ -4204,6 +4236,28 @@ impl fmt::Display for RaisErrorOption {
}
impl fmt::Display for Statement {
/// Formats a SQL statement with support for pretty printing.
///
/// When using the alternate flag (`{:#}`), the statement will be formatted with proper
/// indentation and line breaks. For example:
///
/// ```
/// # use sqlparser::dialect::GenericDialect;
/// # use sqlparser::parser::Parser;
/// let sql = "SELECT a, b FROM table_1";
/// let ast = Parser::parse_sql(&GenericDialect, sql).unwrap();
///
/// // Regular formatting
/// assert_eq!(format!("{}", ast[0]), "SELECT a, b FROM table_1");
///
/// // Pretty printing
/// assert_eq!(format!("{:#}", ast[0]),
/// r#"SELECT
/// a,
/// b
/// FROM
/// table_1"#);
/// ```
// Clippy thinks this function is too complicated, but it is painful to
// split up without extracting structs for each `Statement` variant.
#[allow(clippy::cognitive_complexity)]
@ -4219,7 +4273,8 @@ impl fmt::Display for Statement {
} => {
write!(f, "FLUSH")?;
if let Some(location) = location {
write!(f, " {location}")?;
f.write_str(" ")?;
location.fmt(f)?;
}
write!(f, " {object_type}")?;
@ -4301,7 +4356,7 @@ impl fmt::Display for Statement {
write!(f, "{statement}")
}
Statement::Query(s) => write!(f, "{s}"),
Statement::Query(s) => s.fmt(f),
Statement::Declare { stmts } => {
write!(f, "DECLARE ")?;
write!(f, "{}", display_separated(stmts, "; "))
@ -7056,7 +7111,8 @@ impl fmt::Display for Function {
}
if let Some(o) = &self.over {
write!(f, " OVER {o}")?;
f.write_str(" OVER ")?;
o.fmt(f)?;
}
if self.uses_odbc_syntax {

View file

@ -27,6 +27,7 @@ use sqlparser_derive::{Visit, VisitMut};
use crate::{
ast::*,
display_utils::{indented_list, SpaceOrNewline},
tokenizer::{Token, TokenWithSpan},
};
@ -70,33 +71,41 @@ pub struct Query {
impl fmt::Display for Query {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(ref with) = self.with {
write!(f, "{with} ")?;
with.fmt(f)?;
SpaceOrNewline.fmt(f)?;
}
write!(f, "{}", self.body)?;
self.body.fmt(f)?;
if let Some(ref order_by) = self.order_by {
write!(f, " {order_by}")?;
f.write_str(" ")?;
order_by.fmt(f)?;
}
if let Some(ref limit_clause) = self.limit_clause {
limit_clause.fmt(f)?;
}
if let Some(ref settings) = self.settings {
write!(f, " SETTINGS {}", display_comma_separated(settings))?;
f.write_str(" SETTINGS ")?;
display_comma_separated(settings).fmt(f)?;
}
if let Some(ref fetch) = self.fetch {
write!(f, " {fetch}")?;
f.write_str(" ")?;
fetch.fmt(f)?;
}
if !self.locks.is_empty() {
write!(f, " {}", display_separated(&self.locks, " "))?;
f.write_str(" ")?;
display_separated(&self.locks, " ").fmt(f)?;
}
if let Some(ref for_clause) = self.for_clause {
write!(f, " {}", for_clause)?;
f.write_str(" ")?;
for_clause.fmt(f)?;
}
if let Some(ref format) = self.format_clause {
write!(f, " {}", format)?;
f.write_str(" ")?;
format.fmt(f)?;
}
for pipe_operator in &self.pipe_operators {
write!(f, " |> {}", pipe_operator)?;
f.write_str(" |> ")?;
pipe_operator.fmt(f)?;
}
Ok(())
}
@ -169,29 +178,39 @@ impl SetExpr {
impl fmt::Display for SetExpr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SetExpr::Select(s) => write!(f, "{s}"),
SetExpr::Query(q) => write!(f, "({q})"),
SetExpr::Values(v) => write!(f, "{v}"),
SetExpr::Insert(v) => write!(f, "{v}"),
SetExpr::Update(v) => write!(f, "{v}"),
SetExpr::Delete(v) => write!(f, "{v}"),
SetExpr::Table(t) => write!(f, "{t}"),
SetExpr::Select(s) => s.fmt(f),
SetExpr::Query(q) => {
f.write_str("(")?;
q.fmt(f)?;
f.write_str(")")
}
SetExpr::Values(v) => v.fmt(f),
SetExpr::Insert(v) => v.fmt(f),
SetExpr::Update(v) => v.fmt(f),
SetExpr::Delete(v) => v.fmt(f),
SetExpr::Table(t) => t.fmt(f),
SetExpr::SetOperation {
left,
right,
op,
set_quantifier,
} => {
write!(f, "{left} {op}")?;
left.fmt(f)?;
SpaceOrNewline.fmt(f)?;
op.fmt(f)?;
match set_quantifier {
SetQuantifier::All
| SetQuantifier::Distinct
| SetQuantifier::ByName
| SetQuantifier::AllByName
| SetQuantifier::DistinctByName => write!(f, " {set_quantifier}")?,
SetQuantifier::None => write!(f, "{set_quantifier}")?,
| SetQuantifier::DistinctByName => {
f.write_str(" ")?;
set_quantifier.fmt(f)?;
}
SetQuantifier::None => {}
}
write!(f, " {right}")?;
SpaceOrNewline.fmt(f)?;
right.fmt(f)?;
Ok(())
}
}
@ -242,7 +261,7 @@ impl fmt::Display for SetQuantifier {
SetQuantifier::ByName => write!(f, "BY NAME"),
SetQuantifier::AllByName => write!(f, "ALL BY NAME"),
SetQuantifier::DistinctByName => write!(f, "DISTINCT BY NAME"),
SetQuantifier::None => write!(f, ""),
SetQuantifier::None => Ok(()),
}
}
}
@ -357,90 +376,122 @@ impl fmt::Display for Select {
}
if let Some(value_table_mode) = self.value_table_mode {
write!(f, " {value_table_mode}")?;
f.write_str(" ")?;
value_table_mode.fmt(f)?;
}
if let Some(ref top) = self.top {
if self.top_before_distinct {
write!(f, " {top}")?;
f.write_str(" ")?;
top.fmt(f)?;
}
}
if let Some(ref distinct) = self.distinct {
write!(f, " {distinct}")?;
f.write_str(" ")?;
distinct.fmt(f)?;
}
if let Some(ref top) = self.top {
if !self.top_before_distinct {
write!(f, " {top}")?;
f.write_str(" ")?;
top.fmt(f)?;
}
}
if !self.projection.is_empty() {
write!(f, " {}", display_comma_separated(&self.projection))?;
indented_list(f, &self.projection)?;
}
if let Some(ref into) = self.into {
write!(f, " {into}")?;
f.write_str(" ")?;
into.fmt(f)?;
}
if self.flavor == SelectFlavor::Standard && !self.from.is_empty() {
write!(f, " FROM {}", display_comma_separated(&self.from))?;
SpaceOrNewline.fmt(f)?;
f.write_str("FROM")?;
indented_list(f, &self.from)?;
}
if !self.lateral_views.is_empty() {
for lv in &self.lateral_views {
write!(f, "{lv}")?;
lv.fmt(f)?;
}
}
if let Some(ref prewhere) = self.prewhere {
write!(f, " PREWHERE {prewhere}")?;
f.write_str(" PREWHERE ")?;
prewhere.fmt(f)?;
}
if let Some(ref selection) = self.selection {
write!(f, " WHERE {selection}")?;
SpaceOrNewline.fmt(f)?;
f.write_str("WHERE")?;
SpaceOrNewline.fmt(f)?;
Indent(selection).fmt(f)?;
}
match &self.group_by {
GroupByExpr::All(_) => write!(f, " {}", self.group_by)?,
GroupByExpr::All(_) => {
SpaceOrNewline.fmt(f)?;
self.group_by.fmt(f)?;
}
GroupByExpr::Expressions(exprs, _) => {
if !exprs.is_empty() {
write!(f, " {}", self.group_by)?
SpaceOrNewline.fmt(f)?;
self.group_by.fmt(f)?;
}
}
}
if !self.cluster_by.is_empty() {
write!(
f,
" CLUSTER BY {}",
display_comma_separated(&self.cluster_by)
)?;
SpaceOrNewline.fmt(f)?;
f.write_str("CLUSTER BY")?;
SpaceOrNewline.fmt(f)?;
Indent(display_comma_separated(&self.cluster_by)).fmt(f)?;
}
if !self.distribute_by.is_empty() {
write!(
f,
" DISTRIBUTE BY {}",
display_comma_separated(&self.distribute_by)
)?;
SpaceOrNewline.fmt(f)?;
f.write_str("DISTRIBUTE BY")?;
SpaceOrNewline.fmt(f)?;
display_comma_separated(&self.distribute_by).fmt(f)?;
}
if !self.sort_by.is_empty() {
write!(f, " SORT BY {}", display_comma_separated(&self.sort_by))?;
SpaceOrNewline.fmt(f)?;
f.write_str("SORT BY")?;
SpaceOrNewline.fmt(f)?;
Indent(display_comma_separated(&self.sort_by)).fmt(f)?;
}
if let Some(ref having) = self.having {
write!(f, " HAVING {having}")?;
SpaceOrNewline.fmt(f)?;
f.write_str("HAVING")?;
SpaceOrNewline.fmt(f)?;
Indent(having).fmt(f)?;
}
if self.window_before_qualify {
if !self.named_window.is_empty() {
write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?;
SpaceOrNewline.fmt(f)?;
f.write_str("WINDOW")?;
SpaceOrNewline.fmt(f)?;
display_comma_separated(&self.named_window).fmt(f)?;
}
if let Some(ref qualify) = self.qualify {
write!(f, " QUALIFY {qualify}")?;
SpaceOrNewline.fmt(f)?;
f.write_str("QUALIFY")?;
SpaceOrNewline.fmt(f)?;
qualify.fmt(f)?;
}
} else {
if let Some(ref qualify) = self.qualify {
write!(f, " QUALIFY {qualify}")?;
SpaceOrNewline.fmt(f)?;
f.write_str("QUALIFY")?;
SpaceOrNewline.fmt(f)?;
qualify.fmt(f)?;
}
if !self.named_window.is_empty() {
write!(f, " WINDOW {}", display_comma_separated(&self.named_window))?;
SpaceOrNewline.fmt(f)?;
f.write_str("WINDOW")?;
SpaceOrNewline.fmt(f)?;
display_comma_separated(&self.named_window).fmt(f)?;
}
}
if let Some(ref connect_by) = self.connect_by {
write!(f, " {connect_by}")?;
SpaceOrNewline.fmt(f)?;
connect_by.fmt(f)?;
}
Ok(())
}
@ -546,12 +597,12 @@ pub struct With {
impl fmt::Display for With {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"WITH {}{}",
if self.recursive { "RECURSIVE " } else { "" },
display_comma_separated(&self.cte_tables)
)
f.write_str("WITH ")?;
if self.recursive {
f.write_str("RECURSIVE ")?;
}
display_comma_separated(&self.cte_tables).fmt(f)?;
Ok(())
}
}
@ -598,8 +649,24 @@ pub struct Cte {
impl fmt::Display for Cte {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.materialized.as_ref() {
None => write!(f, "{} AS ({})", self.alias, self.query)?,
Some(materialized) => write!(f, "{} AS {materialized} ({})", self.alias, self.query)?,
None => {
self.alias.fmt(f)?;
f.write_str(" AS (")?;
NewLine.fmt(f)?;
Indent(&self.query).fmt(f)?;
NewLine.fmt(f)?;
f.write_str(")")?;
}
Some(materialized) => {
self.alias.fmt(f)?;
f.write_str(" AS ")?;
materialized.fmt(f)?;
f.write_str(" (")?;
NewLine.fmt(f)?;
Indent(&self.query).fmt(f)?;
NewLine.fmt(f)?;
f.write_str(")")?;
}
};
if let Some(ref fr) = self.from {
write!(f, " FROM {fr}")?;
@ -912,18 +979,21 @@ impl fmt::Display for ReplaceSelectElement {
impl fmt::Display for SelectItem {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use core::fmt::Write;
match &self {
SelectItem::UnnamedExpr(expr) => write!(f, "{expr}"),
SelectItem::ExprWithAlias { expr, alias } => write!(f, "{expr} AS {alias}"),
SelectItem::UnnamedExpr(expr) => expr.fmt(f),
SelectItem::ExprWithAlias { expr, alias } => {
expr.fmt(f)?;
f.write_str(" AS ")?;
alias.fmt(f)
}
SelectItem::QualifiedWildcard(kind, additional_options) => {
write!(f, "{kind}")?;
write!(f, "{additional_options}")?;
Ok(())
kind.fmt(f)?;
additional_options.fmt(f)
}
SelectItem::Wildcard(additional_options) => {
write!(f, "*")?;
write!(f, "{additional_options}")?;
Ok(())
f.write_char('*')?;
additional_options.fmt(f)
}
}
}
@ -939,9 +1009,10 @@ pub struct TableWithJoins {
impl fmt::Display for TableWithJoins {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.relation)?;
self.relation.fmt(f)?;
for join in &self.joins {
write!(f, "{join}")?;
SpaceOrNewline.fmt(f)?;
join.fmt(f)?;
}
Ok(())
}
@ -1334,7 +1405,6 @@ pub enum TableFactor {
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum TableSampleKind {
/// Table sample located before the table alias option
BeforeTableAlias(Box<TableSample>),
@ -1769,9 +1839,9 @@ impl fmt::Display for TableFactor {
sample,
index_hints,
} => {
write!(f, "{name}")?;
name.fmt(f)?;
if let Some(json_path) = json_path {
write!(f, "{json_path}")?;
json_path.fmt(f)?;
}
if !partitions.is_empty() {
write!(f, "PARTITION ({})", display_comma_separated(partitions))?;
@ -1818,7 +1888,11 @@ impl fmt::Display for TableFactor {
if *lateral {
write!(f, "LATERAL ")?;
}
write!(f, "({subquery})")?;
f.write_str("(")?;
NewLine.fmt(f)?;
Indent(subquery).fmt(f)?;
NewLine.fmt(f)?;
f.write_str(")")?;
if let Some(alias) = alias {
write!(f, " AS {alias}")?;
}
@ -2132,116 +2206,104 @@ impl fmt::Display for Join {
Suffix(constraint)
}
if self.global {
write!(f, " GLOBAL")?;
write!(f, "GLOBAL ")?;
}
match &self.join_operator {
JoinOperator::Join(constraint) => write!(
f,
" {}JOIN {}{}",
JoinOperator::Join(constraint) => f.write_fmt(format_args!(
"{}JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::Inner(constraint) => write!(
f,
" {}INNER JOIN {}{}",
)),
JoinOperator::Inner(constraint) => f.write_fmt(format_args!(
"{}INNER JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::Left(constraint) => write!(
f,
" {}LEFT JOIN {}{}",
)),
JoinOperator::Left(constraint) => f.write_fmt(format_args!(
"{}LEFT JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::LeftOuter(constraint) => write!(
f,
" {}LEFT OUTER JOIN {}{}",
)),
JoinOperator::LeftOuter(constraint) => f.write_fmt(format_args!(
"{}LEFT OUTER JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::Right(constraint) => write!(
f,
" {}RIGHT JOIN {}{}",
)),
JoinOperator::Right(constraint) => f.write_fmt(format_args!(
"{}RIGHT JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::RightOuter(constraint) => write!(
f,
" {}RIGHT OUTER JOIN {}{}",
)),
JoinOperator::RightOuter(constraint) => f.write_fmt(format_args!(
"{}RIGHT OUTER JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::FullOuter(constraint) => write!(
f,
" {}FULL JOIN {}{}",
)),
JoinOperator::FullOuter(constraint) => f.write_fmt(format_args!(
"{}FULL JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::CrossJoin => write!(f, " CROSS JOIN {}", self.relation),
JoinOperator::Semi(constraint) => write!(
f,
" {}SEMI JOIN {}{}",
)),
JoinOperator::CrossJoin => f.write_fmt(format_args!("CROSS JOIN {}", self.relation)),
JoinOperator::Semi(constraint) => f.write_fmt(format_args!(
"{}SEMI JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::LeftSemi(constraint) => write!(
f,
" {}LEFT SEMI JOIN {}{}",
)),
JoinOperator::LeftSemi(constraint) => f.write_fmt(format_args!(
"{}LEFT SEMI JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::RightSemi(constraint) => write!(
f,
" {}RIGHT SEMI JOIN {}{}",
)),
JoinOperator::RightSemi(constraint) => f.write_fmt(format_args!(
"{}RIGHT SEMI JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::Anti(constraint) => write!(
f,
" {}ANTI JOIN {}{}",
)),
JoinOperator::Anti(constraint) => f.write_fmt(format_args!(
"{}ANTI JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::LeftAnti(constraint) => write!(
f,
" {}LEFT ANTI JOIN {}{}",
)),
JoinOperator::LeftAnti(constraint) => f.write_fmt(format_args!(
"{}LEFT ANTI JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::RightAnti(constraint) => write!(
f,
" {}RIGHT ANTI JOIN {}{}",
)),
JoinOperator::RightAnti(constraint) => f.write_fmt(format_args!(
"{}RIGHT ANTI JOIN {}{}",
prefix(constraint),
self.relation,
suffix(constraint)
),
JoinOperator::CrossApply => write!(f, " CROSS APPLY {}", self.relation),
JoinOperator::OuterApply => write!(f, " OUTER APPLY {}", self.relation),
)),
JoinOperator::CrossApply => f.write_fmt(format_args!("CROSS APPLY {}", self.relation)),
JoinOperator::OuterApply => f.write_fmt(format_args!("OUTER APPLY {}", self.relation)),
JoinOperator::AsOf {
match_condition,
constraint,
} => write!(
f,
" ASOF JOIN {} MATCH_CONDITION ({match_condition}){}",
} => f.write_fmt(format_args!(
"ASOF JOIN {} MATCH_CONDITION ({match_condition}){}",
self.relation,
suffix(constraint)
),
JoinOperator::StraightJoin(constraint) => {
write!(f, " STRAIGHT_JOIN {}{}", self.relation, suffix(constraint))
}
)),
JoinOperator::StraightJoin(constraint) => f.write_fmt(format_args!(
"STRAIGHT_JOIN {}{}",
self.relation,
suffix(constraint)
)),
}
}
}
@ -2914,8 +2976,9 @@ impl fmt::Display for GroupByExpr {
Ok(())
}
GroupByExpr::Expressions(col_names, modifiers) => {
let col_names = display_comma_separated(col_names);
write!(f, "GROUP BY {col_names}")?;
f.write_str("GROUP BY")?;
SpaceOrNewline.fmt(f)?;
Indent(display_comma_separated(col_names)).fmt(f)?;
if !modifiers.is_empty() {
write!(f, " {}", display_separated(modifiers, " "))?;
}

133
src/display_utils.rs Normal file
View file

@ -0,0 +1,133 @@
//! Utilities for formatting SQL AST nodes with pretty printing support.
//!
//! The module provides formatters that implement the `Display` trait with support
//! for both regular (`{}`) and pretty (`{:#}`) formatting modes. Pretty printing
//! adds proper indentation and line breaks to make SQL statements more readable.
use core::fmt::{self, Display, Write};
/// A wrapper around a value that adds an indent to the value when displayed with {:#}.
pub(crate) struct Indent<T>(pub T);
const INDENT: &str = " ";
impl<T> Display for Indent<T>
where
T: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.write_str(INDENT)?;
write!(Indent(f), "{:#}", self.0)
} else {
self.0.fmt(f)
}
}
}
/// Adds an indent to the inner writer
impl<T> Write for Indent<T>
where
T: Write,
{
fn write_str(&mut self, s: &str) -> fmt::Result {
let mut first = true;
for line in s.split('\n') {
if !first {
write!(self.0, "\n{INDENT}")?;
}
self.0.write_str(line)?;
first = false;
}
Ok(())
}
}
/// A value that inserts a newline when displayed with {:#}, but not when displayed with {}.
pub(crate) struct NewLine;
impl Display for NewLine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.write_char('\n')
} else {
Ok(())
}
}
}
/// A value that inserts a space when displayed with {}, but a newline when displayed with {:#}.
pub(crate) struct SpaceOrNewline;
impl Display for SpaceOrNewline {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.write_char('\n')
} else {
f.write_char(' ')
}
}
}
/// A value that displays a comma-separated list of values.
/// When pretty-printed (using {:#}), it displays each value on a new line.
pub struct DisplayCommaSeparated<'a, T: fmt::Display>(&'a [T]);
impl<T: fmt::Display> fmt::Display for DisplayCommaSeparated<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut first = true;
for t in self.0 {
if !first {
f.write_char(',')?;
SpaceOrNewline.fmt(f)?;
}
first = false;
t.fmt(f)?;
}
Ok(())
}
}
/// Displays a whitespace, followed by a comma-separated list that is indented when pretty-printed.
pub(crate) fn indented_list<T: fmt::Display>(f: &mut fmt::Formatter, slice: &[T]) -> fmt::Result {
SpaceOrNewline.fmt(f)?;
Indent(DisplayCommaSeparated(slice)).fmt(f)
}
#[cfg(test)]
mod tests {
use super::*;
struct DisplayCharByChar<T: Display>(T);
impl<T: Display> Display for DisplayCharByChar<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for c in self.0.to_string().chars() {
write!(f, "{}", c)?;
}
Ok(())
}
}
#[test]
fn test_indent() {
let original = "line 1\nline 2";
let indent = Indent(original);
assert_eq!(
indent.to_string(),
original,
"Only the alternate form should be indented"
);
let expected = " line 1\n line 2";
assert_eq!(format!("{:#}", indent), expected);
let display_char_by_char = DisplayCharByChar(original);
assert_eq!(format!("{:#}", Indent(display_char_by_char)), expected);
}
#[test]
fn test_space_or_newline() {
let space_or_newline = SpaceOrNewline;
assert_eq!(format!("{}", space_or_newline), " ");
assert_eq!(format!("{:#}", space_or_newline), "\n");
}
}

View file

@ -64,6 +64,27 @@
//! // The original SQL text can be generated from the AST
//! assert_eq!(ast[0].to_string(), sql);
//! ```
//!
//! # Pretty Printing
//!
//! SQL statements can be pretty-printed with proper indentation and line breaks using the alternate flag (`{:#}`):
//!
//! ```
//! # use sqlparser::dialect::GenericDialect;
//! # use sqlparser::parser::Parser;
//! let sql = "SELECT a, b FROM table_1";
//! let ast = Parser::parse_sql(&GenericDialect, sql).unwrap();
//!
//! // Pretty print with indentation and line breaks
//! let pretty_sql = format!("{:#}", ast[0]);
//! assert_eq!(pretty_sql, r#"
//! SELECT
//! a,
//! b
//! FROM
//! table_1
//! "#.trim());
//! ```
//! [sqlparser crates.io page]: https://crates.io/crates/sqlparser
//! [`Parser::parse_sql`]: crate::parser::Parser::parse_sql
//! [`Parser::new`]: crate::parser::Parser::new
@ -142,6 +163,7 @@ extern crate pretty_assertions;
pub mod ast;
#[macro_use]
pub mod dialect;
mod display_utils;
pub mod keywords;
pub mod parser;
pub mod tokenizer;

157
tests/pretty_print.rs Normal file
View file

@ -0,0 +1,157 @@
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser;
fn prettify(sql: &str) -> String {
let ast = Parser::parse_sql(&GenericDialect {}, sql).unwrap();
format!("{:#}", ast[0])
}
#[test]
fn test_pretty_print_select() {
assert_eq!(
prettify("SELECT a, b, c FROM my_table WHERE x = 1 AND y = 2"),
r#"
SELECT
a,
b,
c
FROM
my_table
WHERE
x = 1 AND y = 2
"#
.trim()
);
}
#[test]
fn test_pretty_print_join() {
assert_eq!(
prettify("SELECT a FROM table1 JOIN table2 ON table1.id = table2.id"),
r#"
SELECT
a
FROM
table1
JOIN table2 ON table1.id = table2.id
"#
.trim()
);
}
#[test]
fn test_pretty_print_subquery() {
assert_eq!(
prettify("SELECT * FROM (SELECT a, b FROM my_table) AS subquery"),
r#"
SELECT
*
FROM
(
SELECT
a,
b
FROM
my_table
) AS subquery
"#
.trim()
);
}
#[test]
fn test_pretty_print_union() {
assert_eq!(
prettify("SELECT a FROM table1 UNION SELECT b FROM table2"),
r#"
SELECT
a
FROM
table1
UNION
SELECT
b
FROM
table2
"#
.trim()
);
}
#[test]
fn test_pretty_print_group_by() {
assert_eq!(
prettify("SELECT a, COUNT(*) FROM my_table GROUP BY a HAVING COUNT(*) > 1"),
r#"
SELECT
a,
COUNT(*)
FROM
my_table
GROUP BY
a
HAVING
COUNT(*) > 1
"#
.trim()
);
}
#[test]
fn test_pretty_print_cte() {
assert_eq!(
prettify("WITH cte AS (SELECT a, b FROM my_table) SELECT * FROM cte"),
r#"
WITH cte AS (
SELECT
a,
b
FROM
my_table
)
SELECT
*
FROM
cte
"#
.trim()
);
}
#[test]
fn test_pretty_print_case_when() {
assert_eq!(
prettify("SELECT CASE WHEN x > 0 THEN 'positive' WHEN x < 0 THEN 'negative' ELSE 'zero' END FROM my_table"),
r#"
SELECT
CASE
WHEN x > 0 THEN
'positive'
WHEN x < 0 THEN
'negative'
ELSE
'zero'
END
FROM
my_table
"#.trim()
);
}
#[test]
fn test_pretty_print_window_function() {
assert_eq!(
prettify("SELECT id, value, ROW_NUMBER() OVER (PARTITION BY category ORDER BY value DESC) as rank FROM my_table"),
r#"
SELECT
id,
value,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY value DESC
) AS rank
FROM
my_table
"#.trim()
);
}