Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions crates/pgls_configuration/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,37 @@ impl From<KeywordCase> for pgls_pretty_print::renderer::KeywordCase {
}
}

/// How a statement is laid out across lines.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum Layout {
#[default]
Fit,
Expanded,
}

impl FromStr for Layout {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"fit" => Ok(Self::Fit),
"expanded" => Ok(Self::Expanded),
_ => Err("Value not supported for Layout. Use 'fit' or 'expanded'."),
}
}
}

impl From<Layout> for pgls_pretty_print::Layout {
fn from(layout: Layout) -> Self {
match layout {
Layout::Fit => Self::Fit,
Layout::Expanded => Self::Expanded,
}
}
}

/// The configuration for SQL formatting.
#[derive(Clone, Debug, Deserialize, Eq, Partial, PartialEq, Serialize)]
#[partial(derive(Bpaf, Clone, Eq, PartialEq, Merge))]
Expand Down Expand Up @@ -97,6 +128,10 @@ pub struct FormatConfiguration {
/// Data type casing (text, varchar, int): "upper" or "lower". Default: "lower".
#[partial(bpaf(long("type-case")))]
pub type_case: KeywordCase,
/// How a statement is laid out: "fit" breaks only when a line would exceed the line width,
/// "expanded" always breaks between clauses. Default: "fit".
#[partial(bpaf(long("layout")))]
pub layout: Layout,
/// If `true`, skip formatting of SQL function bodies (keep them verbatim). Default: `false`.
#[partial(bpaf(long("skip-fn-bodies")))]
pub skip_fn_bodies: bool,
Expand All @@ -118,6 +153,7 @@ impl Default for FormatConfiguration {
keyword_case: KeywordCase::default(),
constant_case: KeywordCase::default(),
type_case: KeywordCase::default(),
layout: Layout::default(),
skip_fn_bodies: false,
ignore: Default::default(),
include: Default::default(),
Expand Down
21 changes: 18 additions & 3 deletions crates/pgls_pretty_print/src/emitter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::FormatConfig;
pub use crate::codegen::group_kind::GroupKind;
pub use crate::codegen::token_kind::TokenKind;

Expand All @@ -22,14 +23,28 @@ pub enum LayoutEvent {
IndentEnd,
}

#[derive(Debug, Default)]
/// Collects layout events for the renderer.
///
/// The emitter holds the configuration because some options decide which tokens exist at all,
/// such as where a comma sits in a list, and not merely how a token is rendered.
#[derive(Debug)]
pub struct EventEmitter {
pub events: Vec<LayoutEvent>,
config: FormatConfig,
}

impl EventEmitter {
pub fn new() -> Self {
Self::default()
pub fn new(config: FormatConfig) -> Self {
Self {
events: Vec::new(),
config,
}
}

// Later option PRs inspect this while deciding which layout events to emit.
#[allow(dead_code)]
pub fn config(&self) -> &FormatConfig {
&self.config
}

pub fn token(&mut self, token: TokenKind) {
Expand Down
17 changes: 16 additions & 1 deletion crates/pgls_pretty_print/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ pub use crate::renderer::{IndentStyle, KeywordCase, RenderConfig};
use pgls_query::NodeEnum;
use thiserror::Error;

/// How a statement is laid out across lines.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Layout {
/// Break only when a line would exceed the line width.
#[default]
Fit,
/// Always break between the clauses of a statement, whatever the width. The line width then
/// only governs breaking inside a clause.
Expanded,
}

/// Error type for formatting operations.
#[derive(Debug, Error)]
pub enum FormatError {
Expand Down Expand Up @@ -46,6 +57,8 @@ pub struct FormatConfig {
pub constant_case: KeywordCase,
/// Casing for data types (text, varchar, int). Default: Lower.
pub type_case: KeywordCase,
/// How a statement is laid out across lines. Default: Fit.
pub layout: Layout,
}

impl Default for FormatConfig {
Expand All @@ -57,6 +70,7 @@ impl Default for FormatConfig {
keyword_case: KeywordCase::default(),
constant_case: KeywordCase::default(),
type_case: KeywordCase::default(),
layout: Layout::default(),
}
}
}
Expand Down Expand Up @@ -104,7 +118,7 @@ pub fn format_statement(
config: &FormatConfig,
) -> Result<FormatResult, FormatError> {
// Emit layout events from AST
let mut emitter = emitter::EventEmitter::new();
let mut emitter = emitter::EventEmitter::new(config.clone());
nodes::emit_node_enum(ast, &mut emitter);

// Render to string
Expand Down Expand Up @@ -174,5 +188,6 @@ mod tests {
let config = FormatConfig::default();
assert_eq!(config.line_width, 100);
assert_eq!(config.indent_size, 2);
assert_eq!(config.layout, Layout::Fit);
}
}
18 changes: 13 additions & 5 deletions crates/pgls_pretty_print/src/nodes/insert_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,26 @@ fn emit_insert_stmt_impl(e: &mut EventEmitter, n: &InsertStmt, with_semicolon: b
// Emit column list if present
if !n.cols.is_empty() {
e.space();
// Wrap column list in a group so it can try to fit on one line
// The column list has its own group so it can fit on one line in fit layout. In expanded
// layout the separator breaks below are hard, which makes the group open up on purpose.
e.group_start(GroupKind::InsertStmt);
e.token(TokenKind::L_PAREN);
e.line(LineType::Soft);
e.indent_start();
emit_comma_separated_list(e, &n.cols, |node, e| {

for (index, node) in n.cols.iter().enumerate() {
if index > 0 {
e.token(TokenKind::COMMA);
super::emit_layout_break(e);
}

if let Some(pgls_query::NodeEnum::ResTarget(res_target)) = node.node.as_ref() {
emit_column_name(e, res_target);
} else {
super::emit_node(node, e);
}
});
}

e.indent_end();
e.line(LineType::Soft);
e.token(TokenKind::R_PAREN);
Expand Down Expand Up @@ -76,7 +84,7 @@ fn emit_insert_stmt_impl(e: &mut EventEmitter, n: &InsertStmt, with_semicolon: b

// Emit VALUES or SELECT or DEFAULT VALUES
if let Some(ref select_stmt) = n.select_stmt {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
// Use no-semicolon variant since INSERT will emit its own semicolon
if let Some(pgls_query::NodeEnum::SelectStmt(stmt)) = select_stmt.node.as_ref() {
super::emit_select_stmt_no_semicolon(e, stmt);
Expand All @@ -97,7 +105,7 @@ fn emit_insert_stmt_impl(e: &mut EventEmitter, n: &InsertStmt, with_semicolon: b
}

if !n.returning_list.is_empty() {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::RETURNING_KW);
e.space();
emit_comma_separated_list(e, &n.returning_list, super::emit_node);
Expand Down
11 changes: 10 additions & 1 deletion crates/pgls_pretty_print/src/nodes/join_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,16 @@ pub(super) fn emit_join_expr(e: &mut EventEmitter, n: &JoinExpr) {
}

if n.larg.is_some() {
e.line(LineType::SoftOrSpace);
if matches!(e.config().layout, crate::Layout::Expanded) {
// A hard break makes every soft line in the current group break too. Close the left
// operand's group before emitting it so the joined table and qualification can still
// fit.
e.group_end();
super::emit_layout_break(e);
e.group_start(GroupKind::JoinExpr);
} else {
e.line(LineType::SoftOrSpace);
}
}

let mut first_token = true;
Expand Down
41 changes: 41 additions & 0 deletions crates/pgls_pretty_print/src/nodes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,22 @@ pub(super) fn emit_clause_condition(e: &mut EventEmitter, clause: &Node) {
e.indent_end();
}

/// Emits the break that separates a statement clause from the previous one.
///
/// In expanded layout it is a `Hard` line, which `try_single_line` refuses to collapse, so the
/// break propagates to every enclosing group and the whole statement opens up. That propagation is
/// why expanded layout needs no renderer change.
#[allow(dead_code)] // Consumed by the clause emitters introduced in task 3.
pub(super) fn emit_layout_break(e: &mut EventEmitter) {
use crate::Layout;
use crate::emitter::LineType;

match e.config().layout {
Layout::Expanded => e.line(LineType::Hard),
Layout::Fit => e.line(LineType::SoftOrSpace),
}
}

pub fn emit_node_enum(node: &NodeEnum, e: &mut EventEmitter) {
match &node {
NodeEnum::RawStmt(n) => emit_raw_stmt(e, n),
Expand Down Expand Up @@ -818,3 +834,28 @@ pub fn emit_node_enum(node: &NodeEnum, e: &mut EventEmitter) {
NodeEnum::Query(n) => emit_query(e, n),
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::emitter::{EventEmitter, LayoutEvent, LineType};
use crate::{FormatConfig, Layout};

#[test]
fn fit_layout_emits_a_soft_or_space_break() {
let mut e = EventEmitter::new(FormatConfig::default());
emit_layout_break(&mut e);
assert_eq!(e.events, vec![LayoutEvent::Line(LineType::SoftOrSpace)]);
}

#[test]
fn expanded_layout_emits_a_hard_break() {
let config = FormatConfig {
layout: Layout::Expanded,
..Default::default()
};
let mut e = EventEmitter::new(config);
emit_layout_break(&mut e);
assert_eq!(e.events, vec![LayoutEvent::Line(LineType::Hard)]);
}
}
34 changes: 20 additions & 14 deletions crates/pgls_pretty_print/src/nodes/select_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,21 +188,27 @@ fn emit_select_stmt_impl(e: &mut EventEmitter, n: &SelectStmt, with_semicolon: b

if !n.target_list.is_empty() {
e.indent_start();
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);

emit_comma_separated_list(e, &n.target_list, super::emit_node);
for (index, target) in n.target_list.iter().enumerate() {
if index > 0 {
e.token(TokenKind::COMMA);
super::emit_layout_break(e);
}
super::emit_node(target, e);
}

e.indent_end();
}

// Emit INTO clause if present (SELECT ... INTO table_name)
if let Some(ref into_clause) = n.into_clause {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
super::emit_into_clause(e, into_clause);
}

if !n.from_clause.is_empty() {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::FROM_KW);
e.line(LineType::SoftOrSpace);

Expand All @@ -214,14 +220,14 @@ fn emit_select_stmt_impl(e: &mut EventEmitter, n: &SelectStmt, with_semicolon: b
}

if let Some(ref where_clause) = n.where_clause {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::WHERE_KW);
super::emit_clause_condition(e, where_clause);
}

// Emit GROUP BY clause if present
if !n.group_clause.is_empty() {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::GROUP_KW);
e.space();
e.token(TokenKind::BY_KW);
Expand All @@ -238,14 +244,14 @@ fn emit_select_stmt_impl(e: &mut EventEmitter, n: &SelectStmt, with_semicolon: b

// Emit HAVING clause if present
if let Some(ref having_clause) = n.having_clause {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::HAVING_KW);
super::emit_clause_condition(e, having_clause);
}

// Emit WINDOW clause if present
if !n.window_clause.is_empty() {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::WINDOW_KW);
e.line(LineType::SoftOrSpace);
e.indent_start();
Expand All @@ -266,7 +272,7 @@ fn emit_select_stmt_impl(e: &mut EventEmitter, n: &SelectStmt, with_semicolon: b

// Emit ORDER BY clause if present
if !n.sort_clause.is_empty() {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::ORDER_KW);
e.space();
e.token(TokenKind::BY_KW);
Expand All @@ -279,7 +285,7 @@ fn emit_select_stmt_impl(e: &mut EventEmitter, n: &SelectStmt, with_semicolon: b
match n.limit_option() {
LimitOption::WithTies => {
if let Some(ref limit_offset) = n.limit_offset {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::OFFSET_KW);
e.space();
super::emit_node(limit_offset, e);
Expand All @@ -288,7 +294,7 @@ fn emit_select_stmt_impl(e: &mut EventEmitter, n: &SelectStmt, with_semicolon: b
}

if let Some(ref limit_count) = n.limit_count {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::FETCH_KW);
e.space();
e.token(TokenKind::FIRST_KW);
Expand All @@ -304,14 +310,14 @@ fn emit_select_stmt_impl(e: &mut EventEmitter, n: &SelectStmt, with_semicolon: b
}
_ => {
if let Some(ref limit_count) = n.limit_count {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::LIMIT_KW);
e.space();
super::emit_node(limit_count, e);
}

if let Some(ref limit_offset) = n.limit_offset {
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
e.token(TokenKind::OFFSET_KW);
e.space();
super::emit_node(limit_offset, e);
Expand All @@ -324,7 +330,7 @@ fn emit_select_stmt_impl(e: &mut EventEmitter, n: &SelectStmt, with_semicolon: b
if let Some(pgls_query::NodeEnum::LockingClause(locking_clause)) =
locking.node.as_ref()
{
e.line(LineType::SoftOrSpace);
super::emit_layout_break(e);
super::emit_locking_clause(e, locking_clause);
}
}
Expand Down
Loading