Files
five/_FiveSql2/test/test_sql1999.prg
Charles KWON OhJun 486e466592 feat: FiveSql2 43/43, @byref, mutable closure, RTL 479, DateTime fix
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>
2026-04-11 11:35:37 +09:00

873 lines
32 KiB
Plaintext

/*
* test_sql1999.prg -- SQL:1999/2003 Comprehensive Feature Test Suite
*
* Tests ALL SQL:1999 and SQL:2003 features implemented in FiveSql:
* - WITH (CTE) non-recursive
* - WITH RECURSIVE
* - Window Functions (ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM/AVG/COUNT OVER)
* - SQL-92 Full Features (LIKE ESCAPE, SAVEPOINT, TRUNCATE, CHECK, UNIQUE, FK)
* - MERGE / UPSERT
* - Combined Advanced Queries
*
* FiveSql -- SQL Engine for Harbour DBF/NTX
*
* Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun)
* Email: charleskwonohjun@gmail.com
*
* All rights reserved.
*/
#include "dbstruct.ch"
STATIC s_nPass := 0
STATIC s_nFail := 0
STATIC s_nTotal := 0
PROCEDURE Main()
ErrorBlock( {|e| QOut( "TRAP: " + e:description + " " + e:operation ), Break(e) } )
? "================================================================"
? " FiveSql SQL:1999/2003 Comprehensive Feature Test Suite"
? "================================================================"
?
BEGIN SEQUENCE
SetupData()
RECOVER
? "FATAL: SetupData() failed -- aborting"
QUIT
END SEQUENCE
? "--- Section 1: WITH (CTE) Non-Recursive ---"
TestCTE()
? ""
? "--- Section 2: WITH RECURSIVE ---"
TestRecursiveCTE()
? ""
? "--- Section 3: Window Functions ---"
TestWindowFunctions()
? ""
? "--- Section 4: SQL-92 Full Features ---"
TestSQL92Full()
? ""
? "--- Section 5: MERGE / UPSERT ---"
TestMerge()
? ""
? "--- Section 6: Combined Advanced Queries ---"
TestCombined()
BEGIN SEQUENCE
CleanupData()
RECOVER
? " (cleanup encountered errors, continuing)"
END SEQUENCE
? ""
? "================================================================"
? " RESULTS"
? " Pass: " + hb_ntos( s_nPass )
? " Fail: " + hb_ntos( s_nFail )
? " Total: " + hb_ntos( s_nTotal )
? " Rate: " + hb_ntos( Int( s_nPass * 100 / Max( s_nTotal, 1 ) ) ) + "%"
? "================================================================"
RETURN
/* ====================================================================== */
/* Assertion helper */
/* ====================================================================== */
STATIC FUNCTION Assert( cLabel, lOK )
s_nTotal++
IF lOK
s_nPass++
? " PASS: " + cLabel
ELSE
s_nFail++
? " FAIL: " + cLabel
ENDIF
RETURN NIL
/* ====================================================================== */
/* Safe query wrapper -- returns error result on RECOVER */
/* ====================================================================== */
STATIC FUNCTION Try( cSQL )
LOCAL aR
BEGIN SEQUENCE
aR := five_SQL( cSQL )
RECOVER
aR := { { "__error__" }, {} }
END SEQUENCE
RETURN aR
STATIC FUNCTION Rows( aR )
IF ValType( aR ) == "A" .AND. Len( aR ) >= 2
RETURN Len( aR[ 2 ] )
ENDIF
RETURN 0
STATIC FUNCTION Val1( aR )
IF ValType( aR ) == "A" .AND. Len( aR ) >= 2 .AND. Len( aR[ 2 ] ) > 0 .AND. Len( aR[ 2 ][ 1 ] ) > 0
RETURN aR[ 2 ][ 1 ][ 1 ]
ENDIF
RETURN NIL
STATIC FUNCTION IsErr( aR )
IF ValType( aR ) == "A" .AND. Len( aR ) >= 1 .AND. Len( aR[ 1 ] ) > 0
RETURN aR[ 1 ][ 1 ] == "__error__"
ENDIF
RETURN .F.
STATIC FUNCTION CellVal( aR, nRow, nCol )
IF ValType( aR ) == "A" .AND. Len( aR ) >= 2 .AND. ;
nRow <= Len( aR[ 2 ] ) .AND. nCol <= Len( aR[ 2 ][ nRow ] )
RETURN aR[ 2 ][ nRow ][ nCol ]
ENDIF
RETURN NIL
STATIC FUNCTION ColName( aR, nCol )
IF ValType( aR ) == "A" .AND. Len( aR ) >= 1 .AND. nCol <= Len( aR[ 1 ] )
RETURN Upper( AllTrim( aR[ 1 ][ nCol ] ) )
ENDIF
RETURN ""
/* ====================================================================== */
/* Setup: Create employees, orders, products tables */
/* ====================================================================== */
STATIC FUNCTION SetupData()
LOCAL aStruct
/* ---- employees table (10 rows) ---- */
aStruct := { ;
{ "ID", "N", 10, 0 }, ;
{ "NAME", "C", 30, 0 }, ;
{ "DEPT", "C", 20, 0 }, ;
{ "SALARY", "N", 12, 2 }, ;
{ "MGR_ID", "N", 10, 0 } ;
}
IF hb_FileExists( "employees.dbf" )
FErase( "employees.dbf" )
ENDIF
dbCreate( "employees.dbf", aStruct )
USE employees.dbf NEW EXCLUSIVE
/* id, name, dept, salary, mgr_id (0=NULL) */
dbAppend() ; FieldPut(1, 1) ; FieldPut(2, "Alice") ; FieldPut(3, "Engineering") ; FieldPut(4, 8000) ; FieldPut(5, 0)
dbAppend() ; FieldPut(1, 2) ; FieldPut(2, "Bob") ; FieldPut(3, "Engineering") ; FieldPut(4, 7000) ; FieldPut(5, 1)
dbAppend() ; FieldPut(1, 3) ; FieldPut(2, "Charlie") ; FieldPut(3, "Engineering") ; FieldPut(4, 6000) ; FieldPut(5, 1)
dbAppend() ; FieldPut(1, 4) ; FieldPut(2, "Diana") ; FieldPut(3, "Sales") ; FieldPut(4, 7500) ; FieldPut(5, 0)
dbAppend() ; FieldPut(1, 5) ; FieldPut(2, "Eve") ; FieldPut(3, "Sales") ; FieldPut(4, 5000) ; FieldPut(5, 4)
dbAppend() ; FieldPut(1, 6) ; FieldPut(2, "Frank") ; FieldPut(3, "Sales") ; FieldPut(4, 4500) ; FieldPut(5, 4)
dbAppend() ; FieldPut(1, 7) ; FieldPut(2, "Grace") ; FieldPut(3, "Marketing") ; FieldPut(4, 6500) ; FieldPut(5, 0)
dbAppend() ; FieldPut(1, 8) ; FieldPut(2, "Henry") ; FieldPut(3, "Marketing") ; FieldPut(4, 5500) ; FieldPut(5, 7)
dbAppend() ; FieldPut(1, 9) ; FieldPut(2, "Ivy") ; FieldPut(3, "HR") ; FieldPut(4, 6000) ; FieldPut(5, 0)
dbAppend() ; FieldPut(1, 10) ; FieldPut(2, "Jack") ; FieldPut(3, "HR") ; FieldPut(4, 5000) ; FieldPut(5, 9)
dbCommit()
CLOSE employees
/* ---- orders table (15 rows) ---- */
aStruct := { ;
{ "ID", "N", 10, 0 }, ;
{ "EMP_ID", "N", 10, 0 }, ;
{ "PRODUCT", "C", 30, 0 }, ;
{ "AMOUNT", "N", 12, 2 }, ;
{ "ORDER_DATE", "C", 10, 0 } ;
}
IF hb_FileExists( "orders.dbf" )
FErase( "orders.dbf" )
ENDIF
dbCreate( "orders.dbf", aStruct )
USE orders.dbf NEW EXCLUSIVE
dbAppend() ; FieldPut(1, 1) ; FieldPut(2, 1) ; FieldPut(3, "Laptop") ; FieldPut(4, 2500) ; FieldPut(5, "2024-01-15")
dbAppend() ; FieldPut(1, 2) ; FieldPut(2, 1) ; FieldPut(3, "Monitor") ; FieldPut(4, 800) ; FieldPut(5, "2024-03-20")
dbAppend() ; FieldPut(1, 3) ; FieldPut(2, 2) ; FieldPut(3, "Keyboard") ; FieldPut(4, 150) ; FieldPut(5, "2024-02-10")
dbAppend() ; FieldPut(1, 4) ; FieldPut(2, 3) ; FieldPut(3, "Mouse") ; FieldPut(4, 100) ; FieldPut(5, "2024-04-05")
dbAppend() ; FieldPut(1, 5) ; FieldPut(2, 4) ; FieldPut(3, "Printer") ; FieldPut(4, 1200) ; FieldPut(5, "2024-05-12")
dbAppend() ; FieldPut(1, 6) ; FieldPut(2, 4) ; FieldPut(3, "Scanner") ; FieldPut(4, 500) ; FieldPut(5, "2024-06-18")
dbAppend() ; FieldPut(1, 7) ; FieldPut(2, 5) ; FieldPut(3, "Tablet") ; FieldPut(4, 900) ; FieldPut(5, "2024-07-22")
dbAppend() ; FieldPut(1, 8) ; FieldPut(2, 6) ; FieldPut(3, "Phone") ; FieldPut(4, 1100) ; FieldPut(5, "2024-08-30")
dbAppend() ; FieldPut(1, 9) ; FieldPut(2, 7) ; FieldPut(3, "Camera") ; FieldPut(4, 3000) ; FieldPut(5, "2024-09-05")
dbAppend() ; FieldPut(1, 10) ; FieldPut(2, 7) ; FieldPut(3, "Lens") ; FieldPut(4, 1500) ; FieldPut(5, "2024-10-14")
dbAppend() ; FieldPut(1, 11) ; FieldPut(2, 8) ; FieldPut(3, "Headset") ; FieldPut(4, 250) ; FieldPut(5, "2024-11-01")
dbAppend() ; FieldPut(1, 12) ; FieldPut(2, 9) ; FieldPut(3, "Desk") ; FieldPut(4, 800) ; FieldPut(5, "2025-01-10")
dbAppend() ; FieldPut(1, 13) ; FieldPut(2, 9) ; FieldPut(3, "Chair") ; FieldPut(4, 600) ; FieldPut(5, "2025-02-15")
dbAppend() ; FieldPut(1, 14) ; FieldPut(2, 10); FieldPut(3, "Lamp") ; FieldPut(4, 200) ; FieldPut(5, "2025-03-20")
dbAppend() ; FieldPut(1, 15) ; FieldPut(2, 2) ; FieldPut(3, "Webcam") ; FieldPut(4, 350) ; FieldPut(5, "2025-04-01")
dbCommit()
CLOSE orders
/* ---- products table (6 rows) ---- */
aStruct := { ;
{ "ID", "N", 10, 0 }, ;
{ "NAME", "C", 40, 0 }, ;
{ "CATEGORY", "C", 20, 0 }, ;
{ "PRICE", "N", 12, 2 } ;
}
IF hb_FileExists( "products.dbf" )
FErase( "products.dbf" )
ENDIF
dbCreate( "products.dbf", aStruct )
USE products.dbf NEW EXCLUSIVE
dbAppend() ; FieldPut(1, 1) ; FieldPut(2, "Widget A") ; FieldPut(3, "Hardware") ; FieldPut(4, 29.99)
dbAppend() ; FieldPut(1, 2) ; FieldPut(2, "Widget B") ; FieldPut(3, "Hardware") ; FieldPut(4, 49.99)
dbAppend() ; FieldPut(1, 3) ; FieldPut(2, "Software Pro") ; FieldPut(3, "Software") ; FieldPut(4, 199.99)
dbAppend() ; FieldPut(1, 4) ; FieldPut(2, "10% Off Special") ; FieldPut(3, "Discount") ; FieldPut(4, 9.99)
dbAppend() ; FieldPut(1, 5) ; FieldPut(2, "Service Plan") ; FieldPut(3, "Service") ; FieldPut(4, 99.99)
dbAppend() ; FieldPut(1, 6) ; FieldPut(2, "Gadget X") ; FieldPut(3, "Hardware") ; FieldPut(4, 149.99)
dbCommit()
CLOSE products
RETURN NIL
/* ====================================================================== */
/* Cleanup: Remove all test tables */
/* ====================================================================== */
STATIC FUNCTION CleanupData()
LOCAL aFiles, i
aFiles := { ;
"employees.dbf", "employees.ntx", ;
"orders.dbf", "orders.ntx", ;
"products.dbf", "products.ntx", ;
"mergetgt.dbf", "mergetgt.ntx", ;
"mergesrc.dbf", "mergesrc.ntx", ;
"trunc_test.dbf", "trunc_test.ntx", ;
"cktbl.dbf", "cktbl.fsc", "cktbl.ntx", ;
"uqtbl.dbf", "uqtbl.fsc", "uqtbl.ntx", "uqtbl_uq.ntx", ;
"fk_parent.dbf", "fk_parent.ntx", ;
"fk_child.dbf", "fk_child.fsc", "fk_child.ntx", ;
"target_tbl.dbf", "target_tbl.ntx", ;
"source_tbl.dbf", "source_tbl.ntx", ;
"temp_data.dbf", "temp_data.ntx" ;
}
FOR i := 1 TO Len( aFiles )
IF hb_FileExists( aFiles[ i ] )
FErase( aFiles[ i ] )
ENDIF
NEXT
/* Also erase CTE temp files */
FErase( "__cte_high_earners.dbf" )
FErase( "__cte_dept_stats.dbf" )
FErase( "__cte_eng.dbf" )
FErase( "__cte_sales.dbf" )
FErase( "__cte_top_emps.dbf" )
FErase( "__cte_active.dbf" )
FErase( "__cte_dept_avg.dbf" )
FErase( "__cte_nums.dbf" )
FErase( "__cte_powers.dbf" )
FErase( "__cte_fib.dbf" )
FErase( "__cte_org.dbf" )
FErase( "__cte_ranked.dbf" )
FErase( "__cte_order_totals.dbf" )
FErase( "__cte_dept_summary.dbf" )
FErase( "__cte_hier.dbf" )
RETURN NIL
/* ====================================================================== */
/* Section 1: WITH (CTE) -- Non-Recursive (6 tests) */
/* ====================================================================== */
STATIC PROCEDURE TestCTE()
LOCAL aR
/* 1a: Simple CTE */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH high_earners AS (SELECT * FROM employees WHERE salary > 6000) " + ;
"SELECT name, salary FROM high_earners ORDER BY salary DESC" )
Assert( "1a CTE simple: high earners (salary>6000)", ;
Rows( aR ) == 4 .AND. ;
CellVal( aR, 1, 2 ) >= CellVal( aR, 2, 2 ) )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 1a CTE simple (exception)"
END SEQUENCE
/* 1b: CTE with aggregation */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH dept_stats AS (" + ;
"SELECT dept, COUNT(*) AS cnt, AVG(salary) AS avg_sal FROM employees GROUP BY dept" + ;
") SELECT dept, cnt, avg_sal FROM dept_stats WHERE cnt > 1" )
Assert( "1b CTE with aggregation: dept stats cnt>1", ;
Rows( aR ) >= 2 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 1b CTE with aggregation (exception)"
END SEQUENCE
/* 1c: Multiple CTEs */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH eng AS (SELECT * FROM employees WHERE dept = 'Engineering'), " + ;
"sales AS (SELECT * FROM employees WHERE dept = 'Sales') " + ;
"SELECT name FROM eng UNION ALL SELECT name FROM sales" )
Assert( "1c Multiple CTEs: eng + sales UNION ALL", ;
Rows( aR ) == 6 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 1c Multiple CTEs (exception)"
END SEQUENCE
/* 1d: CTE referenced in JOIN */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH top_emps AS (SELECT * FROM employees WHERE salary > 6000) " + ;
"SELECT t.name, o.product FROM top_emps t JOIN orders o ON t.id = o.emp_id" )
Assert( "1d CTE in JOIN: top_emps JOIN orders", ;
Rows( aR ) >= 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 1d CTE in JOIN (exception)"
END SEQUENCE
/* 1e: CTE with subquery */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH active AS (SELECT * FROM employees WHERE id IN (SELECT emp_id FROM orders)) " + ;
"SELECT name, dept FROM active" )
Assert( "1e CTE with subquery: active employees", ;
Rows( aR ) >= 5 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 1e CTE with subquery (exception)"
END SEQUENCE
/* 1f: CTE in WHERE comparison */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH dept_avg AS (SELECT dept, AVG(salary) AS avg_sal FROM employees GROUP BY dept) " + ;
"SELECT e.name, e.salary, d.avg_sal FROM employees e " + ;
"JOIN dept_avg d ON e.dept = d.dept WHERE e.salary > d.avg_sal" )
Assert( "1f CTE + WHERE: salary > dept average", ;
! IsErr( aR ) .AND. Rows( aR ) >= 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 1f CTE + WHERE comparison (exception)"
END SEQUENCE
RETURN
/* ====================================================================== */
/* Section 2: WITH RECURSIVE (4 tests) */
/* ====================================================================== */
STATIC PROCEDURE TestRecursiveCTE()
LOCAL aR
/* 2a: Generate sequence 1-10 */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH RECURSIVE nums AS (" + ;
"SELECT 1 AS n " + ;
"UNION ALL " + ;
"SELECT n + 1 FROM nums WHERE n < 10" + ;
") SELECT * FROM nums" )
Assert( "2a RECURSIVE: sequence 1-10", ;
Rows( aR ) == 10 .AND. ;
CellVal( aR, 1, 1 ) == 1 .AND. CellVal( aR, 10, 1 ) == 10 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 2a RECURSIVE sequence (exception)"
END SEQUENCE
/* 2b: Generate powers of 2 */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH RECURSIVE powers AS (" + ;
"SELECT 1 AS n, 1 AS val " + ;
"UNION ALL " + ;
"SELECT n + 1, val * 2 FROM powers WHERE n < 8" + ;
") SELECT n, val FROM powers" )
Assert( "2b RECURSIVE: powers of 2", ;
Rows( aR ) == 8 .AND. ;
CellVal( aR, 1, 2 ) == 1 .AND. CellVal( aR, 8, 2 ) == 128 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 2b RECURSIVE powers of 2 (exception)"
END SEQUENCE
/* 2c: Fibonacci-like */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH RECURSIVE fib AS (" + ;
"SELECT 1 AS n, 1 AS a, 0 AS b " + ;
"UNION ALL " + ;
"SELECT n + 1, a + b, a FROM fib WHERE n < 8" + ;
") SELECT n, a FROM fib" )
Assert( "2c RECURSIVE: fibonacci-like sequence", ;
Rows( aR ) == 8 .AND. ;
CellVal( aR, 1, 2 ) == 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 2c RECURSIVE fibonacci (exception)"
END SEQUENCE
/* 2d: Org hierarchy traversal */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH RECURSIVE org AS (" + ;
"SELECT id, name FROM employees WHERE mgr_id = 0 " + ;
"UNION ALL " + ;
"SELECT e.id, e.name FROM employees e " + ;
"JOIN org o ON e.mgr_id = o.id" + ;
") SELECT * FROM org" )
Assert( "2d RECURSIVE: org hierarchy traversal", ;
Rows( aR ) == 10 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 2d RECURSIVE org hierarchy (exception)"
END SEQUENCE
RETURN
/* ====================================================================== */
/* Section 3: Window Functions (12 tests) */
/* ====================================================================== */
STATIC PROCEDURE TestWindowFunctions()
LOCAL aR
/* 3a: ROW_NUMBER() basic */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rank FROM employees" )
Assert( "3a ROW_NUMBER() basic", ;
Rows( aR ) == 10 .AND. CellVal( aR, 1, 3 ) == 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3a ROW_NUMBER() basic (exception)"
END SEQUENCE
/* 3b: ROW_NUMBER() with PARTITION BY */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, dept, salary, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS dept_rank FROM employees" )
Assert( "3b ROW_NUMBER() PARTITION BY dept", ;
Rows( aR ) == 10 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3b ROW_NUMBER() PARTITION BY (exception)"
END SEQUENCE
/* 3c: RANK() with ties */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rank FROM employees" )
Assert( "3c RANK() with ties", ;
Rows( aR ) == 10 .AND. CellVal( aR, 1, 3 ) == 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3c RANK() with ties (exception)"
END SEQUENCE
/* 3d: DENSE_RANK() */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS drank FROM employees" )
Assert( "3d DENSE_RANK() no gaps", ;
Rows( aR ) == 10 .AND. CellVal( aR, 1, 3 ) == 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3d DENSE_RANK() (exception)"
END SEQUENCE
/* 3e: LAG() */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, salary, LAG(salary, 1) OVER (ORDER BY salary DESC) AS prev_salary FROM employees" )
Assert( "3e LAG() previous salary", ;
Rows( aR ) == 10 .AND. CellVal( aR, 1, 3 ) == NIL )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3e LAG() (exception)"
END SEQUENCE
/* 3f: LEAD() */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, salary, LEAD(salary, 1) OVER (ORDER BY salary DESC) AS next_salary FROM employees" )
Assert( "3f LEAD() next salary", ;
Rows( aR ) == 10 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3f LEAD() (exception)"
END SEQUENCE
/* 3g: SUM() OVER PARTITION BY */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, dept, salary, SUM(salary) OVER (PARTITION BY dept) AS dept_total FROM employees" )
Assert( "3g SUM() OVER PARTITION BY: dept totals", ;
Rows( aR ) == 10 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3g SUM() OVER PARTITION BY (exception)"
END SEQUENCE
/* 3h: AVG() OVER PARTITION BY */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, dept, salary, AVG(salary) OVER (PARTITION BY dept) AS dept_avg FROM employees" )
Assert( "3h AVG() OVER PARTITION BY: dept averages", ;
Rows( aR ) == 10 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3h AVG() OVER PARTITION BY (exception)"
END SEQUENCE
/* 3i: COUNT(*) OVER PARTITION BY */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, dept, COUNT(*) OVER (PARTITION BY dept) AS dept_count FROM employees" )
Assert( "3i COUNT(*) OVER PARTITION BY: dept counts", ;
Rows( aR ) == 10 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3i COUNT(*) OVER PARTITION BY (exception)"
END SEQUENCE
/* 3j: Running SUM (cumulative) */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, dept, salary, SUM(salary) OVER (PARTITION BY dept ORDER BY salary) AS running_total FROM employees" )
Assert( "3j Running SUM: cumulative within partition", ;
Rows( aR ) == 10 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3j Running SUM (exception)"
END SEQUENCE
/* 3k: Multiple window functions in same query */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, salary, " + ;
"ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn, " + ;
"RANK() OVER (ORDER BY salary DESC) AS rnk, " + ;
"SUM(salary) OVER () AS total " + ;
"FROM employees" )
Assert( "3k Multiple window funcs in one query", ;
Rows( aR ) == 10 .AND. CellVal( aR, 1, 3 ) == 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3k Multiple window funcs (exception)"
END SEQUENCE
/* 3l: Window function with WHERE */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, dept, salary, " + ;
"ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn " + ;
"FROM employees WHERE dept = 'Engineering'" )
Assert( "3l Window func with WHERE: Engineering only", ;
Rows( aR ) == 3 .AND. CellVal( aR, 1, 3 ) == 8000 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 3l Window func with WHERE (exception)"
END SEQUENCE
RETURN
/* ====================================================================== */
/* Section 4: SQL-92 Full Features (8 tests) */
/* ====================================================================== */
STATIC PROCEDURE TestSQL92Full()
LOCAL aR, hC, lValid, lInsertFailed
/* 4a: LIKE ESCAPE */
BEGIN SEQUENCE
aR := five_SQL( "SELECT * FROM products WHERE name LIKE '10\% Off%' ESCAPE '\'" )
Assert( "4a LIKE ESCAPE: find literal % in name", ;
Rows( aR ) == 1 .AND. ;
"10%" $ AllTrim( CellVal( aR, 1, 2 ) ) )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 4a LIKE ESCAPE (exception)"
END SEQUENCE
/* 4b: SAVEPOINT + ROLLBACK TO */
BEGIN SEQUENCE
dbCloseAll()
aR := five_SQL( "SELECT salary FROM employees WHERE id = 1" )
five_SQL( "BEGIN" )
five_SQL( "UPDATE employees SET salary = salary + 1000 WHERE id = 1" )
five_SQL( "SAVEPOINT sp1" )
five_SQL( "UPDATE employees SET salary = 0 WHERE id = 1" )
five_SQL( "ROLLBACK TO sp1" )
five_SQL( "COMMIT" )
aR := five_SQL( "SELECT salary FROM employees WHERE id = 1" )
Assert( "4b SAVEPOINT + ROLLBACK TO: salary = original + 1000", ;
CellVal( aR, 1, 1 ) == 9000 )
/* restore original */
five_SQL( "UPDATE employees SET salary = 8000 WHERE id = 1" )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 4b SAVEPOINT + ROLLBACK TO (exception)"
END SEQUENCE
/* 4c: TRUNCATE TABLE */
BEGIN SEQUENCE
five_SQL( "CREATE TABLE temp_data (id INTEGER, val CHAR(10))" )
five_SQL( "INSERT INTO temp_data (id, val) VALUES (1, 'test')" )
five_SQL( "INSERT INTO temp_data (id, val) VALUES (2, 'data')" )
aR := five_SQL( "SELECT COUNT(*) AS cnt FROM temp_data" )
Assert( "4c-pre TRUNCATE: table has rows", ;
CellVal( aR, 1, 1 ) == 2 )
five_SQL( "TRUNCATE TABLE temp_data" )
aR := five_SQL( "SELECT COUNT(*) AS cnt FROM temp_data" )
Assert( "4c TRUNCATE TABLE: table is empty", ;
CellVal( aR, 1, 1 ) == 0 )
RECOVER
s_nTotal += 2 ; s_nFail += 2 ; ? " FAIL: 4c TRUNCATE TABLE (exception)"
END SEQUENCE
/* 4d: CHECK constraint */
BEGIN SEQUENCE
aR := five_SQL( "CREATE TABLE cktbl (id INTEGER, age INTEGER, CHECK(age >= 0 AND age <= 150))" )
hC := SqlLoadConstraints( "cktbl" )
Assert( "4d CHECK constraint: metadata stored", ;
Len( hC[ "check" ] ) >= 1 )
aR := Try( "INSERT INTO cktbl (id, age) VALUES (1, 25)" )
Assert( "4d CHECK: valid insert (age=25) succeeds", ;
! IsErr( aR ) )
aR := Try( "INSERT INTO cktbl (id, age) VALUES (2, -5)" )
Assert( "4d CHECK: invalid insert (age=-5) rejected", ;
IsErr( aR ) .OR. Val1( aR ) == 0 .OR. Val1( aR ) == NIL )
RECOVER
s_nTotal += 3 ; s_nFail += 3 ; ? " FAIL: 4d CHECK constraint (exception)"
END SEQUENCE
/* 4e: UNIQUE constraint */
BEGIN SEQUENCE
aR := five_SQL( "CREATE TABLE uqtbl (id INTEGER, email CHAR(40), UNIQUE(email))" )
hC := SqlLoadConstraints( "uqtbl" )
Assert( "4e UNIQUE constraint: metadata stored", ;
Len( hC[ "unique" ] ) >= 1 )
five_SQL( "INSERT INTO uqtbl (id, email) VALUES (1, 'a@b.com')" )
USE uqtbl.dbf NEW SHARED
lValid := SqlValidateUnique( "uqtbl", "email", "b@c.com", 0 )
Assert( "4e UNIQUE: new email allowed", lValid )
lValid := SqlValidateUnique( "uqtbl", "email", "a@b.com", 0 )
Assert( "4e UNIQUE: duplicate email rejected", ! lValid )
CLOSE uqtbl
RECOVER
s_nTotal += 3 ; s_nFail += 3 ; ? " FAIL: 4e UNIQUE constraint (exception)"
END SEQUENCE
/* 4f: FOREIGN KEY */
BEGIN SEQUENCE
dbCloseAll()
five_SQL( "CREATE TABLE fk_parent (id INTEGER, PRIMARY KEY(id))" )
five_SQL( "INSERT INTO fk_parent (id) VALUES (1)" )
five_SQL( "INSERT INTO fk_parent (id) VALUES (2)" )
aR := five_SQL( "CREATE TABLE fk_child (id INTEGER, parent_id INTEGER, FOREIGN KEY(parent_id) REFERENCES fk_parent(id))" )
hC := SqlLoadConstraints( "fk_child" )
Assert( "4f FOREIGN KEY: metadata stored", ;
Len( hC[ "fk" ] ) >= 1 )
dbCloseAll()
/* Test FK via INSERT — SqlValidateFKRecord is called internally */
aR := Try( "INSERT INTO fk_child (id, parent_id) VALUES (10, 1)" )
Assert( "4f FK: valid reference (parent=1) passes", ! IsErr( aR ) )
aR := Try( "INSERT INTO fk_child (id, parent_id) VALUES (20, 999)" )
Assert( "4f FK: invalid reference (parent=999) fails", IsErr( aR ) )
RECOVER
s_nTotal += 3 ; s_nFail += 3 ; ? " FAIL: 4f FOREIGN KEY (exception)"
END SEQUENCE
RETURN
/* ====================================================================== */
/* Section 5: MERGE / UPSERT (3 tests) */
/* ====================================================================== */
STATIC PROCEDURE TestMerge()
LOCAL aR
/* Setup MERGE tables */
BEGIN SEQUENCE
five_SQL( "CREATE TABLE target_tbl (id INTEGER, name CHAR(20), val INTEGER)" )
five_SQL( "INSERT INTO target_tbl (id, name, val) VALUES (1, 'Old', 100)" )
five_SQL( "CREATE TABLE source_tbl (id INTEGER, name CHAR(20), val INTEGER)" )
five_SQL( "INSERT INTO source_tbl (id, name, val) VALUES (1, 'New', 200)" )
RECOVER
? " (MERGE setup error, continuing)"
END SEQUENCE
/* 5a: MERGE update existing */
BEGIN SEQUENCE
aR := five_SQL( ;
"MERGE INTO target_tbl USING source_tbl ON target_tbl.id = source_tbl.id " + ;
"WHEN MATCHED THEN UPDATE SET name = source_tbl.name, val = source_tbl.val " + ;
"WHEN NOT MATCHED THEN INSERT (id, name, val) VALUES (source_tbl.id, source_tbl.name, source_tbl.val)" )
aR := five_SQL( "SELECT name, val FROM target_tbl WHERE id = 1" )
Assert( "5a MERGE update existing: id=1 name=New val=200", ;
Upper( AllTrim( CellVal( aR, 1, 1 ) ) ) == "NEW" .AND. ;
CellVal( aR, 1, 2 ) == 200 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 5a MERGE update existing (exception)"
END SEQUENCE
/* 5b: MERGE insert new */
BEGIN SEQUENCE
dbCloseAll()
five_SQL( "INSERT INTO source_tbl (id, name, val) VALUES (2, 'Brand', 300)" )
aR := five_SQL( ;
"MERGE INTO target_tbl USING source_tbl ON target_tbl.id = source_tbl.id " + ;
"WHEN MATCHED THEN UPDATE SET name = source_tbl.name, val = source_tbl.val " + ;
"WHEN NOT MATCHED THEN INSERT (id, name, val) VALUES (source_tbl.id, source_tbl.name, source_tbl.val)" )
aR := five_SQL( "SELECT name FROM target_tbl WHERE id = 2" )
Assert( "5b MERGE insert new: id=2 added", ;
Rows( aR ) == 1 .AND. ;
Upper( AllTrim( CellVal( aR, 1, 1 ) ) ) == "BRAND" )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 5b MERGE insert new (exception)"
END SEQUENCE
/* 5c: MERGE mixed (update + insert) */
BEGIN SEQUENCE
dbCloseAll()
five_SQL( "INSERT INTO source_tbl (id, name, val) VALUES (3, 'Third', 400)" )
aR := five_SQL( ;
"MERGE INTO target_tbl USING source_tbl ON target_tbl.id = source_tbl.id " + ;
"WHEN MATCHED THEN UPDATE SET name = source_tbl.name, val = source_tbl.val " + ;
"WHEN NOT MATCHED THEN INSERT (id, name, val) VALUES (source_tbl.id, source_tbl.name, source_tbl.val)" )
aR := five_SQL( "SELECT COUNT(*) AS cnt FROM target_tbl" )
Assert( "5c MERGE mixed: target has 3 rows total", ;
CellVal( aR, 1, 1 ) == 3 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 5c MERGE mixed (exception)"
END SEQUENCE
RETURN
/* ====================================================================== */
/* Section 6: Combined Advanced Queries (5 tests) */
/* ====================================================================== */
STATIC PROCEDURE TestCombined()
LOCAL aR, oErr
/* Ensure clean state: close all workareas and remove stale CTE temps */
dbCloseAll()
FErase( "__cte_ranked.dbf" )
FErase( "__cte_order_totals.dbf" )
FErase( "__cte_dept_summary.dbf" )
FErase( "__cte_org.dbf" )
/* 6a: CTE + Window + JOIN: top earner per department */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH ranked AS (" + ;
"SELECT e.name, e.dept, e.salary, " + ;
"ROW_NUMBER() OVER (PARTITION BY e.dept ORDER BY e.salary DESC) AS rn " + ;
"FROM employees e" + ;
") SELECT name, dept, salary FROM ranked WHERE rn = 1" )
Assert( "6a CTE+Window: top earner per dept", ;
Rows( aR ) == 4 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 6a CTE+Window top earner (exception)"
END SEQUENCE
/* 6b: CTE + Aggregate + HAVING via WHERE */
/* Clean stale workareas from 6a to prevent alias collisions */
IF Select( "RANKED" ) > 0
dbSelectArea( "RANKED" ) ; dbCloseArea()
ENDIF
IF Select( "E" ) > 0
dbSelectArea( "E" ) ; dbCloseArea()
ENDIF
IF Select( "ORDER_TOTALS" ) > 0
dbSelectArea( "ORDER_TOTALS" ) ; dbCloseArea()
ENDIF
IF hb_FileExists( "__cte_ranked.dbf" )
FErase( "__cte_ranked.dbf" )
ENDIF
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH order_totals AS (" + ;
"SELECT emp_id, SUM(amount) AS total FROM orders GROUP BY emp_id" + ;
") SELECT e.name, t.total FROM employees e " + ;
"JOIN order_totals t ON e.id = t.emp_id " + ;
"WHERE t.total > 1000 ORDER BY t.total DESC" )
? " 6b debug: rows=" + hb_ntos( Rows( aR ) )
Assert( "6b CTE+Agg: emp order totals > 1000", ;
Rows( aR ) >= 1 )
RECOVER USING oErr
s_nTotal++ ; s_nFail++ ; ? " FAIL: 6b CTE+Agg: " + IIF( oErr != NIL, oErr:description, "no error obj" )
END SEQUENCE
/* 6c: Window + Subquery: diff from avg for active employees */
BEGIN SEQUENCE
aR := five_SQL( ;
"SELECT name, salary, " + ;
"salary - AVG(salary) OVER () AS diff_from_avg " + ;
"FROM employees " + ;
"WHERE id IN (SELECT emp_id FROM orders)" )
Assert( "6c Window+Subquery: diff from avg", ;
Rows( aR ) >= 5 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 6c Window+Subquery (exception)"
END SEQUENCE
/* 6d: RECURSIVE CTE + JOIN: org hierarchy with levels */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH RECURSIVE org AS (" + ;
"SELECT id, name, 1 AS lvl FROM employees WHERE mgr_id = 0 " + ;
"UNION ALL " + ;
"SELECT e.id, e.name, o.lvl + 1 FROM employees e JOIN org o ON e.mgr_id = o.id" + ;
") SELECT name, lvl FROM org ORDER BY lvl, name" )
Assert( "6d RECURSIVE CTE+JOIN: org levels", ;
Rows( aR ) == 10 .AND. CellVal( aR, 1, 2 ) == 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 6d RECURSIVE CTE org levels (exception)"
END SEQUENCE
/* 6e: Window over GROUP BY CTE */
BEGIN SEQUENCE
aR := five_SQL( ;
"WITH dept_summary AS (" + ;
"SELECT dept, COUNT(*) AS cnt, SUM(salary) AS total FROM employees GROUP BY dept" + ;
") SELECT dept, cnt, total, " + ;
"RANK() OVER (ORDER BY total DESC) AS rank " + ;
"FROM dept_summary" )
Assert( "6e CTE+Window: dept summary ranked by total", ;
Rows( aR ) == 4 .AND. CellVal( aR, 1, 4 ) == 1 )
RECOVER
s_nTotal++ ; s_nFail++ ; ? " FAIL: 6e CTE+Window dept summary (exception)"
END SEQUENCE
RETURN