feat(format): implement subquery and CASE expression formatting
CI / Test (push) Successful in 45s
CI / Build (push) Successful in 21s

* Add support for formatting subqueries with configurable placement and spacing.
* Implement CASE expression formatting with options for wrapping and collapsing.
* Introduce tests for subquery and CASE expression scenarios to ensure correctness.
This commit is contained in:
Hein
2026-09-21 17:10:49 +02:00
parent cf9ea5fbda
commit 41fdaf415c
3 changed files with 791 additions and 92 deletions
+316
View File
@@ -1,6 +1,7 @@
package format
import (
"strings"
"testing"
"git.warky.dev/wdevs/pgtidy/pkg/config"
@@ -295,3 +296,318 @@ func TestCorpusUnaffectedByDML(t *testing.T) {
t.Errorf("create function: DML formatter changed semantics")
}
}
// --- Subquery formatting ---
func TestDMLSubqueryDerivedTable(t *testing.T) {
src := "select a from (select x, y from t) s where s.x = 1;"
want := "SELECT a\n" +
"FROM (\n" +
" SELECT\n" +
" x\n" +
" ,y\n" +
" FROM t\n" +
") s\n" +
"WHERE s.x = 1;\n"
got := format(src)
if got != want {
t.Errorf("derived table\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
checkDML(t, "derived table", got)
if !semanticallyEqual(src, got) {
t.Errorf("derived table: formatting changed semantics")
}
}
func TestDMLSubqueryScalarInSelect(t *testing.T) {
src := "select a, (select max(x) from t2) as m from t1;"
got := format(src)
if got == "" {
t.Error("empty output")
}
checkDML(t, "scalar subquery", got)
if !semanticallyEqual(src, got) {
t.Errorf("scalar subquery: formatting changed semantics")
}
}
func TestDMLSubqueryIn(t *testing.T) {
src := "select a from t where a in (select b from t2);"
want := "SELECT a\n" +
"FROM t\n" +
"WHERE a IN (\n" +
" SELECT b\n" +
" FROM t2\n" +
");\n"
got := format(src)
if got != want {
t.Errorf("in subquery\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
checkDML(t, "in subquery", got)
}
func TestDMLSubqueryExists(t *testing.T) {
src := "select a from t where exists (select 1 from t2 where t2.a = t.a);"
got := format(src)
if got == "" {
t.Error("empty output")
}
checkDML(t, "exists subquery", got)
if !semanticallyEqual(src, got) {
t.Errorf("exists subquery: formatting changed semantics")
}
}
func TestDMLSubqueryInValueList(t *testing.T) {
// A plain value list must not be mistaken for a subquery.
src := "select a from t where a in (1, 2, 3);"
want := "SELECT a\nFROM t\nWHERE a IN (1, 2, 3);\n"
got := format(src)
if got != want {
t.Errorf("value list in()\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
checkDML(t, "value list in()", got)
}
func TestDMLSubqueryPlacementConfig(t *testing.T) {
st := config.Default()
st.SubqueryContent = config.PlacementSameLine
st.SubqueryClosing = config.PlacementSameLine
src := "select a from t where a in (select b from t2);"
got := File(parser.Parse(src), st)
if got == "" {
t.Error("empty output")
}
twice := File(parser.Parse(got), st)
if twice != got {
t.Errorf("subquery placement config not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
}
}
func TestDMLSubquerySpaceBeforeParen(t *testing.T) {
st := config.Default()
st.SubquerySpaceBeforeParen = true
src := "select array(select x from t) from t2;"
got := File(parser.Parse(src), st)
want := "SELECT ARRAY (\n SELECT x\n FROM t\n)\nFROM t2;\n"
if got != want {
t.Errorf("subquery_space_before_paren\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
twice := File(parser.Parse(got), st)
if twice != got {
t.Errorf("subquery_space_before_paren not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
}
}
func TestDMLCTEUsesSubqueryConfig(t *testing.T) {
// CTE bodies should honor the same subquery_* settings, not a hardcoded layout.
st := config.Default()
st.SubqueryOpening = config.PlacementNewLine
src := "with cte as (select x from y) select x from cte;"
got := File(parser.Parse(src), st)
want := "WITH cte AS\n(\n SELECT x\n FROM y\n)\nSELECT x\nFROM cte;\n"
if got != want {
t.Errorf("cte subquery_opening=new_line\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
twice := File(parser.Parse(got), st)
if twice != got {
t.Errorf("cte subquery_opening not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
}
}
// --- INSERT VALUES collapse ---
func TestDMLInsertValuesCollapseDefault(t *testing.T) {
// insert_collapse_values defaults to true: multiple rows stay on one line.
src := "insert into t (a, b) values (1, 2), (3, 4), (5, 6);"
want := "INSERT INTO t(a, b)\nVALUES (1, 2), (3, 4), (5, 6);\n"
got := format(src)
if got != want {
t.Errorf("values collapse default\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
checkDML(t, "values collapse default", got)
}
func TestDMLInsertValuesNoCollapse(t *testing.T) {
st := config.Default()
st.InsertCollapseValues = false
src := "insert into t (a, b) values (1, 2), (3, 4), (5, 6);"
want := "INSERT INTO t(a, b)\n" +
"VALUES\n" +
" (1, 2)\n" +
" ,(3, 4)\n" +
" ,(5, 6);\n"
got := File(parser.Parse(src), st)
if got != want {
t.Errorf("values no collapse\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
twice := File(parser.Parse(got), st)
if twice != got {
t.Errorf("values no collapse not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
}
if !semanticallyEqual(src, got) {
t.Errorf("values no collapse: formatting changed semantics")
}
}
func TestDMLInsertValuesSingleRowUnaffected(t *testing.T) {
// A single-row VALUES is unaffected by insert_collapse_values either way.
st := config.Default()
st.InsertCollapseValues = false
src := "insert into t (a, b) values (1, 2);"
want := "INSERT INTO t(a, b)\nVALUES (1, 2);\n"
got := File(parser.Parse(src), st)
if got != want {
t.Errorf("single row values\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
}
// --- CASE expression formatting ---
func TestDMLCaseInlineDefault(t *testing.T) {
src := "select case when a = 1 then 'one' when a = 2 then 'two' else 'other' end as label from t;"
want := "SELECT CASE WHEN a = 1 THEN 'one' WHEN a = 2 THEN 'two' ELSE 'other' END AS label\nFROM t;\n"
got := format(src)
if got != want {
t.Errorf("case inline default\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
checkDML(t, "case inline default", got)
if !semanticallyEqual(src, got) {
t.Errorf("case inline default: formatting changed semantics")
}
}
func TestDMLCaseWhenWrap(t *testing.T) {
st := config.Default()
st.CaseWhenWrap = true
src := "select case when a = 1 then 'one' when a = 2 then 'two' else 'other' end as label from t;"
want := "SELECT CASE\n" +
" WHEN a = 1 THEN 'one'\n" +
" WHEN a = 2 THEN 'two'\n" +
" ELSE 'other'\n" +
"END AS label\n" +
"FROM t;\n"
got := File(parser.Parse(src), st)
if got != want {
t.Errorf("case when_wrap\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
twice := File(parser.Parse(got), st)
if twice != got {
t.Errorf("case when_wrap not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
}
if !semanticallyEqual(src, got) {
t.Errorf("case when_wrap: formatting changed semantics")
}
}
func TestDMLCaseEndSameLine(t *testing.T) {
st := config.Default()
st.CaseWhenWrap = true
st.CaseEnd = config.PlacementSameLine
src := "select case when a = 1 then 'one' else 'other' end as label from t;"
want := "SELECT CASE\n" +
" WHEN a = 1 THEN 'one'\n" +
" ELSE 'other' END AS label\n" +
"FROM t;\n"
got := File(parser.Parse(src), st)
if got != want {
t.Errorf("case_end same_line\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
twice := File(parser.Parse(got), st)
if twice != got {
t.Errorf("case_end same_line not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
}
}
func TestDMLCaseCollapseShort(t *testing.T) {
// case_collapse keeps a short CASE on one line even with case_when_wrap set.
st := config.Default()
st.CaseWhenWrap = true
st.CaseCollapse = true
src := "select case when a = 1 then 'x' else 'y' end from t;"
want := "SELECT CASE WHEN a = 1 THEN 'x' ELSE 'y' END\nFROM t;\n"
got := File(parser.Parse(src), st)
if got != want {
t.Errorf("case_collapse short\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
twice := File(parser.Parse(got), st)
if twice != got {
t.Errorf("case_collapse short not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
}
}
func TestDMLCaseCollapseLongStillWraps(t *testing.T) {
// case_collapse only keeps CASE inline when it is short; a long CASE still wraps.
st := config.Default()
st.CaseWhenWrap = true
st.CaseCollapse = true
src := "select case when a = 1 then 'a fairly long result value one' " +
"when a = 2 then 'a fairly long result value two' else 'a fairly long default value' end from t;"
got := File(parser.Parse(src), st)
if !strings.Contains(got, "\n WHEN a = 1") {
t.Errorf("case_collapse long: expected wrapped WHEN branches, got:\n%s", got)
}
twice := File(parser.Parse(got), st)
if twice != got {
t.Errorf("case_collapse long not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
}
}
func TestDMLCaseNestedInSubquery(t *testing.T) {
src := "select a from (select case when x = 1 then 'y' else 'n' end as c from t) s;"
got := format(src)
if got == "" {
t.Error("empty output")
}
checkDML(t, "case nested in subquery", got)
if !semanticallyEqual(src, got) {
t.Errorf("case nested in subquery: formatting changed semantics")
}
}
func TestDMLSubqueryNestedInCase(t *testing.T) {
src := "select case when exists (select 1 from t2 where t2.a = t1.a) then 'y' else 'n' end from t1;"
got := format(src)
if got == "" {
t.Error("empty output")
}
checkDML(t, "subquery nested in case", got)
if !semanticallyEqual(src, got) {
t.Errorf("subquery nested in case: formatting changed semantics")
}
}
func TestDMLCaseSimpleForm(t *testing.T) {
// Simple CASE (with an operand) must round-trip too.
src := "select case a when 1 then 'one' when 2 then 'two' else 'other' end from t;"
want := "SELECT CASE a WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'other' END\nFROM t;\n"
got := format(src)
if got != want {
t.Errorf("simple case\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
checkDML(t, "simple case", got)
}
// --- record_space_before_paren ---
func TestDMLRecordSpaceBeforeParen(t *testing.T) {
src := "select row(1, 2) from t;"
got := format(src)
want := "SELECT ROW(1, 2)\nFROM t;\n"
if got != want {
t.Errorf("row() default\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
st := config.Default()
st.RecordSpaceBeforeParen = true
gotSpaced := File(parser.Parse(src), st)
wantSpaced := "SELECT ROW (1, 2)\nFROM t;\n"
if gotSpaced != wantSpaced {
t.Errorf("row() space_before_paren\n--- got ---\n%s\n--- want ---\n%s", gotSpaced, wantSpaced)
}
twice := File(parser.Parse(gotSpaced), st)
if twice != gotSpaced {
t.Errorf("row() space_before_paren not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", gotSpaced, twice)
}
}