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
73 changes: 73 additions & 0 deletions crates/pgls_configuration/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,70 @@ impl From<KeywordCase> for pgls_pretty_print::renderer::KeywordCase {
}
}

/// Where a comma sits when a list breaks 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 CommaStyle {
#[default]
Trailing,
Leading,
}

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

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"trailing" => Ok(Self::Trailing),
"leading" => Ok(Self::Leading),
_ => Err("Value not supported for CommaStyle. Use 'trailing' or 'leading'."),
}
}
}

impl From<CommaStyle> for pgls_pretty_print::CommaStyle {
fn from(style: CommaStyle) -> Self {
match style {
CommaStyle::Trailing => Self::Trailing,
CommaStyle::Leading => Self::Leading,
}
}
}

/// Where a boolean operator sits when a condition breaks 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 LogicalOperatorPlacement {
#[default]
Trailing,
Leading,
}

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

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"trailing" => Ok(Self::Trailing),
"leading" => Ok(Self::Leading),
_ => Err(
"Value not supported for LogicalOperatorPlacement. Use 'trailing' or 'leading'.",
),
}
}
}

impl From<LogicalOperatorPlacement> for pgls_pretty_print::LogicalOperatorPlacement {
fn from(placement: LogicalOperatorPlacement) -> Self {
match placement {
LogicalOperatorPlacement::Trailing => Self::Trailing,
LogicalOperatorPlacement::Leading => Self::Leading,
}
}
}

/// 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 +161,13 @@ pub struct FormatConfiguration {
/// Data type casing (text, varchar, int): "upper" or "lower". Default: "lower".
#[partial(bpaf(long("type-case")))]
pub type_case: KeywordCase,
/// Where a comma sits when a list breaks: "trailing" or "leading". Default: "trailing".
#[partial(bpaf(long("comma-style")))]
pub comma_style: CommaStyle,
/// Where a boolean operator sits when a condition breaks: "trailing" or "leading".
/// Default: "trailing".
#[partial(bpaf(long("logical-operator-placement")))]
pub logical_operator_placement: LogicalOperatorPlacement,
/// 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 +189,8 @@ impl Default for FormatConfiguration {
keyword_case: KeywordCase::default(),
constant_case: KeywordCase::default(),
type_case: KeywordCase::default(),
comma_style: CommaStyle::default(),
logical_operator_placement: LogicalOperatorPlacement::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
51 changes: 44 additions & 7 deletions crates/pgls_pretty_print/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ pub enum FormatError {
}

/// Configuration for the SQL formatter.
/// Where a comma sits when a list breaks across lines.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum CommaStyle {
/// `a,` at the end of the line.
#[default]
Trailing,
/// `, a` at the start of the continuation line.
Leading,
}

/// Where a boolean operator sits when a condition breaks across lines.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum LogicalOperatorPlacement {
/// `a = 1 AND` at the end of the line.
#[default]
Trailing,
/// `AND a = 1` at the start of the continuation line.
Leading,
}

#[derive(Debug, Clone)]
pub struct FormatConfig {
/// Maximum line width before breaking. Default: 100.
Expand All @@ -46,6 +66,10 @@ pub struct FormatConfig {
pub constant_case: KeywordCase,
/// Casing for data types (text, varchar, int). Default: Lower.
pub type_case: KeywordCase,
/// Where a comma sits when a list breaks. Default: Trailing.
pub comma_style: CommaStyle,
/// Where a boolean operator sits when a condition breaks. Default: Trailing.
pub logical_operator_placement: LogicalOperatorPlacement,
}

impl Default for FormatConfig {
Expand All @@ -57,19 +81,32 @@ impl Default for FormatConfig {
keyword_case: KeywordCase::default(),
constant_case: KeywordCase::default(),
type_case: KeywordCase::default(),
comma_style: CommaStyle::default(),
logical_operator_placement: LogicalOperatorPlacement::default(),
}
}
}

impl From<FormatConfig> for RenderConfig {
fn from(config: FormatConfig) -> Self {
let FormatConfig {
line_width,
indent_size,
indent_style,
keyword_case,
constant_case,
type_case,
comma_style: _,
logical_operator_placement: _,
} = config;

Self {
max_line_length: config.line_width,
indent_size: config.indent_size,
indent_style: config.indent_style,
keyword_case: config.keyword_case,
constant_case: config.constant_case,
type_case: config.type_case,
max_line_length: line_width,
indent_size,
indent_style,
keyword_case,
constant_case,
type_case,
}
}
}
Expand Down Expand Up @@ -104,7 +141,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
20 changes: 16 additions & 4 deletions crates/pgls_pretty_print/src/nodes/bool_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use pgls_query::protobuf::{BoolExpr, BoolExprType};
use pgls_query::{Node, NodeEnum};

use crate::{
TokenKind,
LogicalOperatorPlacement, TokenKind,
emitter::{EventEmitter, GroupKind, LineType},
};

Expand All @@ -21,12 +21,24 @@ pub(super) fn emit_bool_expr(e: &mut EventEmitter, n: &BoolExpr) {

fn emit_variadic_bool_expr(e: &mut EventEmitter, n: &BoolExpr, keyword: TokenKind) {
let parent_prec = bool_precedence(n.boolop());
let leading = matches!(
e.config().logical_operator_placement,
LogicalOperatorPlacement::Leading
);

for (idx, arg) in n.args.iter().enumerate() {
if idx > 0 {
e.space();
e.token(keyword.clone());
e.line(LineType::SoftOrSpace);
if leading {
// The break opportunity sits before the keyword, so a broken condition reads
// "\n\tAND b = 2" while a single line one still reads "a = 1 AND b = 2".
e.line(LineType::SoftOrSpace);
e.token(keyword.clone());
e.space();
} else {
e.space();
e.token(keyword.clone());
e.line(LineType::SoftOrSpace);
}
}

emit_bool_operand(e, arg, parent_prec);
Expand Down
23 changes: 18 additions & 5 deletions crates/pgls_pretty_print/src/nodes/node_list.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use pgls_query::Node;

use crate::TokenKind;
use crate::emitter::{EventEmitter, LineType};
use crate::{CommaStyle, TokenKind};

/// Controls the spacing behavior after separators in list helpers
#[derive(Clone, Copy, Default)]
Expand All @@ -22,12 +22,25 @@ pub(super) fn emit_comma_separated_list_with_spacing<F>(
) where
F: Fn(&Node, &mut EventEmitter),
{
let leading = matches!(e.config().comma_style, CommaStyle::Leading);

for (i, n) in nodes.iter().enumerate() {
if i > 0 {
e.token(TokenKind::COMMA);
match spacing {
ListSeparatorSpacing::SoftOrSpace => e.line(LineType::SoftOrSpace),
ListSeparatorSpacing::Space => e.space(),
if leading {
// The break opportunity sits before the comma, so a broken list reads
// "\n, column" while a single line one still reads "a, b".
match spacing {
ListSeparatorSpacing::SoftOrSpace => e.line(LineType::Soft),
ListSeparatorSpacing::Space => {}
}
e.token(TokenKind::COMMA);
e.space();
} else {
e.token(TokenKind::COMMA);
match spacing {
ListSeparatorSpacing::SoftOrSpace => e.line(LineType::SoftOrSpace),
ListSeparatorSpacing::Space => e.space(),
}
}
}
render(n, e);
Expand Down
10 changes: 5 additions & 5 deletions crates/pgls_pretty_print/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ mod tests {

#[test]
fn test_keyword_case_upper() {
let mut emitter = EventEmitter::new();
let mut emitter = EventEmitter::new(crate::FormatConfig::default());
emitter.token(TokenKind::SELECT_KW);
emitter.space();
emitter.token(TokenKind::INT_NUMBER(1));
Expand All @@ -342,7 +342,7 @@ mod tests {

#[test]
fn test_keyword_case_lower() {
let mut emitter = EventEmitter::new();
let mut emitter = EventEmitter::new(crate::FormatConfig::default());
emitter.token(TokenKind::SELECT_KW);
emitter.space();
emitter.token(TokenKind::INT_NUMBER(1));
Expand All @@ -359,7 +359,7 @@ mod tests {

#[test]
fn test_constant_case_upper() {
let mut emitter = EventEmitter::new();
let mut emitter = EventEmitter::new(crate::FormatConfig::default());
emitter.token(TokenKind::SELECT_KW);
emitter.space();
emitter.token(TokenKind::NULL);
Expand All @@ -382,7 +382,7 @@ mod tests {

#[test]
fn test_constant_case_lower() {
let mut emitter = EventEmitter::new();
let mut emitter = EventEmitter::new(crate::FormatConfig::default());
emitter.token(TokenKind::SELECT_KW);
emitter.space();
emitter.token(TokenKind::NULL);
Expand All @@ -402,7 +402,7 @@ mod tests {

#[test]
fn test_mixed_case_settings() {
let mut emitter = EventEmitter::new();
let mut emitter = EventEmitter::new(crate::FormatConfig::default());
emitter.token(TokenKind::SELECT_KW);
emitter.space();
emitter.token(TokenKind::INT_NUMBER(1));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- pgls-format: keywordCase=upper, indentStyle=tabs, indentSize=4, lineWidth=80
SELECT
t.a,
t.b
FROM s.t
WHERE t.a > 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- pgls-format: logicalOperatorPlacement=leading, indentStyle=tabs, indentSize=4, lineWidth=80
SELECT staging.buildings.id
FROM staging.buildings
WHERE staging.buildings.construction_year > 1950
AND staging.buildings.address_fk IS NOT NULL
AND staging.buildings.identification_number IS NOT NULL;
6 changes: 6 additions & 0 deletions crates/pgls_pretty_print/tests/data/single/leading_commas.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- pgls-format: commaStyle=leading, indentStyle=tabs, indentSize=4, lineWidth=80
SELECT
staging.buildings.identification_number,
staging.buildings.construction_year,
staging.buildings.address_fk
FROM staging.buildings;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: crates/pgls_pretty_print/tests/tests.rs
input_file: crates/pgls_pretty_print/tests/data/single/format_config_header.sql
---
SELECT t.a, t.b FROM s.t WHERE t.a > 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: crates/pgls_pretty_print/tests/tests.rs
input_file: crates/pgls_pretty_print/tests/data/single/leading_boolean_operators.sql
---
select
staging.buildings.id
from
staging.buildings
where
staging.buildings.construction_year >
1950
and staging.buildings.address_fk is not null
and staging.buildings.identification_number is not null;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: crates/pgls_pretty_print/tests/tests.rs
input_file: crates/pgls_pretty_print/tests/data/single/leading_commas.sql
---
select
staging.buildings.identification_number
, staging.buildings.construction_year
, staging.buildings.address_fk
from
staging.buildings;
Loading