TestSQL_Idempotency verifies that parse(sql) → .SQL() → parse → .SQL() produces stable output: the second serialization must match the first. This ensures the AST→SQL roundtrip is idempotent.
(t *testing.T)
| 24 | // stable output: the second serialization must match the first. This ensures the |
| 25 | // AST→SQL roundtrip is idempotent. |
| 26 | func TestSQL_Idempotency(t *testing.T) { |
| 27 | tests := []struct { |
| 28 | name string |
| 29 | sql string |
| 30 | }{ |
| 31 | {"simple select", "SELECT * FROM users"}, |
| 32 | {"select with where", "SELECT id, name FROM users WHERE active = TRUE"}, |
| 33 | {"select distinct", "SELECT DISTINCT status FROM orders"}, |
| 34 | {"select with join", "SELECT u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id"}, |
| 35 | {"select with order limit offset", "SELECT * FROM products ORDER BY price DESC LIMIT 10 OFFSET 5"}, |
| 36 | {"select with group by having", "SELECT dept, COUNT(*) FROM employees GROUP BY dept HAVING COUNT(*) > 5"}, |
| 37 | {"insert values", "INSERT INTO users (name, email) VALUES ('Alice', 'a@b.com')"}, |
| 38 | {"insert multi-row", "INSERT INTO t (a) VALUES ('x'), ('y')"}, |
| 39 | {"update with where", "UPDATE users SET name = 'Bob' WHERE id = 1"}, |
| 40 | {"delete with where", "DELETE FROM users WHERE id = 1"}, |
| 41 | {"create table", "CREATE TABLE users (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL)"}, |
| 42 | {"drop table", "DROP TABLE IF EXISTS users CASCADE"}, |
| 43 | {"CTE", "WITH cte AS (SELECT * FROM t) SELECT * FROM cte"}, |
| 44 | {"union all", "SELECT id FROM a UNION ALL SELECT id FROM b"}, |
| 45 | {"between", "SELECT * FROM t WHERE x BETWEEN 1 AND 10"}, |
| 46 | {"in list", "SELECT * FROM t WHERE status IN ('a', 'b')"}, |
| 47 | {"case expression", "SELECT CASE WHEN x > 0 THEN 'pos' ELSE 'neg' END FROM t"}, |
| 48 | {"cast", "SELECT CAST(price AS INTEGER) FROM t"}, |
| 49 | {"exists subquery", "SELECT * FROM t WHERE EXISTS (SELECT 1 FROM u)"}, |
| 50 | {"window function", "SELECT ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) FROM t"}, |
| 51 | {"insert on conflict", "INSERT INTO t (a) VALUES (1) ON CONFLICT (a) DO NOTHING"}, |
| 52 | {"null check", "SELECT * FROM t WHERE email IS NULL"}, |
| 53 | } |
| 54 | |
| 55 | for _, tt := range tests { |
| 56 | t.Run(tt.name, func(t *testing.T) { |
| 57 | // First parse |
| 58 | ast1, err := gosqlx.Parse(tt.sql) |
| 59 | if err != nil { |
| 60 | t.Fatalf("first parse failed: %v", err) |
| 61 | } |
| 62 | sql1 := ast1.SQL() |
| 63 | |
| 64 | // Second parse from serialized output |
| 65 | ast2, err := gosqlx.Parse(sql1) |
| 66 | if err != nil { |
| 67 | t.Fatalf("second parse failed (from %q): %v", sql1, err) |
| 68 | } |
| 69 | sql2 := ast2.SQL() |
| 70 | |
| 71 | // The two serializations must match (idempotency) |
| 72 | if sql1 != sql2 { |
| 73 | t.Errorf("idempotency failure:\n input: %s\n pass1: %s\n pass2: %s", tt.sql, sql1, sql2) |
| 74 | } |
| 75 | }) |
| 76 | } |
| 77 | } |