use std::collections::HashSet;
use super::comments::CommentAttachments;
use super::doc::{self, Doc};
use crate::ast::Span;
use crate::ast::render::{RenderConfig, RenderCtx, RenderExt};
use crate::ast::{
Cte, CteBody, DerivedSpelling, Expr, GroupByItem, NoExt, OrderByExpr, Query, Resolver, Select,
SelectDistinct, SelectItem, SetExpr, SetOperator, SetQuantifier, SourceStore, Spanned,
Statement, TableFactor, TableWithJoins, With,
};
use crate::dialect::BuiltinDialect;
use crate::parser::Parsed;
use crate::tokenizer::{Punctuation, TokenKind, TriviaKind, TriviaRange};
pub(super) struct PrettyRenderer<'a> {
resolver: &'a dyn Resolver,
source: &'a str,
config: RenderConfig,
comments: &'a CommentAttachments,
indent: usize,
dialect: BuiltinDialect,
emitted: HashSet<u32>,
}
impl<'a> PrettyRenderer<'a> {
pub(super) fn new<S: SourceStore>(
parsed: &'a Parsed<S, NoExt>,
comments: &'a CommentAttachments,
indent: usize,
dialect: BuiltinDialect,
) -> PrettyRenderer<'a> {
PrettyRenderer {
resolver: parsed.resolver(),
source: parsed.source(),
config: RenderConfig::default(),
comments,
indent,
dialect,
emitted: HashSet::new(),
}
}
pub(super) fn document(&mut self, statements: &[Statement<NoExt>]) -> Doc {
let stmts: Vec<Doc> = statements.iter().map(|s| self.statement(s)).collect();
let sep = doc::concat([Doc::text(";"), doc::hardline(), doc::hardline()]);
let body = doc::join(sep, stmts);
let leftover = self.leftover_comments();
doc::concat([body, leftover])
}
fn frag<T: RenderExt>(&self, node: &T) -> String {
let ctx = RenderCtx::new(self.resolver, self.source, &self.config);
format!("{}", node.displayed(&ctx))
}
fn text_of(&self, range: TriviaRange) -> String {
let span = range.span();
self.source[span.start() as usize..span.end() as usize].to_owned()
}
fn leading(&mut self, span: Span) -> Doc {
let ranges: Vec<TriviaRange> = self.comments.leading_for(span).to_vec();
let mut parts = Vec::new();
for range in ranges {
self.emitted.insert(range.span().start());
parts.push(doc::concat([
Doc::text(self.text_of(range)),
doc::hardline(),
]));
}
doc::concat(parts)
}
fn trailing(&mut self, span: Span) -> (Doc, bool) {
let ranges: Vec<TriviaRange> = self.comments.trailing_for(span).to_vec();
let mut parts = Vec::new();
let mut has_line = false;
for range in ranges {
self.emitted.insert(range.span().start());
has_line |= matches!(range.kind(), TriviaKind::LineComment);
parts.push(Doc::text(format!(" {}", self.text_of(range))));
}
(doc::concat(parts), has_line)
}
fn interior(&mut self, span: Span) -> Doc {
let ranges: Vec<TriviaRange> = self
.comments
.all_comments()
.iter()
.filter(|r| {
let s = r.span().start();
!self.emitted.contains(&s) && s >= span.start() && s < span.end()
})
.copied()
.collect();
let mut parts = Vec::new();
for range in ranges {
self.emitted.insert(range.span().start());
let piece = Doc::text(format!(" {}", self.text_of(range)));
if matches!(range.kind(), TriviaKind::LineComment) {
parts.push(doc::concat([piece, doc::hardline()]));
} else {
parts.push(piece);
}
}
doc::concat(parts)
}
fn separator_comma(&self, from: u32, to: u32) -> Option<u32> {
let bytes = self.source.as_bytes();
let end = (to as usize).min(bytes.len());
let mut i = from as usize;
while i < end {
if let Some(run) = self.comments.all_comments().iter().find(|c| {
let s = c.span();
(s.start() as usize) <= i && i < (s.end() as usize)
}) {
i = run.span().end() as usize;
continue;
}
if bytes[i] == b',' {
return Some(i as u32);
}
i += 1;
}
None
}
fn item_comments(&mut self, span: Span, next_start: Option<u32>) -> (Doc, Doc, bool) {
let mut ranges: Vec<TriviaRange> = self
.comments
.all_comments()
.iter()
.filter(|r| {
let s = r.span().start();
!self.emitted.contains(&s) && s >= span.start() && s < span.end()
})
.copied()
.collect();
ranges.extend(
self.comments
.trailing_for(span)
.iter()
.filter(|r| !self.emitted.contains(&r.span().start()))
.copied(),
);
if ranges.is_empty() {
return (doc::nil(), doc::nil(), false);
}
ranges.sort_by_key(|r| r.span().start());
let comma_off = next_start.and_then(|ns| self.separator_comma(span.end(), ns));
let mut pre = Vec::new();
let mut post = Vec::new();
let mut has_line = false;
for range in ranges {
self.emitted.insert(range.span().start());
let is_line = matches!(range.kind(), TriviaKind::LineComment);
has_line |= is_line;
let before_comma = comma_off.is_some_and(|c| range.span().start() < c);
let piece = Doc::text(format!(" {}", self.text_of(range)));
if before_comma && !is_line {
pre.push(piece);
} else {
post.push(piece);
}
}
(doc::concat(pre), doc::concat(post), has_line)
}
fn leftover_comments(&mut self) -> Doc {
let mut ranges: Vec<TriviaRange> = self
.comments
.all_comments()
.iter()
.filter(|r| !self.emitted.contains(&r.span().start()))
.copied()
.collect();
if ranges.is_empty() {
return doc::nil();
}
ranges.sort_by_key(|r| r.span().start());
let mut parts = vec![doc::hardline()];
for range in ranges {
self.emitted.insert(range.span().start());
parts.push(doc::concat([
doc::hardline(),
Doc::text(self.text_of(range)),
]));
}
doc::concat(parts)
}
fn statement(&mut self, stmt: &Statement<NoExt>) -> Doc {
let span = stmt.span();
let lead = self.leading(span);
let body = match stmt {
Statement::Query { query, .. } => self.query(query),
other => doc::concat([Doc::text(self.frag(other)), self.interior(span)]),
};
let (trail, _) = self.trailing(span);
doc::concat([lead, body, trail])
}
fn query(&mut self, query: &Query<NoExt>) -> Doc {
match self.try_query(query) {
Some(doc) => doc,
None => doc::concat([Doc::text(self.frag(query)), self.interior(query.span())]),
}
}
fn try_query(&mut self, query: &Query<NoExt>) -> Option<Doc> {
if query.order_by_all.is_some()
|| query.limit_by.is_some()
|| !query.settings.is_empty()
|| query.format.is_some()
|| !query.locking.is_empty()
|| !query.pipe_operators.is_empty()
|| query.for_clause.is_some()
{
return None;
}
let mut clauses: Vec<Doc> = Vec::new();
if let Some(with) = &query.with {
clauses.push(self.with_clause(with)?);
}
clauses.push(self.set_expr(&query.body)?);
if !query.order_by.is_empty() {
clauses.push(self.order_by_clause(&query.order_by));
}
if let Some(limit) = &query.limit {
clauses.push(Doc::text(self.frag(limit)));
}
Some(doc::join(doc::hardline(), clauses))
}
fn with_clause(&mut self, with: &With<NoExt>) -> Option<Doc> {
let keyword = if with.recursive {
"WITH RECURSIVE "
} else {
"WITH "
};
let mut cte_docs: Vec<Doc> = Vec::new();
for cte in &with.ctes {
cte_docs.push(self.cte(cte)?);
}
let joined = doc::join(doc::concat([Doc::text(","), doc::hardline()]), cte_docs);
Some(doc::concat([Doc::text(keyword), joined]))
}
fn cte(&mut self, cte: &Cte<NoExt>) -> Option<Doc> {
if cte.search.is_some() || cte.cycle.is_some() {
return None;
}
let CteBody::Query { query, .. } = &cte.body else {
return None;
};
let inner = self.try_query(query)?;
let mut head = self.frag(&cte.name);
if !cte.columns.is_empty() {
let cols: Vec<String> = cte.columns.iter().map(|c| self.frag(c)).collect();
head.push_str(&format!(" ({})", cols.join(", ")));
}
head.push_str(" AS ");
match cte.materialized {
Some(true) => head.push_str("MATERIALIZED "),
Some(false) => head.push_str("NOT MATERIALIZED "),
None => {}
}
head.push('(');
Some(doc::concat([
Doc::text(head),
doc::nest(self.indent, doc::concat([doc::hardline(), inner])),
doc::hardline(),
Doc::text(")"),
]))
}
fn set_expr(&mut self, body: &SetExpr<NoExt>) -> Option<Doc> {
match body {
SetExpr::Select { select, .. } => self.select(select),
SetExpr::Query { query, .. } => self.try_query(query),
SetExpr::SetOperation {
op,
all,
by_name,
left,
right,
..
} => {
let left_doc = self.set_expr(left)?;
let right_doc = self.set_expr(right)?;
let word = set_op_word(op, *all, *by_name);
Some(doc::join(
doc::hardline(),
[left_doc, Doc::text(word), right_doc],
))
}
_ => None,
}
}
fn select(&mut self, select: &Select<NoExt>) -> Option<Doc> {
if !is_simple_select(select) {
return None;
}
let mut clauses: Vec<Doc> = Vec::new();
let mut select_kw = String::from("SELECT");
if let Some(distinct) = &select.distinct {
select_kw.push(' ');
select_kw.push_str(&self.distinct_text(distinct));
}
let proj_items: Vec<ListItem> = select
.projection
.iter()
.map(|item| ListItem {
span: item.span(),
doc: self.projection_item_doc(item),
})
.collect();
clauses.push(self.list_clause(select_kw, &proj_items));
if !select.from.is_empty() {
let from_items: Vec<ListItem> = select
.from
.iter()
.map(|twj| ListItem {
span: twj.span(),
doc: self.table_item_doc(twj),
})
.collect();
clauses.push(self.list_clause(String::from("FROM"), &from_items));
}
if let Some(selection) = &select.selection {
clauses.push(self.expr_clause("WHERE", selection));
}
if !select.group_by.is_empty() {
let group_items: Vec<ListItem> = select
.group_by
.iter()
.map(|item| ListItem {
span: group_by_item_span(item),
doc: Doc::text(self.frag(item)),
})
.collect();
clauses.push(self.list_clause(String::from("GROUP BY"), &group_items));
}
if let Some(having) = &select.having {
clauses.push(self.expr_clause("HAVING", having));
}
Some(doc::join(doc::hardline(), clauses))
}
fn distinct_text(&self, distinct: &SelectDistinct<NoExt>) -> String {
match distinct {
SelectDistinct::Quantifier { quantifier, .. } => match quantifier {
SetQuantifier::All => "ALL".to_owned(),
SetQuantifier::Distinct => "DISTINCT".to_owned(),
},
SelectDistinct::On { exprs, .. } => {
let keys: Vec<String> = exprs.iter().map(|e| self.frag(e)).collect();
format!("DISTINCT ON ({})", keys.join(", "))
}
}
}
fn order_by_clause(&mut self, items: &[OrderByExpr<NoExt>]) -> Doc {
let list: Vec<ListItem> = items
.iter()
.map(|item| ListItem {
span: item.span(),
doc: Doc::text(self.frag(item)),
})
.collect();
self.list_clause(String::from("ORDER BY"), &list)
}
fn expr_clause(&mut self, keyword: &'static str, expr: &Expr<NoExt>) -> Doc {
let span = expr.span();
let lead = self.leading(span);
let body = self.expr_body_doc(expr);
let interior = self.interior(span);
let (trail, _) = self.trailing(span);
doc::concat([
lead,
Doc::text(keyword),
Doc::text(" "),
body,
interior,
trail,
])
}
fn projection_item_doc(&mut self, item: &SelectItem<NoExt>) -> Doc {
let whole = self.frag(item);
let mut subs: Vec<&Query<NoExt>> = Vec::new();
if let SelectItem::Expr { expr, .. } = item {
collect_expr_subqueries(expr, &mut subs);
}
self.relayout_fragment(whole, &subs)
}
fn table_item_doc(&mut self, twj: &TableWithJoins<NoExt>) -> Doc {
let whole = self.frag(twj);
let mut subs: Vec<&Query<NoExt>> = Vec::new();
collect_factor_subqueries(&twj.relation, &mut subs);
for join in &twj.joins {
collect_factor_subqueries(&join.relation, &mut subs);
}
self.relayout_fragment(whole, &subs)
}
fn expr_body_doc(&mut self, expr: &Expr<NoExt>) -> Doc {
let whole = self.frag(expr);
let mut subs: Vec<&Query<NoExt>> = Vec::new();
collect_expr_subqueries(expr, &mut subs);
self.relayout_fragment(whole, &subs)
}
fn relayout_fragment(&mut self, whole: String, subs: &[&Query<NoExt>]) -> Doc {
if subs.is_empty() {
return Doc::text(whole);
}
let Ok(tokens) = crate::tokenize_with_builtin(&whole, self.dialect) else {
return Doc::text(whole);
};
let lparen_offsets: HashSet<u32> = tokens
.iter()
.filter(|t| matches!(t.kind, TokenKind::Punctuation(Punctuation::LParen)))
.map(|t| t.span.start())
.collect();
let mut parts: Vec<Doc> = Vec::new();
let mut cursor = 0usize;
for &query in subs {
let needle = format!("({})", self.frag(query));
let mut search = cursor;
let found = loop {
let Some(rel) = whole[search..].find(&needle) else {
break None;
};
let at = search + rel;
if lparen_offsets.contains(&(at as u32)) {
break Some(at);
}
search = at + 1;
};
let Some(at) = found else {
continue;
};
let Some(sub_doc) = self.structured_subquery(query) else {
continue;
};
if at > cursor {
parts.push(Doc::text(whole[cursor..at].to_owned()));
}
parts.push(sub_doc);
cursor = at + needle.len();
}
if cursor < whole.len() {
parts.push(Doc::text(whole[cursor..].to_owned()));
}
if parts.is_empty() {
return Doc::text(whole);
}
doc::concat(parts)
}
fn structured_subquery(&mut self, query: &Query<NoExt>) -> Option<Doc> {
let snapshot = self.emitted.clone();
match self.try_query(query) {
Some(inner) if doc_has_hardline(&inner) => Some(doc::concat([
Doc::text("("),
doc::nest(self.indent, doc::concat([doc::hardline(), inner])),
doc::hardline(),
Doc::text(")"),
])),
_ => {
self.emitted = snapshot;
None
}
}
}
fn list_clause(&mut self, keyword: String, items: &[ListItem]) -> Doc {
let keyword_lead = items
.first()
.map(|it| self.leading(it.span))
.unwrap_or_else(doc::nil);
let mut force_break = !matches!(keyword_lead, Doc::Nil);
let mut pieces: Vec<Doc> = Vec::with_capacity(items.len());
let last = items.len().saturating_sub(1);
for (i, item) in items.iter().enumerate() {
let lead = if i == 0 {
doc::nil()
} else {
self.leading(item.span)
};
if !matches!(lead, Doc::Nil) {
force_break = true;
}
let comma = if i < last { "," } else { "" };
let next_start = items.get(i + 1).map(|it| it.span.start());
let (pre, post, has_line) = self.item_comments(item.span, next_start);
force_break |= has_line;
pieces.push(doc::concat([
lead,
item.doc.clone(),
pre,
Doc::text(comma),
post,
]));
}
let sep = if force_break {
doc::hardline()
} else {
doc::line()
};
let body = doc::nest(
self.indent,
doc::concat([doc::line(), doc::join(sep, pieces)]),
);
let clause = doc::concat([Doc::text(keyword), body]);
doc::concat([
keyword_lead,
if force_break {
clause
} else {
doc::group(clause)
},
])
}
}
struct ListItem {
span: Span,
doc: Doc,
}
fn collect_expr_subqueries<'e>(expr: &'e Expr<NoExt>, out: &mut Vec<&'e Query<NoExt>>) {
match expr {
Expr::Subquery { query, .. } | Expr::Exists { query, .. } => out.push(query),
Expr::InSubquery { expr, subquery, .. } => {
collect_expr_subqueries(expr, out);
out.push(subquery);
}
Expr::QuantifiedComparison { left, subquery, .. } => {
collect_expr_subqueries(left, out);
out.push(subquery);
}
Expr::BinaryOp { left, right, .. } => {
collect_expr_subqueries(left, out);
collect_expr_subqueries(right, out);
}
Expr::UnaryOp { expr, .. } => collect_expr_subqueries(expr, out),
_ => {}
}
}
fn collect_factor_subqueries<'e>(factor: &'e TableFactor<NoExt>, out: &mut Vec<&'e Query<NoExt>>) {
if let TableFactor::Derived {
subquery, spelling, ..
} = factor
{
if matches!(spelling, DerivedSpelling::Parenthesized) {
out.push(subquery);
}
}
}
fn doc_has_hardline(doc: &Doc) -> bool {
match doc {
Doc::HardLine => true,
Doc::Concat(parts) => parts.iter().any(doc_has_hardline),
Doc::Nest(_, inner) => doc_has_hardline(inner),
Doc::Group(inner) => doc_has_hardline(inner),
Doc::Nil | Doc::Text(_) | Doc::Line | Doc::SoftLine => false,
}
}
fn set_op_word(op: &SetOperator, all: bool, by_name: bool) -> String {
let base = match op {
SetOperator::Union => "UNION",
SetOperator::Intersect => "INTERSECT",
SetOperator::Except => "EXCEPT",
};
let mut word = String::from(base);
if all {
word.push_str(" ALL");
}
if by_name {
word.push_str(" BY NAME");
}
word
}
fn group_by_item_span(item: &GroupByItem<NoExt>) -> Span {
item.span()
}
fn is_simple_select(select: &Select<NoExt>) -> bool {
matches!(select.spelling, crate::ast::SelectSpelling::Select)
&& !select.straight_join
&& select.into.is_none()
&& select.lateral_views.is_empty()
&& select.connect_by.is_none()
&& select.group_by_quantifier.is_none()
&& select.group_by_all.is_none()
&& select.windows.is_empty()
&& select.qualify.is_none()
&& select.sample.is_none()
}