/* * test_parser_cmp.prg — Compare AST output: TSqlParser vs TSqlParser2 (Pratt) * * Runs identical SQL through both parsers and verifies the AST hashes * are structurally identical. * * Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun) * All rights reserved. */ #include "FiveSqlDef.ch" #include "hbclass.ch" STATIC s_nPass := 0 STATIC s_nFail := 0 PROCEDURE Main() ? "================================================================" ? " TSqlParser vs TSqlParser2 (Pratt) — AST Comparison" ? "================================================================" ? Cmp( "Simple SELECT", "SELECT name, salary FROM employees WHERE salary > 5000" ) Cmp( "SELECT *", "SELECT * FROM employees" ) Cmp( "JOIN", "SELECT e.name, o.product FROM employees e JOIN orders o ON e.id = o.emp_id" ) Cmp( "LEFT JOIN", "SELECT e.name, o.product FROM employees e LEFT JOIN orders o ON e.id = o.emp_id" ) Cmp( "GROUP BY", "SELECT dept, COUNT(*) AS cnt FROM employees GROUP BY dept" ) Cmp( "HAVING", "SELECT dept, AVG(salary) AS avg_sal FROM employees GROUP BY dept HAVING AVG(salary) > 6000" ) Cmp( "ORDER BY DESC", "SELECT name, salary FROM employees ORDER BY salary DESC, name ASC" ) Cmp( "DISTINCT + TOP", "SELECT DISTINCT TOP 5 dept FROM employees" ) Cmp( "Subquery IN", "SELECT name FROM employees WHERE id IN (SELECT emp_id FROM orders)" ) Cmp( "Subquery NOT IN", "SELECT name FROM employees WHERE id NOT IN (1, 2, 3)" ) Cmp( "BETWEEN", "SELECT name FROM employees WHERE salary BETWEEN 5000 AND 8000" ) Cmp( "NOT BETWEEN", "SELECT name FROM employees WHERE salary NOT BETWEEN 1000 AND 2000" ) Cmp( "LIKE ESCAPE", "SELECT name FROM products WHERE name LIKE '%10!%%' ESCAPE '!'" ) Cmp( "IS NULL", "SELECT name FROM employees WHERE mgr_id IS NULL" ) Cmp( "IS NOT NULL", "SELECT name FROM employees WHERE mgr_id IS NOT NULL" ) Cmp( "OR + AND", "SELECT * FROM employees WHERE dept = 'Sales' OR (salary > 7000 AND mgr_id = 0)" ) Cmp( "NOT", "SELECT * FROM employees WHERE NOT (salary < 5000)" ) Cmp( "Arithmetic", "SELECT name, salary * 12 + 1000 AS annual FROM employees" ) Cmp( "Unary minus", "SELECT -salary FROM employees" ) Cmp( "String concat", "SELECT name || ' - ' || dept FROM employees" ) Cmp( "CASE WHEN", "SELECT name, CASE WHEN salary > 7000 THEN 'High' WHEN salary > 5000 THEN 'Mid' ELSE 'Low' END AS tier FROM employees" ) Cmp( "Nested function", "SELECT UPPER(SUBSTR(name, 1, 3)) FROM employees" ) Cmp( "COUNT(*)", "SELECT COUNT(*) FROM employees" ) Cmp( "Window ROW_NUMBER", "SELECT name, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn FROM employees" ) Cmp( "Window PARTITION", "SELECT name, RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk FROM employees" ) Cmp( "UNION ALL", "SELECT name FROM employees WHERE dept = 'Sales' UNION ALL SELECT name FROM employees WHERE dept = 'HR'" ) Cmp( "INSERT", "INSERT INTO employees (id, name) VALUES (99, 'Test')" ) Cmp( "UPDATE", "UPDATE employees SET salary = 9999 WHERE id = 1" ) Cmp( "DELETE", "DELETE FROM employees WHERE id = 99" ) Cmp( "CTE simple", "WITH top_e AS (SELECT name FROM employees WHERE salary > 7000) SELECT * FROM top_e" ) ? ? "================================================================" ? " Pass: " + hb_ntos( s_nPass ) + "/" + hb_ntos( s_nPass + s_nFail ) IF s_nFail == 0 ? " ALL AST OUTPUTS IDENTICAL" ENDIF ? "================================================================" RETURN STATIC FUNCTION Cmp( cLabel, cSQL ) LOCAL oLex, aTokens, oP1, oP2, h1, h2 LOCAL cAST1, cAST2 /* Tokenize once */ oLex := TSqlLexer():New( cSQL ) oLex:Tokenize() aTokens := oLex:GetTokens() /* Parse with TSqlParser (original) */ oP1 := TSqlParser():New( AClone( aTokens ), {} ) h1 := oP1:Parse() /* Parse with TSqlParser2 (Pratt) */ oP2 := TSqlParser2():New( AClone( aTokens ), {} ) h2 := oP2:Parse() /* Serialize both ASTs for comparison */ cAST1 := hb_ValToExp( h1 ) cAST2 := hb_ValToExp( h2 ) IF cAST1 == cAST2 s_nPass++ ? " PASS: " + cLabel ELSE s_nFail++ ? " FAIL: " + cLabel ? " P1: " + Left( cAST1, 120 ) ? " P2: " + Left( cAST2, 120 ) ENDIF RETURN NIL