feat(dml): implement DML statement formatting and tests
* Add formatDML function for formatting top-level DML statements * Introduce tests for various DML scenarios including SELECT, INSERT, UPDATE, and DELETE * Enhance printer to handle DML statements correctly
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user