Files
five/_FiveSql2/src/TSqlParser2.prg
CharlesKWON 7babfb7281 fix(FiveSql2): 9 latent bugs from static analysis sweep
Systematic bug-hunt driven by an automated analysis of all FiveSql2
source files. Each fix is targeted — no speculative refactoring.

--- #1 CLASSDATA hSubCache leaked across queries (CRITICAL) ---

  CLASSDATA hSubCache INIT { => } SHARED

shared one hash across ALL TSqlExecutor instances. A non-correlated
subquery cached in query A was silently returned for an unrelated
query B if the subquery text happened to produce the same cache key.
Converted to instance DATA initialized in New().

--- #5+#21 IS NULL / COALESCE treated empty string as NULL (HIGH) ---

  RETURN xL == NIL .OR. ( ValType(xL) == "C" .AND. Empty(AllTrim(xL)) )

SQL standard: '' is a valid non-NULL value. Removed the empty-string
check from both IS NULL evaluation and COALESCE skip logic.

--- #4 Multiple ? parameters all returned first value (HIGH) ---

ND_PAR nodes had no index — EvalExpr always returned ::aParams[1].
Parser now stamps each ? with a sequential 1-based index in xNode[2].
EvalExpr uses it to return the correct ::aParams[n].

--- #10+#11 SqlEvalRowExpr missing / and || operators, single-arg
    function eval (MEDIUM) ---

Division and string concatenation fell through to RETURN NIL in the
row-expression evaluator used by recursive CTEs and aggregate
ComputeAgg. Also, multi-argument functions like SUBSTR(x,2,3) only
received the first argument. Both fixed.

--- #9 SUM/AVG/MIN/MAX of all NULLs returned 0 instead of NULL
    (MEDIUM) ---

SQL standard requires NULL. Changed the aggregate return path to
return NIL when nCount == 0 (SUM/AVG) or when xMin/xMax == NIL.

--- #8 MIN/MAX used SqlCoerceNum for comparison (MEDIUM) ---

Strings and dates were coerced to numbers (Val()) before comparing,
making MIN('banana') == MIN('apple') == 0. Switched to SqlCmpLt
which handles type-appropriate comparison.

--- #7 SqlExprHasAgg only checked top-level node (MEDIUM) ---

Expressions like `salary + COUNT(*)` were not detected as containing
an aggregate because the top node was ND_BIN, not ND_FN. Made the
function recursive — walks ND_BIN, ND_UNI, ND_FN args, ND_CASE
branches.

--- #13 SELECT * only expanded first table in JOINs (MEDIUM) ---

`SELECT * FROM orders o JOIN customers c ON ...` only included
fields from orders. Changed the expansion loop to iterate ALL
entries in ::aTables.

--- #2 s_aOuterStack not unwound on subquery error (HIGH) ---

SubqueryCached's PushOuter/PopOuter pair was not protected by
BEGIN SEQUENCE. A runtime error inside the subquery left a stale
entry on the module-level outer stack, corrupting all subsequent
queries' correlated column resolution. Wrapped in SEQUENCE/RECOVER.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:26:05 +09:00

2249 lines
64 KiB
Plaintext

/*
* TSqlParser.prg — Recursive descent SQL parser
*
* FiveSql — SQL Engine for Harbour DBF/NTX
*
* Copyright (c) 2025-2026 Charles KWON (Charles KWON OhJun)
* Email: charleskwonohjun@gmail.com
*
* All rights reserved.
*/
#include "hbclass.ch"
#include "FiveSqlDef.ch"
/*
* SQL:1999-2023 Grammar (PEG notation) — Living documentation.
* When adding new SQL features, add the grammar rule here first,
* then implement the corresponding Parse* method.
*
* statement <- with_stmt / select_stmt / insert_stmt / update_stmt /
* delete_stmt / merge_stmt / create_stmt / drop_stmt /
* alter_stmt / truncate_stmt / transaction_stmt / set_stmt /
* values_stmt / table_stmt / call_stmt
*
* with_stmt <- 'WITH' 'RECURSIVE'? cte_def (',' cte_def)* 'SELECT' select_body
* cte_def <- NAME 'AS' '(' select_body ')'
*
* select_body <- 'DISTINCT'? 'TOP' NUMBER? column_list
* ('FROM' table_refs)?
* ('WHERE' expr)?
* ('GROUP' 'BY' group_by_list ('HAVING' expr)?)?
* ('WINDOW' window_def (',' window_def)*)?
* ('ORDER' 'BY' order_item (',' order_item)*)?
* ('LIMIT' NUMBER ('OFFSET' NUMBER)?)?
* ('OFFSET' NUMBER 'ROWS'? ('FETCH' 'FIRST' NUMBER 'ROWS' ('ONLY'/'WITH' 'TIES'))?)?
* ('FETCH' 'FIRST' NUMBER 'ROWS' ('ONLY'/'WITH' 'TIES'))?
* ('FOR' ('UPDATE'/'SHARE') ('OF' name_list)?)?
* (('UNION' 'ALL'? / 'INTERSECT' / 'EXCEPT') 'SELECT' select_body)?
*
* column_list <- column_item (',' column_item)*
* column_item <- ('*' / expr) ('AS' NAME)?
*
* table_refs <- table_ref (',' table_ref)* join_clause*
* table_ref <- 'LATERAL'? ('(' select_body ')' / NAME) ('AS'? NAME)?
* join_clause <- ('LEFT'/'RIGHT'/'INNER'/'FULL'/'CROSS')? 'OUTER'? 'JOIN'
* ('LATERAL'? table_ref) 'ON' expr
*
* group_by_list <- group_item (',' group_item)*
* group_item <- 'GROUPING' 'SETS' '(' group_set (',' group_set)* ')' /
* 'ROLLUP' '(' expr_list ')' /
* 'CUBE' '(' expr_list ')' /
* expr
* group_set <- '(' expr_list? ')' / expr
*
* expr <- or_expr
* or_expr <- and_expr ('OR' and_expr)*
* and_expr <- not_expr ('AND' not_expr)*
* not_expr <- 'NOT'? cmp_expr
* cmp_expr <- add_expr (cmp_op add_expr /
* 'LIKE' add_expr ('ESCAPE' add_expr)? /
* 'SIMILAR' 'TO' add_expr ('ESCAPE' add_expr)? /
* 'IS' 'DISTINCT' 'FROM' add_expr /
* 'IS' 'NOT' 'DISTINCT' 'FROM' add_expr /
* 'NOT'? 'IN' ('(' select_body ')' / '(' expr_list ')') /
* 'NOT'? 'BETWEEN' add_expr 'AND' add_expr /
* 'IS' 'NOT'? 'NULL')?
* add_expr <- mul_expr (('+'/'-'/'||') mul_expr)*
* mul_expr <- unary_expr (('*'/'/') unary_expr)*
* unary_expr <- '-'? primary
* primary <- 'NULL' / NUMBER / STRING / '?' /
* '(' (select_body / expr) ')' /
* 'EXISTS' '(' select_body ')' /
* 'CASE' ('WHEN' expr 'THEN' expr)+ ('ELSE' expr)? 'END' /
* 'CAST' '(' expr 'AS' type_name ')' /
* 'EXTRACT' '(' part 'FROM' expr ')' /
* 'TRIM' '(' ('LEADING'/'TRAILING'/'BOTH')? expr? 'FROM' expr ')' /
* 'POSITION' '(' expr 'IN' expr ')' /
* 'OVERLAY' '(' expr 'PLACING' expr 'FROM' expr ('FOR' expr)? ')' /
* 'ARRAY' '(' expr_list? ')' /
* 'ROW' '(' expr_list ')' /
* 'JSON_VALUE' '(' expr ',' expr ')' /
* 'JSON_QUERY' '(' expr ',' expr ')' /
* 'JSON_EXISTS' '(' expr ',' expr ')' /
* 'JSON_TABLE' '(' expr ',' expr 'COLUMNS' '(' col_def+ ')' ')' /
* 'JSON_OBJECT' '(' (key ':' expr (',' key ':' expr)*)? ')' /
* 'JSON_ARRAY' '(' expr_list? ')' /
* 'JSON_ARRAYAGG' '(' expr ')' /
* 'JSON_OBJECTAGG' '(' expr ':' expr ')' /
* 'XMLELEMENT' '(' 'NAME' name (',' expr)* ')' /
* 'XMLFOREST' '(' expr ('AS' name)? (',' expr ('AS' name)?)* ')' /
* 'XMLAGG' '(' expr ('ORDER' 'BY' order_item)? ')' /
* 'LISTAGG' '(' expr (',' expr)? ')' ('WITHIN' 'GROUP' '(' 'ORDER' 'BY' order_item+ ')')? /
* NAME '.' NAME / NAME '(' (('*' / expr_list))? ')' ('OVER' window)? /
* NAME
*
* window <- '(' (NAME /
* ('PARTITION' 'BY' expr_list)?
* ('ORDER' 'BY' order_item (',' order_item)*)?
* frame_clause? ) ')'
* window_def <- NAME 'AS' window
* frame_clause <- ('ROWS'/'RANGE'/'GROUPS') frame_extent
* ('EXCLUDE' ('NO' 'OTHERS' / 'CURRENT' 'ROW' / 'GROUP' / 'TIES'))?
* frame_extent <- 'BETWEEN' frame_bound 'AND' frame_bound / frame_bound
* frame_bound <- 'UNBOUNDED' 'PRECEDING' / NUMBER 'PRECEDING' /
* 'CURRENT' 'ROW' / NUMBER 'FOLLOWING' / 'UNBOUNDED' 'FOLLOWING'
*
* insert_stmt <- 'INSERT' 'INTO' NAME '(' name_list ')' 'VALUES' '(' expr_list ')'
* update_stmt <- 'UPDATE' NAME 'SET' set_list ('WHERE' expr)?
* delete_stmt <- 'DELETE' 'FROM' NAME ('WHERE' expr)?
* merge_stmt <- 'MERGE' 'INTO' NAME 'USING' NAME 'ON' expr when_clause+
* when_clause <- 'WHEN' ('MATCHED' ('AND' expr)? 'THEN' ('UPDATE' 'SET' set_list / 'DELETE') /
* 'NOT' 'MATCHED' ('AND' expr)? 'THEN' 'INSERT' '(' name_list ')' 'VALUES' '(' expr_list ')')
*
* values_stmt <- 'VALUES' row_list (standalone VALUES query, SQL:2003)
* row_list <- '(' expr_list ')' (',' '(' expr_list ')')*
*
* table_stmt <- 'TABLE' NAME (shorthand for SELECT * FROM NAME, SQL:2003)
*
* call_stmt <- 'CALL' NAME '(' expr_list? ')' (SQL:2003 procedure call)
*
* order_item <- expr ('ASC'/'DESC')? ('NULLS' ('FIRST'/'LAST'))?
* expr_list <- expr (',' expr)*
* name_list <- NAME (',' NAME)*
* set_list <- NAME '=' expr (',' NAME '=' expr)*
*/
CLASS TSqlParser2
DATA aTokens
DATA nPos
DATA aParams
DATA nParamSeq INIT 0 /* positional ? counter (1-based) */
DATA hInfixTT /* hash: token_type => { op, lbp, rbp, ndType } */
DATA hInfixKW /* hash: keyword => { op, lbp, rbp, ndType } */
METHOD New( aTokens, aParams ) CONSTRUCTOR
METHOD InitInfixTables()
METHOD Parse()
METHOD ParseSelect()
METHOD ParseInsert()
METHOD ParseUpdate()
METHOD ParseDelete()
METHOD ParseExpr()
METHOD PrattExpr( nMinBP )
METHOD PrattPrefix()
METHOD InfixBP()
METHOD ParsePrimary()
METHOD ParseSubquery()
METHOD ParseColumnList()
METHOD ParseFrom()
METHOD ParseOrderBy()
METHOD ParseExprList()
METHOD TType( n )
METHOD TVal( n )
METHOD IsKW( n, c )
METHOD EatKW( c )
METHOD IsFromKW( cVal )
METHOD ParseWindow( cFuncName, aFuncArgs )
METHOD ParseMerge()
METHOD ParseGroupingSets()
METHOD ParseWindowSpec()
METHOD ParseFrameClause()
METHOD ParseFrameBound()
ENDCLASS
METHOD New( aTokens, aParams ) CLASS TSqlParser2
::aTokens := aTokens
::nPos := 1
::aParams := iif( aParams == NIL, {}, aParams )
IF ::hInfixTT == NIL
::InitInfixTables()
ENDIF
RETURN SELF
/* Build hash-based operator lookup tables once (O(1) per lookup). */
METHOD InitInfixTables() CLASS TSqlParser2
::hInfixTT := { => }
::hInfixTT[ TK_EQ ] := { "=", 40, 41, ND_BIN }
::hInfixTT[ TK_NEQ ] := { "<>", 40, 41, ND_BIN }
::hInfixTT[ TK_LT ] := { "<", 40, 41, ND_BIN }
::hInfixTT[ TK_GT ] := { ">", 40, 41, ND_BIN }
::hInfixTT[ TK_LTE ] := { "<=", 40, 41, ND_BIN }
::hInfixTT[ TK_GTE ] := { ">=", 40, 41, ND_BIN }
::hInfixTT[ TK_PLUS ] := { "+", 50, 51, ND_BIN }
::hInfixTT[ TK_MINUS ] := { "-", 50, 51, ND_BIN }
::hInfixTT[ TK_PIPES ] := { "||", 50, 51, ND_BIN }
::hInfixTT[ TK_STAR ] := { "*", 60, 61, ND_BIN }
::hInfixTT[ TK_SLASH ] := { "/", 60, 61, ND_BIN }
::hInfixKW := { => }
::hInfixKW[ "OR" ] := { "OR", 10, 11, ND_BIN }
::hInfixKW[ "AND" ] := { "AND", 20, 21, ND_BIN }
::hInfixKW[ "LIKE" ] := { "LIKE", 40, 50, ND_BIN }
::hInfixKW[ "IN" ] := { "IN", 40, 50, ND_BIN }
::hInfixKW[ "BETWEEN" ] := { "BETWEEN", 40, 50, ND_BIN }
::hInfixKW[ "COLLATE" ] := { "COLLATE", 90, 91, ND_BIN }
RETURN NIL
/* Token type at position n */
METHOD TType( n ) CLASS TSqlParser2
IF n > 0 .AND. n <= Len( ::aTokens )
RETURN ::aTokens[ n ][ TK_TYPE ]
ENDIF
RETURN TK_END
/* Token value at position n */
METHOD TVal( n ) CLASS TSqlParser2
IF n > 0 .AND. n <= Len( ::aTokens )
RETURN ::aTokens[ n ][ TK_VALUE ]
ENDIF
RETURN ""
/* Check whether token at position n is a keyword matching c */
METHOD IsKW( n, c ) CLASS TSqlParser2
RETURN ::TType( n ) == TK_NAME .AND. ::TVal( n ) == c
/* Consume keyword c at current position; advance and return .T. on match */
METHOD EatKW( c ) CLASS TSqlParser2
IF ::IsKW( ::nPos, c )
::nPos++
RETURN .T.
ENDIF
RETURN .F.
/* Test whether a value is a SQL clause keyword that terminates a FROM list */
METHOD IsFromKW( cVal ) CLASS TSqlParser2
LOCAL aKW := { "WHERE", "ORDER", "GROUP", "HAVING", "JOIN", "LEFT", ;
"RIGHT", "INNER", "ON", "OUTER", "CROSS", "FULL", ;
"SET", "VALUES", "LIMIT", "TOP", "UNION", ;
"INTERSECT", "EXCEPT", "WITH", "FETCH", "OFFSET", ;
"WINDOW", "FOR", "LATERAL" }
RETURN AScan( aKW, {|x| x == cVal } ) > 0
/* Top-level statement dispatcher */
METHOD Parse() CLASS TSqlParser2
LOCAL cType, h
LOCAL aCTE, cName, xSub, lRecursive, aCTEColNames
IF Len( ::aTokens ) < 2
RETURN NIL
ENDIF
cType := ::TVal( ::nPos )
/* WITH (Common Table Expression), including RECURSIVE */
IF cType == "WITH"
::nPos++
aCTE := {}
lRecursive := .F.
WHILE .T.
/* Detect RECURSIVE keyword */
IF ::IsKW( ::nPos, "RECURSIVE" )
lRecursive := .T.
::nPos++
ENDIF
cName := ::TVal( ::nPos )
::nPos++
/* Parse optional column aliases: name(col1, col2, ...) */
aCTEColNames := {}
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::nPos <= Len( ::aTokens )
AAdd( aCTEColNames, ::TVal( ::nPos ) )
::nPos++
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ENDIF
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
::EatKW( "AS" )
xSub := ::ParseSubquery()
AAdd( aCTE, { cName, xSub, aCTEColNames } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
/* Parse the main SELECT statement after WITH */
::EatKW( "SELECT" )
h := ::ParseSelect()
IF h != NIL
h[ "cte" ] := aCTE
h[ "cte_recursive" ] := lRecursive
ENDIF
RETURN h
ENDIF
DO CASE
CASE cType == "SELECT"
::nPos++
RETURN ::ParseSelect()
CASE cType == "INSERT"
::nPos++
RETURN ::ParseInsert()
CASE cType == "UPDATE"
::nPos++
RETURN ::ParseUpdate()
CASE cType == "DELETE"
::nPos++
RETURN ::ParseDelete()
CASE cType == "CREATE"
h := { => }
::nPos++
h[ "type" ] := "CREATE"
h[ "tokens" ] := ::aTokens
h[ "pos" ] := ::nPos
RETURN h
CASE cType == "DROP"
h := { => }
::nPos++
h[ "type" ] := "DROP"
h[ "tokens" ] := ::aTokens
h[ "pos" ] := ::nPos
RETURN h
CASE cType == "SET"
h := { => }
::nPos++
IF ::IsKW( ::nPos, "COLLATION" )
::nPos++
::EatKW( "TO" )
h[ "type" ] := "SET_COLLATION"
h[ "value" ] := ::TVal( ::nPos )
::nPos++
ELSE
h[ "type" ] := "SET"
h[ "tokens" ] := ::aTokens
h[ "pos" ] := ::nPos
ENDIF
RETURN h
CASE cType == "ALTER"
h := { => }
::nPos++
h[ "type" ] := "ALTER"
h[ "tokens" ] := ::aTokens
h[ "pos" ] := ::nPos
RETURN h
CASE cType == "BEGIN"
h := { => }
h[ "type" ] := "BEGIN"
RETURN h
CASE cType == "COMMIT"
h := { => }
h[ "type" ] := "COMMIT"
RETURN h
CASE cType == "ROLLBACK"
h := { => }
::nPos++
IF ::IsKW( ::nPos, "TO" )
::nPos++
/* ROLLBACK TO [SAVEPOINT] name */
IF ::IsKW( ::nPos, "SAVEPOINT" )
::nPos++
ENDIF
h[ "type" ] := "ROLLBACK_TO"
h[ "savepoint" ] := ::TVal( ::nPos )
::nPos++
ELSE
h[ "type" ] := "ROLLBACK"
ENDIF
RETURN h
CASE cType == "SAVEPOINT"
h := { => }
::nPos++
h[ "type" ] := "SAVEPOINT"
h[ "name" ] := ::TVal( ::nPos )
::nPos++
RETURN h
CASE cType == "TRUNCATE"
h := { => }
::nPos++
::EatKW( "TABLE" )
h[ "type" ] := "TRUNCATE"
h[ "table" ] := ::TVal( ::nPos )
::nPos++
RETURN h
CASE cType == "MERGE"
RETURN ::ParseMerge()
/* VALUES (row), (row), ... — standalone query (SQL:2003) */
CASE cType == "VALUES"
h := { => }
h[ "type" ] := "VALUES"
::nPos++
h[ "rows" ] := {}
WHILE .T.
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
AAdd( h[ "rows" ], ::ParseExprList() )
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ELSE
EXIT
ENDIF
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
RETURN h
/* TABLE name — shorthand for SELECT * FROM name (SQL:2003) */
CASE cType == "TABLE"
h := { => }
::nPos++
h[ "type" ] := "TABLE"
h[ "table" ] := ::TVal( ::nPos )
::nPos++
RETURN h
/* CALL procedure(args) (SQL:2003) */
CASE cType == "CALL"
h := { => }
::nPos++
h[ "type" ] := "CALL"
h[ "procedure" ] := ::TVal( ::nPos )
::nPos++
h[ "args" ] := {}
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
IF ::TType( ::nPos ) != TK_RPAR
AAdd( h[ "args" ], ::ParseExpr() )
DO WHILE ::TType( ::nPos ) == TK_COMMA
::nPos++
AAdd( h[ "args" ], ::ParseExpr() )
ENDDO
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
RETURN h
ENDCASE
RETURN NIL
/* Parse SELECT statement */
METHOD ParseSelect() CLASS TSqlParser2
LOCAL h := { => }
LOCAL nTop := 0, lDistinct := .F.
LOCAL aCols, aTables := {}, aJoins := {}
LOCAL xWhere := NIL, aGroupBy := {}, xHaving := NIL, aOrderBy := {}
LOCAL nLimit := 0, nOffset := 0
LOCAL hUnion := NIL
LOCAL lAll
LOCAL aWindowDefs, cWinName, hWinDef
LOCAL nFetch, cFetchTies
LOCAL cForLock, aForCols
h[ "type" ] := "SELECT"
/* DISTINCT */
IF ::IsKW( ::nPos, "DISTINCT" )
lDistinct := .T.
::nPos++
ENDIF
h[ "distinct" ] := lDistinct
/* TOP n */
IF ::IsKW( ::nPos, "TOP" )
::nPos++
IF ::TType( ::nPos ) == TK_NUM
nTop := Val( ::TVal( ::nPos ) )
::nPos++
ENDIF
ENDIF
h[ "top" ] := nTop
/* Column list */
aCols := ::ParseColumnList()
h[ "columns" ] := aCols
/* FROM */
IF ::IsKW( ::nPos, "FROM" )
::nPos++
::ParseFrom( @aTables, @aJoins )
ENDIF
h[ "tables" ] := aTables
h[ "joins" ] := aJoins
/* WHERE */
IF ::IsKW( ::nPos, "WHERE" )
::nPos++
xWhere := ::ParseExpr()
ENDIF
h[ "where" ] := xWhere
/* GROUP BY — with GROUPING SETS / CUBE / ROLLUP support (SQL:2003) */
IF ::IsKW( ::nPos, "GROUP" )
::nPos++
::EatKW( "BY" )
aGroupBy := {}
DO WHILE ::TType( ::nPos ) != TK_END .AND. ;
! ::IsKW( ::nPos, "HAVING" ) .AND. ! ::IsKW( ::nPos, "ORDER" ) .AND. ;
! ::IsKW( ::nPos, "LIMIT" ) .AND. ! ::IsKW( ::nPos, "UNION" ) .AND. ;
! ::IsKW( ::nPos, "INTERSECT" ) .AND. ! ::IsKW( ::nPos, "EXCEPT" ) .AND. ;
! ::IsKW( ::nPos, "WINDOW" ) .AND. ! ::IsKW( ::nPos, "FETCH" ) .AND. ;
! ::IsKW( ::nPos, "OFFSET" ) .AND. ! ::IsKW( ::nPos, "FOR" )
/* GROUPING SETS (...) */
IF ::IsKW( ::nPos, "GROUPING" ) .AND. ::IsKW( ::nPos + 1, "SETS" )
::nPos += 2
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
AAdd( aGroupBy, SqlNode( ND_FN, "GROUPING SETS", ;
::ParseGroupingSets(), NIL, NIL ) )
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
/* ROLLUP (...) */
ELSEIF ::IsKW( ::nPos, "ROLLUP" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
AAdd( aGroupBy, SqlNode( ND_FN, "ROLLUP", ;
::ParseExprList(), NIL, NIL ) )
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
/* CUBE (...) */
ELSEIF ::IsKW( ::nPos, "CUBE" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
AAdd( aGroupBy, SqlNode( ND_FN, "CUBE", ;
::ParseExprList(), NIL, NIL ) )
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
ELSE
AAdd( aGroupBy, ::ParseExpr() )
ENDIF
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
h[ "group_by" ] := aGroupBy
/* HAVING */
IF ::IsKW( ::nPos, "HAVING" )
::nPos++
xHaving := ::ParseExpr()
ENDIF
h[ "having" ] := xHaving
/* WINDOW clause — named windows (SQL:2003) */
IF ::IsKW( ::nPos, "WINDOW" )
::nPos++
aWindowDefs := {}
WHILE .T.
cWinName := ::TVal( ::nPos )
::nPos++
::EatKW( "AS" )
hWinDef := ::ParseWindowSpec()
AAdd( aWindowDefs, { cWinName, hWinDef } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
h[ "window_defs" ] := aWindowDefs
ENDIF
/* ORDER BY — with NULLS FIRST/LAST support (SQL:2003) */
IF ::IsKW( ::nPos, "ORDER" )
::nPos++
::EatKW( "BY" )
aOrderBy := ::ParseOrderBy()
ENDIF
h[ "order_by" ] := aOrderBy
/* LIMIT / OFFSET (MySQL/PostgreSQL syntax) */
IF ::IsKW( ::nPos, "LIMIT" )
::nPos++
IF ::TType( ::nPos ) == TK_NUM
nLimit := Val( ::TVal( ::nPos ) )
::nPos++
ENDIF
IF ::IsKW( ::nPos, "OFFSET" )
::nPos++
IF ::TType( ::nPos ) == TK_NUM
nOffset := Val( ::TVal( ::nPos ) )
::nPos++
ENDIF
ENDIF
ENDIF
h[ "limit" ] := nLimit
h[ "offset" ] := nOffset
/* OFFSET n ROWS (SQL:2008) */
IF ::IsKW( ::nPos, "OFFSET" )
::nPos++
IF ::TType( ::nPos ) == TK_NUM
nOffset := Val( ::TVal( ::nPos ) )
::nPos++
h[ "offset" ] := nOffset
ENDIF
::EatKW( "ROWS" ) /* optional ROWS keyword */
::EatKW( "ROW" ) /* or ROW */
ENDIF
/* FETCH FIRST n ROWS ONLY / WITH TIES (SQL:2008) */
IF ::IsKW( ::nPos, "FETCH" )
::nPos++
::EatKW( "FIRST" )
IF ! ::EatKW( "NEXT" )
/* already consumed or not present */
ENDIF
nFetch := 1
IF ::TType( ::nPos ) == TK_NUM
nFetch := Val( ::TVal( ::nPos ) )
::nPos++
ENDIF
::EatKW( "ROWS" )
::EatKW( "ROW" )
cFetchTies := "ONLY"
IF ::IsKW( ::nPos, "ONLY" )
::nPos++
ELSEIF ::IsKW( ::nPos, "WITH" )
::nPos++
IF ::IsKW( ::nPos, "TIES" )
cFetchTies := "WITH TIES"
::nPos++
ENDIF
ENDIF
h[ "fetch" ] := nFetch
h[ "fetch_ties" ] := cFetchTies
ENDIF
/* FOR UPDATE / FOR SHARE (SQL:2003) */
IF ::IsKW( ::nPos, "FOR" )
::nPos++
cForLock := ""
IF ::IsKW( ::nPos, "UPDATE" )
cForLock := "UPDATE"
::nPos++
ELSEIF ::IsKW( ::nPos, "SHARE" )
cForLock := "SHARE"
::nPos++
ENDIF
aForCols := {}
IF ::IsKW( ::nPos, "OF" )
::nPos++
WHILE ::TType( ::nPos ) == TK_NAME .AND. ;
! ::IsKW( ::nPos, "UNION" ) .AND. ;
! ::IsKW( ::nPos, "INTERSECT" ) .AND. ;
! ::IsKW( ::nPos, "EXCEPT" )
AAdd( aForCols, ::TVal( ::nPos ) )
::nPos++
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
h[ "for_lock" ] := cForLock
h[ "for_lock_cols" ] := aForCols
ENDIF
/* UNION / UNION ALL / INTERSECT / EXCEPT */
IF ::IsKW( ::nPos, "UNION" )
::nPos++
lAll := .F.
IF ::IsKW( ::nPos, "ALL" )
lAll := .T.
::nPos++
ENDIF
::EatKW( "SELECT" )
hUnion := ::ParseSelect()
IF hUnion != NIL
hUnion[ "union_all" ] := lAll
ENDIF
ELSEIF ::IsKW( ::nPos, "INTERSECT" )
::nPos++
::EatKW( "SELECT" )
hUnion := ::ParseSelect()
IF hUnion != NIL
hUnion[ "set_op" ] := "INTERSECT"
ENDIF
ELSEIF ::IsKW( ::nPos, "EXCEPT" )
::nPos++
::EatKW( "SELECT" )
hUnion := ::ParseSelect()
IF hUnion != NIL
hUnion[ "set_op" ] := "EXCEPT"
ENDIF
ENDIF
h[ "union" ] := hUnion
RETURN h
/* Parse GROUPING SETS argument list: (col), (col, col), () etc. */
METHOD ParseGroupingSets() CLASS TSqlParser2
LOCAL aSets := {}, aInner
WHILE .T.
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aInner := {}
IF ::TType( ::nPos ) != TK_RPAR
aInner := ::ParseExprList()
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
AAdd( aSets, aInner )
ELSE
AAdd( aSets, { ::ParseExpr() } )
ENDIF
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
RETURN aSets
/* Parse column list */
METHOD ParseColumnList() CLASS TSqlParser2
LOCAL aCols := {}, xExpr, cAlias
DO WHILE .T.
xExpr := ::ParseExpr()
cAlias := ""
IF ::IsKW( ::nPos, "AS" )
::nPos++
cAlias := ::TVal( ::nPos )
::nPos++
ENDIF
AAdd( aCols, { xExpr, cAlias } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
RETURN aCols
/* Parse expression list (GROUP BY) */
METHOD ParseExprList() CLASS TSqlParser2
LOCAL aList := {}
DO WHILE ::TType( ::nPos ) != TK_END .AND. ;
::TType( ::nPos ) != TK_RPAR .AND. ;
! ::IsKW( ::nPos, "HAVING" ) .AND. ! ::IsKW( ::nPos, "ORDER" ) .AND. ;
! ::IsKW( ::nPos, "LIMIT" ) .AND. ! ::IsKW( ::nPos, "UNION" ) .AND. ;
! ::IsKW( ::nPos, "INTERSECT" ) .AND. ! ::IsKW( ::nPos, "EXCEPT" ) .AND. ;
! ::IsKW( ::nPos, "WINDOW" ) .AND. ! ::IsKW( ::nPos, "FETCH" ) .AND. ;
! ::IsKW( ::nPos, "OFFSET" ) .AND. ! ::IsKW( ::nPos, "FOR" )
AAdd( aList, ::ParseExpr() )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
RETURN aList
/* Parse FROM clause */
METHOD ParseFrom( aTables, aJoins ) CLASS TSqlParser2
LOCAL cTable, cAlias, cJoinType, xOnCond, xSubQ
LOCAL lLateral
/* LATERAL keyword before derived table (SQL:2003) */
lLateral := .F.
IF ::IsKW( ::nPos, "LATERAL" )
lLateral := .T.
::nPos++
ENDIF
/* Derived table: FROM (subquery) [AS] alias */
IF ::TType( ::nPos ) == TK_LPAR .AND. ::IsKW( ::nPos + 1, "SELECT" )
xSubQ := ::ParseSubquery()
cAlias := ""
IF ::IsKW( ::nPos, "AS" )
::nPos++
ENDIF
IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) )
cAlias := ::TVal( ::nPos )
::nPos++
ENDIF
IF Empty( cAlias )
cAlias := "__DRV1"
ENDIF
AAdd( aTables, { "__SUBQUERY__", cAlias, xSubQ } )
IF lLateral
ATail( aTables )[ 1 ] := "__LATERAL__"
ENDIF
ELSE
/* Primary table */
cTable := ::TVal( ::nPos )
::nPos++
cAlias := ""
IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) )
cAlias := ::TVal( ::nPos )
::nPos++
ENDIF
AAdd( aTables, { cTable, cAlias, "" } )
ENDIF
/* Additional comma-separated tables */
DO WHILE ::TType( ::nPos ) == TK_COMMA
::nPos++
/* LATERAL before a comma-joined derived table */
lLateral := .F.
IF ::IsKW( ::nPos, "LATERAL" )
lLateral := .T.
::nPos++
ENDIF
IF ::TType( ::nPos ) == TK_LPAR .AND. ::IsKW( ::nPos + 1, "SELECT" )
xSubQ := ::ParseSubquery()
cAlias := ""
IF ::IsKW( ::nPos, "AS" )
::nPos++
ENDIF
IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) )
cAlias := ::TVal( ::nPos )
::nPos++
ENDIF
IF Empty( cAlias )
cAlias := "__DRV" + hb_ntos( Len( aTables ) + 1 )
ENDIF
AAdd( aTables, { iif( lLateral, "__LATERAL__", "__SUBQUERY__" ), cAlias, xSubQ } )
ELSE
cTable := ::TVal( ::nPos ) ; ::nPos++
cAlias := ""
IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) )
cAlias := ::TVal( ::nPos ) ; ::nPos++
ENDIF
AAdd( aTables, { cTable, cAlias, "" } )
ENDIF
ENDDO
/* Explicit JOIN clauses */
DO WHILE ::IsKW( ::nPos, "JOIN" ) .OR. ::IsKW( ::nPos, "LEFT" ) .OR. ;
::IsKW( ::nPos, "RIGHT" ) .OR. ::IsKW( ::nPos, "INNER" ) .OR. ;
::IsKW( ::nPos, "CROSS" ) .OR. ::IsKW( ::nPos, "FULL" )
cJoinType := ::TVal( ::nPos )
::nPos++
IF ::IsKW( ::nPos, "OUTER" )
::nPos++
ENDIF
IF ::IsKW( ::nPos, "JOIN" )
::nPos++
ENDIF
/* LATERAL before join table (SQL:2003) */
lLateral := .F.
IF ::IsKW( ::nPos, "LATERAL" )
lLateral := .T.
::nPos++
ENDIF
cTable := ::TVal( ::nPos ) ; ::nPos++
cAlias := ""
IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) )
cAlias := ::TVal( ::nPos ) ; ::nPos++
ENDIF
AAdd( aTables, { iif( lLateral, "__LATERAL_" + cTable, cTable ), cAlias, "" } )
xOnCond := NIL
IF ::IsKW( ::nPos, "ON" )
::nPos++
xOnCond := ::ParseExpr()
ENDIF
AAdd( aJoins, { cJoinType, cTable, cAlias, xOnCond } )
ENDDO
RETURN NIL
/* Parse ORDER BY clause — with NULLS FIRST/LAST (SQL:2003) */
METHOD ParseOrderBy() CLASS TSqlParser2
LOCAL aOrder := {}, xExpr, cDir, cNulls
DO WHILE ::TType( ::nPos ) != TK_END .AND. ;
! ::IsKW( ::nPos, "LIMIT" ) .AND. ! ::IsKW( ::nPos, "UNION" ) .AND. ;
! ::IsKW( ::nPos, "INTERSECT" ) .AND. ! ::IsKW( ::nPos, "EXCEPT" ) .AND. ;
! ::IsKW( ::nPos, "FETCH" ) .AND. ! ::IsKW( ::nPos, "OFFSET" ) .AND. ;
! ::IsKW( ::nPos, "FOR" ) .AND. ! ::IsKW( ::nPos, "WINDOW" ) .AND. ;
::TType( ::nPos ) != TK_RPAR
xExpr := ::ParseExpr()
cDir := "ASC"
IF ::IsKW( ::nPos, "ASC" )
::nPos++
ELSEIF ::IsKW( ::nPos, "DESC" )
cDir := "DESC"
::nPos++
ENDIF
/* NULLS FIRST / NULLS LAST (SQL:2003) */
cNulls := ""
IF ::IsKW( ::nPos, "NULLS" )
::nPos++
IF ::IsKW( ::nPos, "FIRST" )
cNulls := "FIRST"
::nPos++
ELSEIF ::IsKW( ::nPos, "LAST" )
cNulls := "LAST"
::nPos++
ENDIF
ENDIF
IF Empty( cNulls )
AAdd( aOrder, { xExpr, cDir } )
ELSE
AAdd( aOrder, { xExpr, cDir, cNulls } )
ENDIF
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
RETURN aOrder
/* Parse INSERT INTO */
METHOD ParseInsert() CLASS TSqlParser2
LOCAL h := { => }, cTable, aFields := {}, aValues := {}, xE
h[ "type" ] := "INSERT"
::EatKW( "INTO" )
cTable := ::TVal( ::nPos ) ; ::nPos++
h[ "table" ] := cTable
/* Optional column list */
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
DO WHILE ::TType( ::nPos ) == TK_NAME
AAdd( aFields, ::TVal( ::nPos ) ) ; ::nPos++
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
h[ "fields" ] := aFields
/* VALUES clause */
IF ::IsKW( ::nPos, "VALUES" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END
xE := ::ParseExpr()
AAdd( aValues, xE )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ENDIF
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
ENDIF
h[ "values" ] := aValues
RETURN h
/* Parse UPDATE */
METHOD ParseUpdate() CLASS TSqlParser2
LOCAL h := { => }, cTable, aSet := {}, cCol, xVal
h[ "type" ] := "UPDATE"
cTable := ::TVal( ::nPos ) ; ::nPos++
h[ "table" ] := cTable
IF ::IsKW( ::nPos, "SET" )
::nPos++
DO WHILE ::TType( ::nPos ) == TK_NAME
cCol := ::TVal( ::nPos ) ; ::nPos++
IF ::TType( ::nPos ) == TK_EQ
::nPos++
ENDIF
xVal := ::ParseExpr()
AAdd( aSet, { cCol, xVal } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
h[ "set" ] := aSet
h[ "where" ] := NIL
IF ::IsKW( ::nPos, "WHERE" )
::nPos++
h[ "where" ] := ::ParseExpr()
ENDIF
RETURN h
/* Parse DELETE FROM */
METHOD ParseDelete() CLASS TSqlParser2
LOCAL h := { => }
h[ "type" ] := "DELETE"
::EatKW( "FROM" )
h[ "table" ] := ::TVal( ::nPos ) ; ::nPos++
h[ "where" ] := NIL
IF ::IsKW( ::nPos, "WHERE" )
::nPos++
h[ "where" ] := ::ParseExpr()
ENDIF
RETURN h
/* ======================================================================
* Pratt Parser — Data-driven expression parsing
*
* Operator precedence and associativity are defined as DATA in the
* InfixBP() method. To add a new operator, just add one line there.
*
* Binary precedence levels (higher = binds tighter):
* 10 OR
* 20 AND
* 30 NOT (prefix)
* 40 = <> < > <= >= LIKE IN BETWEEN IS SIMILAR TO IS DISTINCT FROM
* 50 + - ||
* 60 * /
* 70 unary -
*
* Adding a new operator (e.g., XOR at precedence 15):
* Just add to InfixBP: { "XOR", 15, 16, ND_BIN }
* No structural code changes needed.
* ====================================================================== */
METHOD ParseExpr() CLASS TSqlParser2
RETURN ::PrattExpr( 0 )
/* Pratt expression parser: parse expression with minimum binding power */
METHOD PrattExpr( nMinBP ) CLASS TSqlParser2
LOCAL xL, xR, cOp, nLBP, nRBP, nType
LOCAL aInf
LOCAL xLow, xHigh, xR2, aList, lNeg
/* Parse prefix / primary (NUD in Pratt terminology) */
xL := ::PrattPrefix()
/* Parse infix operators (LED in Pratt terminology) */
WHILE .T.
aInf := ::InfixBP()
IF aInf == NIL
EXIT
ENDIF
cOp := aInf[ 1 ]
nLBP := aInf[ 2 ]
nRBP := aInf[ 3 ]
nType := aInf[ 4 ]
IF nLBP < nMinBP
EXIT
ENDIF
/* Special postfix operators: LIKE, IN, BETWEEN, IS */
IF cOp == "LIKE"
::nPos++
xR := ::PrattExpr( nRBP )
IF ::IsKW( ::nPos, "ESCAPE" )
::nPos++
xR2 := ::PrattExpr( nRBP )
xL := SqlNode( ND_BIN, "LIKE", xL, xR, xR2 )
ELSE
xL := SqlNode( ND_BIN, "LIKE", xL, xR, NIL )
ENDIF
LOOP
ENDIF
IF cOp == "NOT LIKE"
::nPos += 2
xR := ::PrattExpr( nRBP )
IF ::IsKW( ::nPos, "ESCAPE" )
::nPos++
xR2 := ::PrattExpr( nRBP )
xL := SqlNode( ND_UNI, "NOT", SqlNode( ND_BIN, "LIKE", xL, xR, xR2 ), NIL, NIL )
ELSE
xL := SqlNode( ND_UNI, "NOT", SqlNode( ND_BIN, "LIKE", xL, xR, NIL ), NIL, NIL )
ENDIF
LOOP
ENDIF
/* SIMILAR TO (SQL:2003) — regex-like pattern matching */
IF cOp == "SIMILAR TO"
::nPos += 2 /* skip SIMILAR TO */
xR := ::PrattExpr( nRBP )
IF ::IsKW( ::nPos, "ESCAPE" )
::nPos++
xR2 := ::PrattExpr( nRBP )
xL := SqlNode( ND_BIN, "SIMILAR TO", xL, xR, xR2 )
ELSE
xL := SqlNode( ND_BIN, "SIMILAR TO", xL, xR, NIL )
ENDIF
LOOP
ENDIF
/* NOT SIMILAR TO (SQL:2003) */
IF cOp == "NOT SIMILAR TO"
::nPos += 3 /* skip NOT SIMILAR TO */
xR := ::PrattExpr( nRBP )
IF ::IsKW( ::nPos, "ESCAPE" )
::nPos++
xR2 := ::PrattExpr( nRBP )
xL := SqlNode( ND_UNI, "NOT", SqlNode( ND_BIN, "SIMILAR TO", xL, xR, xR2 ), NIL, NIL )
ELSE
xL := SqlNode( ND_UNI, "NOT", SqlNode( ND_BIN, "SIMILAR TO", xL, xR, NIL ), NIL, NIL )
ENDIF
LOOP
ENDIF
/* IS DISTINCT FROM / IS NOT DISTINCT FROM (SQL:2003) */
IF cOp == "IS DISTINCT FROM"
::nPos += 3 /* skip IS DISTINCT FROM */
xR := ::PrattExpr( nRBP )
xL := SqlNode( ND_BIN, "IS DISTINCT FROM", xL, xR, NIL )
LOOP
ENDIF
IF cOp == "IS NOT DISTINCT FROM"
::nPos += 4 /* skip IS NOT DISTINCT FROM */
xR := ::PrattExpr( nRBP )
xL := SqlNode( ND_BIN, "IS NOT DISTINCT FROM", xL, xR, NIL )
LOOP
ENDIF
IF cOp == "IN" .OR. cOp == "NOT IN"
lNeg := ( cOp == "NOT IN" )
IF lNeg
::nPos++ /* skip NOT */
ENDIF
::nPos++ /* skip IN */
IF ::TType( ::nPos ) == TK_LPAR
IF ::IsKW( ::nPos + 1, "SELECT" )
xR := ::ParseSubquery()
ELSE
::nPos++
aList := {}
WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END
AAdd( aList, ::ParseExpr() )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ENDIF
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
xR := SqlNode( ND_LIST, aList, NIL, NIL, NIL )
ENDIF
ENDIF
xL := SqlNode( ND_BIN, "IN", xL, xR, NIL )
IF lNeg
xL := SqlNode( ND_UNI, "NOT", xL, NIL, NIL )
ENDIF
LOOP
ENDIF
IF cOp == "BETWEEN" .OR. cOp == "NOT BETWEEN"
lNeg := ( cOp == "NOT BETWEEN" )
IF lNeg
::nPos++
ENDIF
::nPos++
xLow := ::PrattExpr( 50 )
::EatKW( "AND" )
xHigh := ::PrattExpr( 50 )
xL := SqlNode( ND_RANGE, "BETWEEN", xL, xLow, xHigh )
IF lNeg
xL := SqlNode( ND_UNI, "NOT", xL, NIL, NIL )
ENDIF
LOOP
ENDIF
IF cOp == "IS NULL"
::nPos++
::EatKW( "NULL" )
xL := SqlNode( ND_BIN, "IS NULL", xL, NIL, NIL )
LOOP
ENDIF
IF cOp == "IS NOT NULL"
::nPos++
::nPos++
::EatKW( "NULL" )
xL := SqlNode( ND_BIN, "IS NOT NULL", xL, NIL, NIL )
LOOP
ENDIF
IF cOp == "COLLATE"
::nPos++
IF ::TType( ::nPos ) == TK_NAME
::nPos++
ENDIF
LOOP
ENDIF
/* Standard binary operator */
::nPos++
xR := ::PrattExpr( nRBP )
xL := SqlNode( nType, cOp, xL, xR, NIL )
ENDDO
RETURN xL
/*
* InfixBP — Operator precedence table (the ONLY place to add operators).
*
* Returns { operator, leftBP, rightBP, nodeType } or NIL if not an operator.
* leftBP: minimum binding power to associate left
* rightBP: binding power for right operand (leftBP+1 for left-assoc)
*/
METHOD InfixBP() CLASS TSqlParser2
LOCAL nT, cV
nT := ::TType( ::nPos )
cV := ::TVal( ::nPos )
/* Symbol operators: O(1) hash lookup */
IF hb_HHasKey( ::hInfixTT, nT )
RETURN ::hInfixTT[ nT ]
ENDIF
/* Keyword operators: O(1) hash lookup for simple ones */
IF nT == TK_NAME
IF hb_HHasKey( ::hInfixKW, cV )
RETURN ::hInfixKW[ cV ]
ENDIF
/* Multi-token keyword operators (require lookahead) */
IF cV == "SIMILAR"
IF ::IsKW( ::nPos + 1, "TO" ) ; RETURN { "SIMILAR TO", 40, 50, ND_BIN }
ENDIF
ELSEIF cV == "IS"
IF ::IsKW( ::nPos + 1, "NOT" ) .AND. ::IsKW( ::nPos + 2, "DISTINCT" ) .AND. ::IsKW( ::nPos + 3, "FROM" )
RETURN { "IS NOT DISTINCT FROM", 40, 41, ND_BIN }
ELSEIF ::IsKW( ::nPos + 1, "DISTINCT" ) .AND. ::IsKW( ::nPos + 2, "FROM" )
RETURN { "IS DISTINCT FROM", 40, 41, ND_BIN }
ELSEIF ::IsKW( ::nPos + 1, "NOT" ) ; RETURN { "IS NOT NULL", 40, 41, ND_BIN }
ELSE ; RETURN { "IS NULL", 40, 41, ND_BIN }
ENDIF
ELSEIF cV == "NOT"
IF ::IsKW( ::nPos + 1, "LIKE" ) ; RETURN { "NOT LIKE", 40, 50, ND_BIN }
ELSEIF ::IsKW( ::nPos + 1, "IN" ) ; RETURN { "NOT IN", 40, 50, ND_BIN }
ELSEIF ::IsKW( ::nPos + 1, "BETWEEN" ) ; RETURN { "NOT BETWEEN", 40, 50, ND_BIN }
ELSEIF ::IsKW( ::nPos + 1, "SIMILAR" ) .AND. ::IsKW( ::nPos + 2, "TO" )
RETURN { "NOT SIMILAR TO", 40, 50, ND_BIN }
ENDIF
ENDIF
ENDIF
RETURN NIL
/* Pratt prefix / primary parser (NUD) */
METHOD PrattPrefix() CLASS TSqlParser2
LOCAL xE
/* NOT prefix */
IF ::IsKW( ::nPos, "NOT" )
::nPos++
xE := ::PrattExpr( 30 )
RETURN SqlNode( ND_UNI, "NOT", xE, NIL, NIL )
ENDIF
/* Unary minus */
IF ::TType( ::nPos ) == TK_MINUS
::nPos++
xE := ::PrattExpr( 70 )
RETURN SqlNode( ND_UNI, "-", xE, NIL, NIL )
ENDIF
RETURN ::ParsePrimary()
/* Parse primary expressions */
METHOD ParsePrimary() CLASS TSqlParser2
LOCAL cVal, cName, xE, aArgs, aCases, xElse, xCond, xThen
LOCAL cPart, cTrimSpec, xTrimChar, xFrom
LOCAL aColDefs, cColName, cColPath, aOrdItems, cDir, xExpr
/* NULL literal */
IF ::IsKW( ::nPos, "NULL" )
::nPos++
RETURN SqlNode( ND_NIL, NIL, NIL, NIL, NIL )
ENDIF
/* Numeric literal */
IF ::TType( ::nPos ) == TK_NUM
cVal := ::TVal( ::nPos )
::nPos++
RETURN SqlNode( ND_LIT, Val( cVal ), NIL, NIL, NIL )
ENDIF
/* String literal */
IF ::TType( ::nPos ) == TK_TEXT
cVal := ::TVal( ::nPos )
::nPos++
RETURN SqlNode( ND_LIT, cVal, NIL, NIL, NIL )
ENDIF
/* Positional parameter — each ? gets a sequential 1-based index
* so EvalExpr can return the correct ::aParams[n] value. */
IF ::TType( ::nPos ) == TK_QMARK
::nPos++
::nParamSeq++
RETURN SqlNode( ND_PAR, ::nParamSeq, NIL, NIL, NIL )
ENDIF
/* Parenthesized expression or scalar subquery */
IF ::TType( ::nPos ) == TK_LPAR
IF ::IsKW( ::nPos + 1, "SELECT" )
RETURN ::ParseSubquery()
ENDIF
::nPos++
xE := ::ParseExpr()
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN xE
ENDIF
/* EXISTS (subquery) */
IF ::IsKW( ::nPos, "EXISTS" )
::nPos++
xE := ::ParseSubquery()
RETURN SqlNode( ND_FN, "EXISTS", { xE }, NIL, NIL )
ENDIF
/* CASE WHEN ... THEN ... [ELSE ...] END */
IF ::IsKW( ::nPos, "CASE" )
::nPos++
aCases := {}
xElse := NIL
DO WHILE ::IsKW( ::nPos, "WHEN" )
::nPos++
xCond := ::ParseExpr()
::EatKW( "THEN" )
xThen := ::ParseExpr()
AAdd( aCases, { xCond, xThen } )
ENDDO
IF ::IsKW( ::nPos, "ELSE" )
::nPos++
xElse := ::ParseExpr()
ENDIF
::EatKW( "END" )
RETURN SqlNode( ND_CASE, aCases, xElse, NIL, NIL )
ENDIF
/* TIMESTAMP literal */
IF ::IsKW( ::nPos, "TIMESTAMP" )
IF ::TType( ::nPos + 1 ) == TK_TEXT
::nPos++
cVal := ::TVal( ::nPos )
::nPos++
RETURN SqlNode( ND_FN, "TIMESTAMP", ;
{ SqlNode( ND_LIT, cVal, NIL, NIL, NIL ) }, NIL, NIL )
ENDIF
ENDIF
/* CAST(expr AS type) — SQL:1999 special syntax */
IF ::IsKW( ::nPos, "CAST" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
xE := ::ParseExpr()
::EatKW( "AS" )
/* Parse type name (may include size e.g. VARCHAR(100)) */
cName := ::TVal( ::nPos )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
cName += "(" + ::TVal( ::nPos )
::nPos++
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
cName += "," + ::TVal( ::nPos )
::nPos++
ENDIF
cName += ")"
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "CAST", ;
{ xE, SqlNode( ND_LIT, cName, NIL, NIL, NIL ) }, NIL, NIL )
ENDIF
ENDIF
/* EXTRACT(part FROM expr) — SQL:1992 */
IF ::IsKW( ::nPos, "EXTRACT" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
cPart := ::TVal( ::nPos )
::nPos++
::EatKW( "FROM" )
xE := ::ParseExpr()
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "EXTRACT", ;
{ SqlNode( ND_LIT, cPart, NIL, NIL, NIL ), xE }, NIL, NIL )
ENDIF
ENDIF
/* TRIM(LEADING/TRAILING/BOTH char FROM expr) — SQL:1992 extended syntax
* Only activates when LEADING/TRAILING/BOTH keyword is present, or when
* FROM keyword separates arguments. Simple TRIM(expr) falls through to
* the generic function handler to preserve backward compatibility. */
IF ::IsKW( ::nPos, "TRIM" ) .AND. ::TType( ::nPos + 1 ) == TK_LPAR
cTrimSpec := ""
IF ::IsKW( ::nPos + 2, "LEADING" ) .OR. ::IsKW( ::nPos + 2, "TRAILING" ) .OR. ;
::IsKW( ::nPos + 2, "BOTH" )
cTrimSpec := ::TVal( ::nPos + 2 )
ENDIF
IF ! Empty( cTrimSpec )
::nPos++ /* eat TRIM */
::nPos++ /* eat ( */
::nPos++ /* eat LEADING/TRAILING/BOTH */
xTrimChar := NIL
/* Check for trim character before FROM */
IF ! ::IsKW( ::nPos, "FROM" ) .AND. ::TType( ::nPos ) != TK_RPAR
xTrimChar := ::ParseExpr()
ENDIF
::EatKW( "FROM" )
xFrom := ::ParseExpr()
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
aArgs := { SqlNode( ND_LIT, cTrimSpec, NIL, NIL, NIL ), xFrom }
IF xTrimChar != NIL
AAdd( aArgs, xTrimChar )
ENDIF
RETURN SqlNode( ND_FN, "TRIM", aArgs, NIL, NIL )
ENDIF
ENDIF
/* POSITION(str IN str) — SQL:1992
* Uses PrattExpr(50) for first arg to stop before IN keyword which
* has binding power 40 in the infix table. */
IF ::IsKW( ::nPos, "POSITION" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
xE := ::PrattExpr( 50 )
::EatKW( "IN" )
xFrom := ::ParseExpr()
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "POSITION", { xE, xFrom }, NIL, NIL )
ENDIF
ENDIF
/* OVERLAY(str PLACING str FROM n FOR n) — SQL:2003 */
IF ::IsKW( ::nPos, "OVERLAY" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
AAdd( aArgs, ::ParseExpr() )
::EatKW( "PLACING" )
AAdd( aArgs, ::ParseExpr() )
::EatKW( "FROM" )
AAdd( aArgs, ::ParseExpr() )
IF ::IsKW( ::nPos, "FOR" )
::nPos++
AAdd( aArgs, ::ParseExpr() )
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "OVERLAY", aArgs, NIL, NIL )
ENDIF
ENDIF
/* ARRAY(expr, ...) — SQL:2003 array constructor */
IF ::IsKW( ::nPos, "ARRAY" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
IF ::TType( ::nPos ) != TK_RPAR
AAdd( aArgs, ::ParseExpr() )
DO WHILE ::TType( ::nPos ) == TK_COMMA
::nPos++
AAdd( aArgs, ::ParseExpr() )
ENDDO
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "ARRAY", aArgs, NIL, NIL )
ENDIF
ENDIF
/* ROW(expr, ...) — SQL:2003 row value constructor */
IF ::IsKW( ::nPos, "ROW" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
IF ::TType( ::nPos ) != TK_RPAR
AAdd( aArgs, ::ParseExpr() )
DO WHILE ::TType( ::nPos ) == TK_COMMA
::nPos++
AAdd( aArgs, ::ParseExpr() )
ENDDO
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "ROW", aArgs, NIL, NIL )
ENDIF
ENDIF
/* JSON_TABLE(expr, path COLUMNS(...)) — SQL:2016 */
IF ::IsKW( ::nPos, "JSON_TABLE" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
AAdd( aArgs, ::ParseExpr() ) /* JSON expression */
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ENDIF
AAdd( aArgs, ::ParseExpr() ) /* path expression */
/* COLUMNS clause */
aColDefs := {}
IF ::IsKW( ::nPos, "COLUMNS" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END
cColName := ::TVal( ::nPos )
::nPos++
/* Eat type name tokens until comma or ) */
cColPath := ""
DO WHILE ::TType( ::nPos ) != TK_COMMA .AND. ;
::TType( ::nPos ) != TK_RPAR .AND. ;
::TType( ::nPos ) != TK_END
cColPath += ::TVal( ::nPos ) + " "
::nPos++
ENDDO
AAdd( aColDefs, { cColName, AllTrim( cColPath ) } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ENDIF
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
ENDIF
AAdd( aArgs, SqlNode( ND_LIST, aColDefs, NIL, NIL, NIL ) )
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "JSON_TABLE", aArgs, NIL, NIL )
ENDIF
ENDIF
/* JSON_OBJECT(key: value, ...) / JSON_OBJECT(key VALUE value, ...) — SQL:2016 */
IF ::IsKW( ::nPos, "JSON_OBJECT" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
IF ::TType( ::nPos ) != TK_RPAR
DO WHILE .T.
/* Parse key */
AAdd( aArgs, ::ParseExpr() )
/* Separator is ':' or VALUE keyword — skip it */
IF ::IsKW( ::nPos, "VALUE" )
::nPos++
ENDIF
/* Parse value */
AAdd( aArgs, ::ParseExpr() )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "JSON_OBJECT", aArgs, NIL, NIL )
ENDIF
ENDIF
/* JSON_OBJECTAGG(key: value) — SQL:2016 */
IF ::IsKW( ::nPos, "JSON_OBJECTAGG" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
AAdd( aArgs, ::ParseExpr() ) /* key */
IF ::IsKW( ::nPos, "VALUE" )
::nPos++
ENDIF
AAdd( aArgs, ::ParseExpr() ) /* value */
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "JSON_OBJECTAGG", aArgs, NIL, NIL )
ENDIF
ENDIF
/* XMLELEMENT(NAME name, expr, ...) — SQL:2003 */
IF ::IsKW( ::nPos, "XMLELEMENT" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
::EatKW( "NAME" )
aArgs := {}
AAdd( aArgs, SqlNode( ND_LIT, ::TVal( ::nPos ), NIL, NIL, NIL ) )
::nPos++
DO WHILE ::TType( ::nPos ) == TK_COMMA
::nPos++
AAdd( aArgs, ::ParseExpr() )
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "XMLELEMENT", aArgs, NIL, NIL )
ENDIF
ENDIF
/* XMLFOREST(expr AS name, ...) — SQL:2003 */
IF ::IsKW( ::nPos, "XMLFOREST" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
DO WHILE .T.
xE := ::ParseExpr()
cName := ""
IF ::IsKW( ::nPos, "AS" )
::nPos++
cName := ::TVal( ::nPos )
::nPos++
ENDIF
AAdd( aArgs, { xE, cName } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "XMLFOREST", aArgs, NIL, NIL )
ENDIF
ENDIF
/* XMLAGG(expr ORDER BY ...) — SQL:2003 */
IF ::IsKW( ::nPos, "XMLAGG" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
xE := ::ParseExpr()
aArgs := { xE }
IF ::IsKW( ::nPos, "ORDER" )
::nPos++
::EatKW( "BY" )
AAdd( aArgs, ::ParseOrderBy() )
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
RETURN SqlNode( ND_FN, "XMLAGG", aArgs, NIL, NIL )
ENDIF
ENDIF
/* LISTAGG(expr, sep) WITHIN GROUP (ORDER BY ...) — SQL:2016 */
IF ::IsKW( ::nPos, "LISTAGG" )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
AAdd( aArgs, ::ParseExpr() )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
AAdd( aArgs, ::ParseExpr() )
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
/* WITHIN GROUP (ORDER BY ...) */
aOrdItems := {}
IF ::IsKW( ::nPos, "WITHIN" )
::nPos++
::EatKW( "GROUP" )
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
IF ::IsKW( ::nPos, "ORDER" )
::nPos++
::EatKW( "BY" )
DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END
xExpr := ::ParseExpr()
cDir := "ASC"
IF ::IsKW( ::nPos, "ASC" )
::nPos++
ELSEIF ::IsKW( ::nPos, "DESC" )
cDir := "DESC"
::nPos++
ENDIF
AAdd( aOrdItems, { xExpr, cDir } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
ENDIF
/* OVER clause for window usage */
IF ::IsKW( ::nPos, "OVER" )
RETURN ::ParseWindow( "LISTAGG", aArgs )
ENDIF
RETURN SqlNode( ND_FN, "LISTAGG", aArgs, aOrdItems, NIL )
ENDIF
ENDIF
/* Wildcard star */
IF ::TType( ::nPos ) == TK_STAR
::nPos++
RETURN SqlNode( ND_COL, "*", NIL, NIL, NIL )
ENDIF
/* Identifier: column reference or function call */
IF ::TType( ::nPos ) == TK_NAME
cName := ::TVal( ::nPos )
::nPos++
/* Qualified column: table.column */
IF ::TType( ::nPos ) == TK_DOT
::nPos++
cName += "." + ::TVal( ::nPos )
::nPos++
ENDIF
/* Function call: name( args ) */
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
aArgs := {}
IF ::TType( ::nPos ) == TK_STAR
AAdd( aArgs, SqlNode( ND_COL, "*", NIL, NIL, NIL ) )
::nPos++
ELSEIF ::TType( ::nPos ) != TK_RPAR
AAdd( aArgs, ::ParseExpr() )
DO WHILE ::TType( ::nPos ) == TK_COMMA
::nPos++
AAdd( aArgs, ::ParseExpr() )
ENDDO
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
/* WITHIN GROUP (ORDER BY ...) for aggregate functions */
IF ::IsKW( ::nPos, "WITHIN" )
::nPos++
::EatKW( "GROUP" )
aOrdItems := {}
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
IF ::IsKW( ::nPos, "ORDER" )
::nPos++
::EatKW( "BY" )
DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END
xExpr := ::ParseExpr()
cDir := "ASC"
IF ::IsKW( ::nPos, "ASC" )
::nPos++
ELSEIF ::IsKW( ::nPos, "DESC" )
cDir := "DESC"
::nPos++
ENDIF
AAdd( aOrdItems, { xExpr, cDir } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
ENDIF
/* Window function: func(...) OVER (...) */
IF ::IsKW( ::nPos, "OVER" )
RETURN ::ParseWindow( cName, aArgs )
ENDIF
RETURN SqlNode( ND_FN, cName, aArgs, NIL, NIL )
ENDIF
RETURN SqlNode( ND_COL, cName, NIL, NIL, NIL )
ENDIF
/* Fallback: skip unrecognized token */
::nPos++
RETURN SqlNode( ND_NIL, NIL, NIL, NIL, NIL )
/* Parse a parenthesized subquery */
METHOD ParseSubquery() CLASS TSqlParser2
LOCAL nDepth := 0, aSubTokens := {}, oSub, aParsed
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
ENDIF
DO WHILE ::nPos <= Len( ::aTokens )
IF ::TType( ::nPos ) == TK_LPAR
nDepth++
ELSEIF ::TType( ::nPos ) == TK_RPAR
IF nDepth == 0
::nPos++
EXIT
ENDIF
nDepth--
ENDIF
AAdd( aSubTokens, ::aTokens[ ::nPos ] )
::nPos++
ENDDO
AAdd( aSubTokens, { TK_END, "" } )
oSub := TSqlParser2():New( aSubTokens, ::aParams )
aParsed := oSub:Parse()
RETURN SqlNode( ND_SUB, aParsed, NIL, NIL, NIL )
/* Parse window specification (shared by OVER and WINDOW clause) */
METHOD ParseWindowSpec() CLASS TSqlParser2
LOCAL aPartBy := {}, aOrdBy := {}, xExpr, cDir, cNulls
LOCAL hFrame := NIL
IF ::TType( ::nPos ) == TK_LPAR
::nPos++ /* eat ( */
/* PARTITION BY */
IF ::IsKW( ::nPos, "PARTITION" )
::nPos++
::EatKW( "BY" )
DO WHILE ::TType( ::nPos ) == TK_NAME .AND. ;
! ::IsKW( ::nPos, "ORDER" ) .AND. ;
! ::IsKW( ::nPos, "ROWS" ) .AND. ;
! ::IsKW( ::nPos, "RANGE" ) .AND. ;
! ::IsKW( ::nPos, "GROUPS" ) .AND. ;
::TType( ::nPos ) != TK_RPAR .AND. ;
::TType( ::nPos ) != TK_END
AAdd( aPartBy, ::ParseExpr() )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
/* ORDER BY with NULLS FIRST/LAST */
IF ::IsKW( ::nPos, "ORDER" )
::nPos++
::EatKW( "BY" )
DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END .AND. ;
! ::IsKW( ::nPos, "ROWS" ) .AND. ;
! ::IsKW( ::nPos, "RANGE" ) .AND. ;
! ::IsKW( ::nPos, "GROUPS" )
xExpr := ::ParseExpr()
cDir := "ASC"
IF ::IsKW( ::nPos, "ASC" )
::nPos++
ELSEIF ::IsKW( ::nPos, "DESC" )
cDir := "DESC"
::nPos++
ENDIF
cNulls := ""
IF ::IsKW( ::nPos, "NULLS" )
::nPos++
IF ::IsKW( ::nPos, "FIRST" )
cNulls := "FIRST"
::nPos++
ELSEIF ::IsKW( ::nPos, "LAST" )
cNulls := "LAST"
::nPos++
ENDIF
ENDIF
IF Empty( cNulls )
AAdd( aOrdBy, { xExpr, cDir } )
ELSE
AAdd( aOrdBy, { xExpr, cDir, cNulls } )
ENDIF
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
/* Frame clause: ROWS/RANGE/GROUPS ... (SQL:2003/2011) */
IF ::IsKW( ::nPos, "ROWS" ) .OR. ::IsKW( ::nPos, "RANGE" ) .OR. ;
::IsKW( ::nPos, "GROUPS" )
hFrame := ::ParseFrameClause()
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++ /* eat ) */
ENDIF
ENDIF
RETURN { aPartBy, aOrdBy, hFrame }
/* Parse OVER(...) for window functions */
METHOD ParseWindow( cFuncName, aFuncArgs ) CLASS TSqlParser2
LOCAL aSpec
::nPos++ /* eat OVER */
/* Named window reference: OVER window_name (not followed by parenthesis) */
IF ::TType( ::nPos ) == TK_NAME
aSpec := { {}, {}, ::TVal( ::nPos ) }
::nPos++
RETURN SqlNode( ND_WINDOW, cFuncName, aFuncArgs, aSpec[ 1 ], aSpec[ 2 ] )
ENDIF
aSpec := ::ParseWindowSpec()
RETURN SqlNode( ND_WINDOW, cFuncName, aFuncArgs, aSpec[ 1 ], aSpec[ 2 ] )
/* Parse frame clause: ROWS/RANGE/GROUPS BETWEEN ... AND ... (SQL:2003/2011) */
METHOD ParseFrameClause() CLASS TSqlParser2
LOCAL hFrame := { => }
LOCAL cFrameType, cStart, cEnd, cExclude
cFrameType := ::TVal( ::nPos )
::nPos++
hFrame[ "type" ] := cFrameType
IF ::IsKW( ::nPos, "BETWEEN" )
::nPos++
cStart := ::ParseFrameBound()
::EatKW( "AND" )
cEnd := ::ParseFrameBound()
hFrame[ "start" ] := cStart
hFrame[ "end" ] := cEnd
ELSE
cStart := ::ParseFrameBound()
hFrame[ "start" ] := cStart
ENDIF
/* EXCLUDE clause (SQL:2011) */
IF ::IsKW( ::nPos, "EXCLUDE" )
::nPos++
cExclude := ""
IF ::IsKW( ::nPos, "NO" )
::nPos++
::EatKW( "OTHERS" )
cExclude := "NO OTHERS"
ELSEIF ::IsKW( ::nPos, "CURRENT" )
::nPos++
::EatKW( "ROW" )
cExclude := "CURRENT ROW"
ELSEIF ::IsKW( ::nPos, "GROUP" )
cExclude := "GROUP"
::nPos++
ELSEIF ::IsKW( ::nPos, "TIES" )
cExclude := "TIES"
::nPos++
ENDIF
hFrame[ "exclude" ] := cExclude
ENDIF
RETURN hFrame
/* Parse a single frame bound */
METHOD ParseFrameBound() CLASS TSqlParser2
LOCAL cBound
IF ::IsKW( ::nPos, "UNBOUNDED" )
::nPos++
IF ::IsKW( ::nPos, "PRECEDING" )
::nPos++
RETURN "UNBOUNDED PRECEDING"
ELSEIF ::IsKW( ::nPos, "FOLLOWING" )
::nPos++
RETURN "UNBOUNDED FOLLOWING"
ENDIF
RETURN "UNBOUNDED"
ENDIF
IF ::IsKW( ::nPos, "CURRENT" )
::nPos++
::EatKW( "ROW" )
RETURN "CURRENT ROW"
ENDIF
IF ::TType( ::nPos ) == TK_NUM
cBound := ::TVal( ::nPos )
::nPos++
IF ::IsKW( ::nPos, "PRECEDING" )
::nPos++
RETURN cBound + " PRECEDING"
ELSEIF ::IsKW( ::nPos, "FOLLOWING" )
::nPos++
RETURN cBound + " FOLLOWING"
ENDIF
RETURN cBound
ENDIF
RETURN "CURRENT ROW"
/* Parse MERGE INTO ... USING ... ON ... WHEN MATCHED/NOT MATCHED */
METHOD ParseMerge() CLASS TSqlParser2
LOCAL h := { => }, cTarget, cSource, cSrcAlias
LOCAL xOnCond, aUpdSet := {}, aInsFlds := {}, aInsVals := {}
LOCAL cCol, xVal
LOCAL lHasMatched := .F., lHasNotMatched := .F.
LOCAL xMatchCond, xNotMatchCond, lDelete
h[ "type" ] := "MERGE"
::nPos++ /* eat MERGE */
::EatKW( "INTO" )
cTarget := ::TVal( ::nPos )
::nPos++
h[ "target" ] := cTarget
::EatKW( "USING" )
cSource := ::TVal( ::nPos )
::nPos++
cSrcAlias := ""
IF ::IsKW( ::nPos, "AS" )
::nPos++
ENDIF
IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsKW( ::nPos, "ON" )
cSrcAlias := ::TVal( ::nPos )
::nPos++
ENDIF
h[ "source" ] := cSource
h[ "source_alias" ] := cSrcAlias
::EatKW( "ON" )
xOnCond := ::ParseExpr()
h[ "on" ] := xOnCond
xMatchCond := NIL
xNotMatchCond := NIL
lDelete := .F.
/* WHEN MATCHED [AND cond] THEN UPDATE SET ... / DELETE
* WHEN NOT MATCHED [AND cond] THEN INSERT ... */
DO WHILE ::IsKW( ::nPos, "WHEN" )
::nPos++ /* eat WHEN */
IF ::IsKW( ::nPos, "MATCHED" )
::nPos++ /* eat MATCHED */
/* Extended MERGE: optional AND condition (SQL:2008) */
IF ::IsKW( ::nPos, "AND" )
::nPos++
xMatchCond := ::ParseExpr()
ENDIF
::EatKW( "THEN" )
/* WHEN MATCHED THEN DELETE (SQL:2008) */
IF ::IsKW( ::nPos, "DELETE" )
::nPos++
lHasMatched := .T.
lDelete := .T.
ELSE
::EatKW( "UPDATE" )
::EatKW( "SET" )
lHasMatched := .T.
DO WHILE ::TType( ::nPos ) == TK_NAME
cCol := ::TVal( ::nPos )
::nPos++
/* skip table.col format */
IF ::TType( ::nPos ) == TK_DOT
::nPos++
cCol := ::TVal( ::nPos )
::nPos++
ENDIF
IF ::TType( ::nPos ) == TK_EQ
::nPos++
ENDIF
xVal := ::ParseExpr()
AAdd( aUpdSet, { cCol, xVal } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
ELSEIF ::IsKW( ::nPos, "NOT" )
::nPos++ /* eat NOT */
::EatKW( "MATCHED" )
/* Extended MERGE: optional AND condition (SQL:2008) */
IF ::IsKW( ::nPos, "AND" )
::nPos++
xNotMatchCond := ::ParseExpr()
ENDIF
::EatKW( "THEN" )
::EatKW( "INSERT" )
lHasNotMatched := .T.
/* Optional column list */
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
DO WHILE ::TType( ::nPos ) == TK_NAME
AAdd( aInsFlds, ::TVal( ::nPos ) )
::nPos++
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
::EatKW( "VALUES" )
IF ::TType( ::nPos ) == TK_LPAR
::nPos++
DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END
AAdd( aInsVals, ::ParseExpr() )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ENDIF
ENDDO
IF ::TType( ::nPos ) == TK_RPAR
::nPos++
ENDIF
ENDIF
ELSE
EXIT
ENDIF
ENDDO
h[ "has_matched" ] := lHasMatched
h[ "update_set" ] := aUpdSet
h[ "has_not_matched" ] := lHasNotMatched
h[ "insert_fields" ] := aInsFlds
h[ "insert_values" ] := aInsVals
h[ "match_condition" ] := xMatchCond
h[ "not_match_condition" ] := xNotMatchCond
h[ "matched_delete" ] := lDelete
RETURN h