Major changes since last commit: - FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS - 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.) - @byref pass-by-reference via RefCell pattern - Mutable closure capture (EnsureLocalRef + RefCell sharing) - RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8) - DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display) - Reserved word guard (39 keywords blocked from function calls) - AEval arg order fix (element before index) - Closure capture redecl fix (unique _cap_ names per block) - Hash/string indexing in ArrayPush/ArrayPop - Harbour compat test suite: 51/51 - 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99 lines
4.5 KiB
Plaintext
99 lines
4.5 KiB
Plaintext
/*
|
|
* 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
|