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 an explicit cast is spelled.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Merge, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum CastStyle {
#[default]
Cast,
Operator,
}

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

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cast" => Ok(Self::Cast),
"operator" => Ok(Self::Operator),
_ => Err("Value not supported for CastStyle. Use 'cast' or 'operator'."),
}
}
}

impl From<CastStyle> for pgls_pretty_print::CastStyle {
fn from(style: CastStyle) -> Self {
match style {
CastStyle::Cast => Self::Cast,
CastStyle::Operator => Self::Operator,
}
}
}

/// 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 an explicit cast is spelled: "cast" for `CAST(x AS t)`, "operator" for `x::t`.
/// Default: "cast".
#[partial(bpaf(long("cast-style")))]
pub cast_style: CastStyle,
/// 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(),
cast_style: CastStyle::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
15 changes: 14 additions & 1 deletion crates/pgls_pretty_print/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ pub enum FormatError {
BetaUnsupported { message: String },
}

/// How an explicit cast is spelled.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum CastStyle {
/// `CAST(expr AS type)`.
#[default]
Cast,
/// `expr::type`.
Operator,
}

/// Configuration for the SQL formatter.
#[derive(Debug, Clone)]
pub struct FormatConfig {
Expand All @@ -46,6 +56,8 @@ pub struct FormatConfig {
pub constant_case: KeywordCase,
/// Casing for data types (text, varchar, int). Default: Lower.
pub type_case: KeywordCase,
/// How an explicit cast is spelled. Default: Cast.
pub cast_style: CastStyle,
}

impl Default for FormatConfig {
Expand All @@ -57,6 +69,7 @@ impl Default for FormatConfig {
keyword_case: KeywordCase::default(),
constant_case: KeywordCase::default(),
type_case: KeywordCase::default(),
cast_style: CastStyle::default(),
}
}
}
Expand Down Expand Up @@ -104,7 +117,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
66 changes: 60 additions & 6 deletions crates/pgls_pretty_print/src/nodes/type_cast.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
use crate::{
TokenKind,
CastStyle, TokenKind,
emitter::{EventEmitter, GroupKind, LineType},
};
use pgls_query::protobuf::TypeCast;
use pgls_query::{
Node, NodeEnum,
protobuf::{AExprKind, TypeCast},
};

pub(super) fn emit_type_cast(e: &mut EventEmitter, n: &TypeCast) {
e.group_start(GroupKind::TypeCast);

// CAST(expr AS type) syntax
match e.config().cast_style {
CastStyle::Operator => emit_operator_cast(e, n),
CastStyle::Cast => emit_cast_call(e, n),
}

e.group_end();
}

fn emit_cast_call(e: &mut EventEmitter, n: &TypeCast) {
e.token(TokenKind::CAST_KW);
e.token(TokenKind::L_PAREN);

// Emit the expression
if let Some(ref arg) = n.arg {
super::emit_node(arg, e);
}
Expand All @@ -20,12 +30,56 @@ pub(super) fn emit_type_cast(e: &mut EventEmitter, n: &TypeCast) {
e.token(TokenKind::AS_KW);
e.space();

// Emit the type
if let Some(ref type_name) = n.type_name {
super::emit_type_name(e, type_name);
}

e.token(TokenKind::R_PAREN);
}

e.group_end();
fn emit_operator_cast(e: &mut EventEmitter, n: &TypeCast) {
if let Some(ref arg) = n.arg {
// `::` binds tighter than every infix operator, so anything that is not a self contained
// primary expression has to be parenthesised: `a + b::int` would cast b alone.
if needs_parentheses(arg) {
e.token(TokenKind::L_PAREN);
super::emit_node(arg, e);
e.token(TokenKind::R_PAREN);
} else {
super::emit_node(arg, e);
}
}

e.token(TokenKind::IDENT("::".to_string()));

if let Some(ref type_name) = n.type_name {
super::emit_type_name(e, type_name);
}
}

/// Whitelist of self contained expressions that can carry a `::` without parentheses.
///
/// Anything absent from this list is parenthesised, which is always valid SQL. Listing the unsafe
/// kinds instead would fail open on any node nobody thought about.
fn needs_parentheses(node: &Node) -> bool {
match node.node.as_ref() {
Some(NodeEnum::AExpr(a_expr)) => a_expr.kind != AExprKind::AexprNullif as i32,
Some(
NodeEnum::AConst(_)
| NodeEnum::ColumnRef(_)
| NodeEnum::ParamRef(_)
| NodeEnum::FuncCall(_)
| NodeEnum::NullIfExpr(_)
| NodeEnum::TypeCast(_)
| NodeEnum::SubLink(_)
| NodeEnum::CaseExpr(_)
| NodeEnum::CoalesceExpr(_)
| NodeEnum::MinMaxExpr(_)
| NodeEnum::ArrayExpr(_)
| NodeEnum::RowExpr(_)
| NodeEnum::AIndirection(_)
| NodeEnum::AArrayExpr(_),
) => false,
_ => true,
}
}
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
12 changes: 12 additions & 0 deletions crates/pgls_pretty_print/tests/data/single/cast_operator_style.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- pgls-format: castStyle=operator
SELECT
CAST(t.id AS bigint),
CAST(t.name AS text),
CAST(t.id AS public.object_id),
CAST(nullif(t.a, '') AS date),
CAST(t.a + t.b AS int),
CAST(t.a || t.b AS text),
CAST(CAST(t.a AS text) AS bigint),
CAST((SELECT max(u.id) FROM s.u) AS bigint),
CAST(CASE WHEN t.a THEN 1 ELSE 2 END AS text)
FROM s.t;
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,16 @@
---
source: crates/pgls_pretty_print/tests/tests.rs
input_file: crates/pgls_pretty_print/tests/data/single/cast_operator_style.sql
---
select
t.id::bigint,
t.name::text,
t.id::public.object_id,
nullif(t.a, '')::date,
(t.a + t.b)::int,
(t.a || t.b)::text,
t.a::text::bigint,
(select MAX(u.id) from s.u)::bigint,
case when t.a then 1 else 2 end::text
from
s.t;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/pgls_pretty_print/tests/tests.rs
input_file: crates/pgls_pretty_print/tests/data/single/cast_operator_style.sql
---
select
t.id::bigint,
t.name::text,
t.id::public.object_id,
nullif(t.a, '')::date,
(t.a + t.b)::int,
(t.a || t.b)::text,
t.a::text::bigint,
(select MAX(u.id) from s.u)::bigint,
case when t.a then 1 else 2 end::text
from
s.t;
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;
Loading