diff --git a/crates/pgls_pretty_print/src/nodes/case_expr.rs b/crates/pgls_pretty_print/src/nodes/case_expr.rs index b8ed85b53..1a86320ee 100644 --- a/crates/pgls_pretty_print/src/nodes/case_expr.rs +++ b/crates/pgls_pretty_print/src/nodes/case_expr.rs @@ -6,10 +6,28 @@ use crate::{ }; pub(super) fn emit_case_expr(e: &mut EventEmitter, n: &CaseExpr) { + emit_case_expr_impl(e, n, true); +} + +/// Emits a branch result, `then ` or `else `. +/// +/// In expanded layout a CASE aligns its branches with the column it starts at. A CASE nested as a +/// result would start after `then `, so every level would move the next one right by the keyword +/// and two indents, and a few levels run past the line width. A nested CASE is therefore indented +/// from the line of its THEN or ELSE instead: its branches one level in, `end` under `then`. +pub(super) fn emit_case_result(e: &mut EventEmitter, result: &pgls_query::protobuf::Node) { + super::emit_node_with(result, e, |node, e| match node { + pgls_query::NodeEnum::CaseExpr(case) => emit_case_expr_impl(e, case, false), + node => super::emit_node_enum(node, e), + }); +} + +fn emit_case_expr_impl(e: &mut EventEmitter, n: &CaseExpr, align: bool) { e.group_start(GroupKind::CaseExpr); let expanded = matches!(e.config().layout, crate::Layout::Expanded); - if expanded { + let align = expanded && align; + if align { e.align_start(); } e.token(TokenKind::CASE_KW); @@ -35,14 +53,14 @@ pub(super) fn emit_case_expr(e: &mut EventEmitter, n: &CaseExpr) { super::emit_layout_break(e); e.token(TokenKind::ELSE_KW); e.space(); - super::emit_node(defresult, e); + emit_case_result(e, defresult); } e.indent_end(); super::emit_layout_break(e); e.token(TokenKind::END_KW); - if expanded { + if align { e.align_end(); } e.group_end(); diff --git a/crates/pgls_pretty_print/src/nodes/case_when.rs b/crates/pgls_pretty_print/src/nodes/case_when.rs index 2ce18c85a..a9312da30 100644 --- a/crates/pgls_pretty_print/src/nodes/case_when.rs +++ b/crates/pgls_pretty_print/src/nodes/case_when.rs @@ -31,7 +31,7 @@ pub(super) fn emit_case_when(e: &mut EventEmitter, n: &CaseWhen) { if let Some(ref result) = n.result { e.space(); - super::emit_node(result, e); + super::case_expr::emit_case_result(e, result); } if expanded { e.indent_end(); diff --git a/crates/pgls_pretty_print/tests/case_layout.rs b/crates/pgls_pretty_print/tests/case_layout.rs index 6c8489e4f..d0e4fde3c 100644 --- a/crates/pgls_pretty_print/tests/case_layout.rs +++ b/crates/pgls_pretty_print/tests/case_layout.rs @@ -87,10 +87,10 @@ fn expanded_nested_case_keeps_each_branch_at_its_own_indent() { \tCASE status\n\ \t\tWHEN 'A'\n\ \t\t\tTHEN CASE\n\ - \t\t\t\t WHEN enabled\n\ - \t\t\t\t\t THEN 'active'\n\ - \t\t\t\t ELSE 'paused'\n\ - \t\t\t END\n\ + \t\t\t\tWHEN enabled\n\ + \t\t\t\t\tTHEN 'active'\n\ + \t\t\t\tELSE 'paused'\n\ + \t\t\tEND\n\ \t\tELSE 'closed'\n\ \tEND\n\ ;"; @@ -249,3 +249,37 @@ fn the_documented_expanded_case_layout() { "select\n id\n , case\n when status = 'active'\n and deleted_at is null\n then 'live'\n else 'archived'\n end as state\nfrom\n accounts;" ); } + +#[test] +fn expanded_case_nested_in_else_is_indented_from_else() { + let sql = "SELECT CASE WHEN a THEN 1 ELSE CASE WHEN b THEN 2 ELSE 3 END END;"; + let expected = "SELECT\n\ + \tCASE\n\ + \t\tWHEN a\n\ + \t\t\tTHEN 1\n\ + \t\tELSE CASE\n\ + \t\t\tWHEN b\n\ + \t\t\t\tTHEN 2\n\ + \t\t\tELSE 3\n\ + \t\tEND\n\ + \tEND\n\ + ;"; + assert_expanded_layout(sql, expected); +} + +/// Every level of a CASE nested in THEN moves right by two indents, so twenty levels stay within +/// a width of 100 with the default indent. +#[test] +fn deeply_nested_expanded_case_stays_within_the_width() { + let mut sql = String::from("0"); + for level in 0..20 { + sql = format!("case when c{level} > {level} then {sql} else {level} end"); + } + let config = FormatConfig { + layout: Layout::Expanded, + ..Default::default() + }; + let output = format_idempotently(&format!("select {sql} as x from t"), &config); + let widest = output.lines().map(str::len).max().unwrap(); + assert!(widest <= 100, "{widest} columns:\n{output}"); +} diff --git a/crates/pgls_pretty_print/tests/snapshots/multi/tests__expanded_case_alignment_100.snap b/crates/pgls_pretty_print/tests/snapshots/multi/tests__expanded_case_alignment_100.snap index f5cef8a26..deff38ce5 100644 --- a/crates/pgls_pretty_print/tests/snapshots/multi/tests__expanded_case_alignment_100.snap +++ b/crates/pgls_pretty_print/tests/snapshots/multi/tests__expanded_case_alignment_100.snap @@ -34,10 +34,10 @@ from accounts; select case kind when 'a' then case - when x > 1 - then 'big' - else 'small' - end + when x > 1 + then 'big' + else 'small' + end else 'other' end from t; diff --git a/crates/pgls_pretty_print/tests/snapshots/multi/tests__expanded_case_alignment_80.snap b/crates/pgls_pretty_print/tests/snapshots/multi/tests__expanded_case_alignment_80.snap index f5cef8a26..deff38ce5 100644 --- a/crates/pgls_pretty_print/tests/snapshots/multi/tests__expanded_case_alignment_80.snap +++ b/crates/pgls_pretty_print/tests/snapshots/multi/tests__expanded_case_alignment_80.snap @@ -34,10 +34,10 @@ from accounts; select case kind when 'a' then case - when x > 1 - then 'big' - else 'small' - end + when x > 1 + then 'big' + else 'small' + end else 'other' end from t; diff --git a/docs/features/formatting.md b/docs/features/formatting.md index c3bdaad46..4cd4c8662 100644 --- a/docs/features/formatting.md +++ b/docs/features/formatting.md @@ -94,8 +94,10 @@ where With `layout: "expanded"`, a `CASE` expression puts every `WHEN` and `ELSE` on its own line, one indent level in from where `CASE` starts, `THEN` one level further in, and `END` back under `CASE`. This holds wherever the `CASE` -starts, after a leading comma or `THEN` for instance. A `WHEN` condition that -breaks continues one level in from `WHEN`: +starts, after a leading comma for instance. A `CASE` nested as the result of a +`THEN` or `ELSE` is indented from that line instead, with `END` under `THEN` or +`ELSE`, so that deep nesting doesn't run past the line width. A `WHEN` +condition that breaks continues one level in from `WHEN`: ```sql select