package format import ( "testing" "git.warky.dev/wdevs/pgtidy/pkg/config" "git.warky.dev/wdevs/pgtidy/pkg/parser" ) // checkDML asserts that got is idempotent under formatting. func checkDML(t *testing.T, name, got string) { t.Helper() twice := format(got) if twice != got { t.Errorf("%s: not idempotent\n--- once ---\n%s\n--- twice ---\n%s", name, got, twice) } } func TestDMLSelectSingleCol(t *testing.T) { src := "select a from t where x = 1;" want := "SELECT a\nFROM t\nWHERE x = 1;\n" got := format(src) if got != want { t.Errorf("select single col\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "select single col", got) if !semanticallyEqual(src, got) { t.Errorf("select single col: formatting changed semantics") } } func TestDMLSelectMultiCol(t *testing.T) { src := "select a, b, c from t;" want := "SELECT\n a\n ,b\n ,c\nFROM t;\n" got := format(src) if got != want { t.Errorf("select multi col\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "select multi col", got) } func TestDMLSelectClauses(t *testing.T) { src := "select a, b from t where x = 1 group by a having count(*) > 1 order by b limit 10 offset 5;" want := "SELECT\n a\n ,b\nFROM t\nWHERE x = 1\nGROUP BY a\nHAVING count(*) > 1\nORDER BY b\nLIMIT 10\nOFFSET 5;\n" got := format(src) if got != want { t.Errorf("select clauses\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "select clauses", got) } func TestDMLSelectJoins(t *testing.T) { src := "select a.x, b.y from a join b on a.id = b.id left join c on b.id = c.id;" want := "SELECT\n a.x\n ,b.y\nFROM a\nJOIN b ON a.id = b.id\nLEFT JOIN c ON b.id = c.id;\n" got := format(src) if got != want { t.Errorf("select joins\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "select joins", got) } func TestDMLInsert(t *testing.T) { src := "insert into t (a, b) values (1, 2);" // Table name before '(' looks like a function call to the spacing engine; // accepted limitation — no space between table name and column list. want := "INSERT INTO t(a, b)\nVALUES (1, 2);\n" got := format(src) if got != want { t.Errorf("insert\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "insert", got) } func TestDMLInsertSelect(t *testing.T) { src := "insert into t (a, b) select x, y from s where z = 1;" want := "INSERT INTO t(a, b)\nSELECT\n x\n ,y\nFROM s\nWHERE z = 1;\n" got := format(src) if got != want { t.Errorf("insert select\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "insert select", got) } func TestDMLUpdate(t *testing.T) { src := "update t set a = 1, b = 2 where id = 3;" want := "UPDATE t\nSET\n a = 1\n ,b = 2\nWHERE id = 3;\n" got := format(src) if got != want { t.Errorf("update\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "update", got) } func TestDMLUpdateSingleSet(t *testing.T) { src := "update t set a = 1 where id = 2;" want := "UPDATE t\nSET a = 1\nWHERE id = 2;\n" got := format(src) if got != want { t.Errorf("update single set\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "update single set", got) } func TestDMLDelete(t *testing.T) { src := "delete from t where id = 1;" want := "DELETE FROM t\nWHERE id = 1;\n" got := format(src) if got != want { t.Errorf("delete\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "delete", got) } func TestDMLWith(t *testing.T) { src := "with cte as (select x from y) select x from cte;" want := "WITH cte AS (\n SELECT x\n FROM y\n)\nSELECT x\nFROM cte;\n" got := format(src) if got != want { t.Errorf("with cte\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "with cte", got) } func TestDMLWithMultiple(t *testing.T) { src := "with a as (select 1 as n), b as (select n + 1 from a) select n from b;" want := "WITH\n" + " a AS (\n" + " SELECT 1 AS n\n" + " )\n" + " ,b AS (\n" + " SELECT n + 1\n" + " FROM a\n" + " )\n" + "SELECT n\n" + "FROM b;\n" got := format(src) if got != want { t.Errorf("with multiple ctes\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "with multiple ctes", got) } func TestDMLSelectReturning(t *testing.T) { src := "insert into t (a) values (1) returning id, a;" want := "INSERT INTO t(a)\nVALUES (1)\nRETURNING\n id\n ,a;\n" got := format(src) if got != want { t.Errorf("returning\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "returning", got) } func TestDMLSelectUnion(t *testing.T) { src := "select a from t1 union select a from t2;" want := "SELECT a\nFROM t1\nUNION\nSELECT a\nFROM t2;\n" got := format(src) if got != want { t.Errorf("union\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "union", got) } func TestDMLTrailingCommaStyle(t *testing.T) { st := config.Default() st.Commas = config.CommaTrailing src := "select a, b, c from t;" got := File(parser.Parse(src), st) want := "SELECT\n a,\n b,\n c\nFROM t;\n" if got != want { t.Errorf("trailing comma\n--- got ---\n%s\n--- want ---\n%s", got, want) } } func TestDMLWithWriteable(t *testing.T) { // Writeable CTE (UPDATE ... RETURNING) should format the inner DML too. src := "with upd as (update t set a = 1 where id = 1 returning id, a) select id from upd;" want := "WITH upd AS (\n" + " UPDATE t\n" + " SET a = 1\n" + " WHERE id = 1\n" + " RETURNING\n" + " id\n" + " ,a\n" + ")\n" + "SELECT id\n" + "FROM upd;\n" got := format(src) if got != want { t.Errorf("writeable cte\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "writeable cte", got) } func TestDMLWithMultiColBody(t *testing.T) { // CTE body with multi-column SELECT should use leading-comma style. src := "with cte as (select a, b, c from t where x = 1) select a from cte;" want := "WITH cte AS (\n" + " SELECT\n" + " a\n" + " ,b\n" + " ,c\n" + " FROM t\n" + " WHERE x = 1\n" + ")\n" + "SELECT a\n" + "FROM cte;\n" got := format(src) if got != want { t.Errorf("cte multi-col body\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "cte multi-col body", got) } func TestDMLIdempotent(t *testing.T) { cases := []string{ "SELECT\n a\n ,b\nFROM t\nWHERE x = 1;\n", "UPDATE t\nSET\n a = 1\n ,b = 2\nWHERE id = 3;\n", "DELETE FROM t\nWHERE id = 1;\n", "WITH cte AS (\n SELECT x\n FROM y\n)\nSELECT x\nFROM cte;\n", } for _, src := range cases { got := format(src) if got != src { t.Errorf("not idempotent:\n--- input ---\n%s\n--- got ---\n%s", src, got) } } } func TestDMLWhereAndOr(t *testing.T) { // where_wrap=always should split AND/OR conditions onto separate lines. src := "select a from t where x = 1 and y = 2 or z = 3;" got := format(src) want := "SELECT a\nFROM t\nWHERE\n x = 1\n AND y = 2\n OR z = 3;\n" if got != want { t.Errorf("where and/or\n--- got ---\n%s\n--- want ---\n%s", got, want) } checkDML(t, "where and/or", got) } func TestDMLIndentJoin(t *testing.T) { st := config.Default() st.IndentJoin = true src := "select a from t join s on t.id = s.id;" got := File(parser.Parse(src), st) want := "SELECT a\nFROM t\n JOIN s ON t.id = s.id;\n" if got != want { t.Errorf("indent join\n--- got ---\n%s\n--- want ---\n%s", got, want) } // Idempotence with same config. twice := File(parser.Parse(got), st) if twice != got { t.Errorf("indent join not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice) } } func TestDMLSetAlignEqual(t *testing.T) { st := config.Default() st.SetAlignEqual = true src := "update t set a = 1, bb = 2, ccc = 3 where id = 1;" got := File(parser.Parse(src), st) // All = signs should align. if got == "" { t.Error("empty output") } // Idempotence. twice := File(parser.Parse(got), st) if twice != got { t.Errorf("set_align_equal not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice) } } func TestDMLAlignParamTypes(t *testing.T) { src := "create function f(in p_name text, in p_long_name integer, out p_result boolean) returns void language sql as $$ select 1 $$;" got := format(src) // p_name and p_long_name should have aligned types. if got == "" { t.Error("empty output") } twice := format(got) if twice != got { t.Errorf("align_param_types not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice) } } func TestCorpusUnaffectedByDML(t *testing.T) { // Verify the corpus (which contains only CREATE FUNCTION) is not affected // by the new DML formatting path. src := "create function f(a int) returns void language sql as $$ select 1 $$;" once := format(src) twice := format(once) if once != twice { t.Errorf("create function not idempotent after DML change") } if !semanticallyEqual(src, once) { t.Errorf("create function: DML formatter changed semantics") } }