Files
five/_FiveSql2/src/TSqlParser_orig.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

1174 lines
28 KiB
Plaintext

/*
* TSqlParser.prg — Recursive descent SQL parser
*
* FiveSql — SQL Engine for Harbour DBF/NTX
*
* Copyright (c) 2025 Charles KWON (Charles KWON OhJun)
* Email: charleskwonohjun@gmail.com
*
* All rights reserved.
*/
#include "hbclass.ch"
#include "FiveSqlDef.ch"
CLASS TSqlParser
DATA aTokens
DATA nPos
DATA aParams
METHOD New( aTokens, aParams ) CONSTRUCTOR
METHOD Parse()
METHOD ParseSelect()
METHOD ParseInsert()
METHOD ParseUpdate()
METHOD ParseDelete()
METHOD ParseExpr()
METHOD ParseOr()
METHOD ParseAnd()
METHOD ParseNot()
METHOD ParseCompare()
METHOD ParseAdd()
METHOD ParseMul()
METHOD ParseUnary()
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()
ENDCLASS
METHOD New( aTokens, aParams ) CLASS TSqlParser
::aTokens := aTokens
::nPos := 1
::aParams := iif( aParams == NIL, {}, aParams )
RETURN SELF
/* Token type at position n */
METHOD TType( n ) CLASS TSqlParser
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 TSqlParser
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 TSqlParser
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 TSqlParser
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 TSqlParser
LOCAL aKW := { "WHERE", "ORDER", "GROUP", "HAVING", "JOIN", "LEFT", ;
"RIGHT", "INNER", "ON", "OUTER", "CROSS", "FULL", ;
"SET", "VALUES", "LIMIT", "TOP", "UNION", ;
"INTERSECT", "EXCEPT", "WITH" }
RETURN AScan( aKW, {|x| x == cVal } ) > 0
/* Top-level statement dispatcher */
METHOD Parse() CLASS TSqlParser
LOCAL cType, h
LOCAL aCTE, cName, xSub, lRecursive
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++
::EatKW( "AS" )
xSub := ::ParseSubquery()
AAdd( aCTE, { cName, xSub } )
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()
ENDCASE
RETURN NIL
/* Parse SELECT statement */
METHOD ParseSelect() CLASS TSqlParser
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
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 */
IF ::IsKW( ::nPos, "GROUP" )
::nPos++
::EatKW( "BY" )
aGroupBy := ::ParseExprList()
ENDIF
h[ "group_by" ] := aGroupBy
/* HAVING */
IF ::IsKW( ::nPos, "HAVING" )
::nPos++
xHaving := ::ParseExpr()
ENDIF
h[ "having" ] := xHaving
/* ORDER BY */
IF ::IsKW( ::nPos, "ORDER" )
::nPos++
::EatKW( "BY" )
aOrderBy := ::ParseOrderBy()
ENDIF
h[ "order_by" ] := aOrderBy
/* LIMIT / OFFSET */
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
/* 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 column list */
METHOD ParseColumnList() CLASS TSqlParser
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 TSqlParser
LOCAL aList := {}
DO WHILE ::TType( ::nPos ) == TK_NAME .AND. ;
! ::IsKW( ::nPos, "HAVING" ) .AND. ! ::IsKW( ::nPos, "ORDER" ) .AND. ;
! ::IsKW( ::nPos, "LIMIT" ) .AND. ! ::IsKW( ::nPos, "UNION" ) .AND. ;
! ::IsKW( ::nPos, "INTERSECT" ) .AND. ! ::IsKW( ::nPos, "EXCEPT" )
AAdd( aList, ::ParseExpr() )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
RETURN aList
/* Parse FROM clause */
METHOD ParseFrom( aTables, aJoins ) CLASS TSqlParser
LOCAL cTable, cAlias, cJoinType, xOnCond, xSubQ
/* 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 } )
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++
cTable := ::TVal( ::nPos ) ; ::nPos++
cAlias := ""
IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) )
cAlias := ::TVal( ::nPos ) ; ::nPos++
ENDIF
AAdd( aTables, { cTable, cAlias, "" } )
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
cTable := ::TVal( ::nPos ) ; ::nPos++
cAlias := ""
IF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) )
cAlias := ::TVal( ::nPos ) ; ::nPos++
ENDIF
AAdd( aTables, { 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 */
METHOD ParseOrderBy() CLASS TSqlParser
LOCAL aOrder := {}, xExpr, cDir
DO WHILE ::TType( ::nPos ) != TK_END .AND. ;
! ::IsKW( ::nPos, "LIMIT" ) .AND. ! ::IsKW( ::nPos, "UNION" ) .AND. ;
! ::IsKW( ::nPos, "INTERSECT" ) .AND. ! ::IsKW( ::nPos, "EXCEPT" )
xExpr := ::ParseExpr()
cDir := "ASC"
IF ::IsKW( ::nPos, "ASC" )
::nPos++
ELSEIF ::IsKW( ::nPos, "DESC" )
cDir := "DESC"
::nPos++
ENDIF
AAdd( aOrder, { xExpr, cDir } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
RETURN aOrder
/* Parse INSERT INTO */
METHOD ParseInsert() CLASS TSqlParser
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 TSqlParser
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 TSqlParser
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
/* Parse expression entry point */
METHOD ParseExpr() CLASS TSqlParser
RETURN ::ParseOr()
/* Parse OR-level */
METHOD ParseOr() CLASS TSqlParser
LOCAL xL, xN
xL := ::ParseAnd()
DO WHILE ::IsKW( ::nPos, "OR" )
::nPos++
xN := ::ParseAnd()
xL := SqlNode( ND_BIN, "OR", xL, xN, NIL )
ENDDO
RETURN xL
/* Parse AND-level */
METHOD ParseAnd() CLASS TSqlParser
LOCAL xL, xN
xL := ::ParseNot()
DO WHILE ::IsKW( ::nPos, "AND" )
::nPos++
xN := ::ParseNot()
xL := SqlNode( ND_BIN, "AND", xL, xN, NIL )
ENDDO
RETURN xL
/* Parse NOT prefix */
METHOD ParseNot() CLASS TSqlParser
LOCAL xE
IF ::IsKW( ::nPos, "NOT" )
::nPos++
xE := ::ParseCompare()
RETURN SqlNode( ND_UNI, "NOT", xE, NIL, NIL )
ENDIF
RETURN ::ParseCompare()
/* Parse comparison-level */
METHOD ParseCompare() CLASS TSqlParser
LOCAL xL, xR, cOp, xLow, xHigh
LOCAL aList
LOCAL lNotIn, lNotBet
xL := ::ParseAdd()
/* Standard comparison operators */
IF ::TType( ::nPos ) == TK_EQ .OR. ::TType( ::nPos ) == TK_NEQ .OR. ;
::TType( ::nPos ) == TK_LT .OR. ::TType( ::nPos ) == TK_GT .OR. ;
::TType( ::nPos ) == TK_LTE .OR. ::TType( ::nPos ) == TK_GTE
cOp := ::TVal( ::nPos )
::nPos++
IF ::TType( ::nPos ) == TK_LPAR .AND. ::IsKW( ::nPos + 1, "SELECT" )
xR := ::ParseSubquery()
ELSE
xR := ::ParseAdd()
ENDIF
RETURN SqlNode( ND_BIN, cOp, xL, xR, NIL )
ENDIF
/* LIKE [ESCAPE] */
IF ::IsKW( ::nPos, "LIKE" )
::nPos++
xR := ::ParseAdd()
IF ::IsKW( ::nPos, "ESCAPE" )
::nPos++
RETURN SqlNode( ND_BIN, "LIKE", xL, xR, ::ParseAdd() )
ENDIF
RETURN SqlNode( ND_BIN, "LIKE", xL, xR, NIL )
ENDIF
/* NOT LIKE [ESCAPE] */
IF ::IsKW( ::nPos, "NOT" ) .AND. ::IsKW( ::nPos + 1, "LIKE" )
::nPos += 2
xR := ::ParseAdd()
IF ::IsKW( ::nPos, "ESCAPE" )
::nPos++
RETURN SqlNode( ND_UNI, "NOT", SqlNode( ND_BIN, "LIKE", xL, xR, ::ParseAdd() ), NIL, NIL )
ENDIF
RETURN SqlNode( ND_UNI, "NOT", SqlNode( ND_BIN, "LIKE", xL, xR, NIL ), NIL, NIL )
ENDIF
/* IN / NOT IN */
IF ::IsKW( ::nPos, "IN" ) .OR. ;
( ::IsKW( ::nPos, "NOT" ) .AND. ::IsKW( ::nPos + 1, "IN" ) )
lNotIn := .F.
IF ::IsKW( ::nPos, "NOT" )
lNotIn := .T.
::nPos++
ENDIF
::nPos++
IF ::TType( ::nPos ) == TK_LPAR
IF ::IsKW( ::nPos + 1, "SELECT" )
xR := ::ParseSubquery()
ELSE
::nPos++
aList := {}
DO 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 lNotIn
xL := SqlNode( ND_UNI, "NOT", xL, NIL, NIL )
ENDIF
RETURN xL
ENDIF
/* BETWEEN / NOT BETWEEN */
IF ::IsKW( ::nPos, "BETWEEN" ) .OR. ;
( ::IsKW( ::nPos, "NOT" ) .AND. ::IsKW( ::nPos + 1, "BETWEEN" ) )
lNotBet := .F.
IF ::IsKW( ::nPos, "NOT" )
lNotBet := .T.
::nPos++
ENDIF
::nPos++
xLow := ::ParseAdd()
::EatKW( "AND" )
xHigh := ::ParseAdd()
xL := SqlNode( ND_RANGE, "BETWEEN", xL, xLow, xHigh )
IF lNotBet
xL := SqlNode( ND_UNI, "NOT", xL, NIL, NIL )
ENDIF
RETURN xL
ENDIF
/* IS [NOT] NULL */
IF ::IsKW( ::nPos, "IS" )
::nPos++
IF ::IsKW( ::nPos, "NOT" )
::nPos++
::EatKW( "NULL" )
RETURN SqlNode( ND_BIN, "IS NOT NULL", xL, NIL, NIL )
ENDIF
::EatKW( "NULL" )
RETURN SqlNode( ND_BIN, "IS NULL", xL, NIL, NIL )
ENDIF
/* COLLATE keyword (skip) */
IF ::IsKW( ::nPos, "COLLATE" )
::nPos++
IF ::TType( ::nPos ) == TK_NAME
::nPos++
ENDIF
ENDIF
RETURN xL
/* Parse addition / subtraction / string concatenation */
METHOD ParseAdd() CLASS TSqlParser
LOCAL xL, cOp, xR
xL := ::ParseMul()
DO WHILE ::TType( ::nPos ) == TK_PLUS .OR. ::TType( ::nPos ) == TK_MINUS .OR. ::TType( ::nPos ) == TK_PIPES
cOp := ::TVal( ::nPos )
::nPos++
xR := ::ParseMul()
xL := SqlNode( ND_BIN, cOp, xL, xR, NIL )
ENDDO
RETURN xL
/* Parse multiplication / division */
METHOD ParseMul() CLASS TSqlParser
LOCAL xL, cOp, xR
xL := ::ParseUnary()
DO WHILE ::TType( ::nPos ) == TK_STAR .OR. ::TType( ::nPos ) == TK_SLASH
cOp := ::TVal( ::nPos )
::nPos++
xR := ::ParseUnary()
xL := SqlNode( ND_BIN, cOp, xL, xR, NIL )
ENDDO
RETURN xL
/* Parse unary minus */
METHOD ParseUnary() CLASS TSqlParser
LOCAL xE
IF ::TType( ::nPos ) == TK_MINUS
::nPos++
xE := ::ParsePrimary()
RETURN SqlNode( ND_UNI, "-", xE, NIL, NIL )
ENDIF
RETURN ::ParsePrimary()
/* Parse primary expressions */
METHOD ParsePrimary() CLASS TSqlParser
LOCAL cVal, cName, xE, aArgs, aCases, xElse, xCond, xThen
/* 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 */
IF ::TType( ::nPos ) == TK_QMARK
::nPos++
RETURN SqlNode( ND_PAR, NIL, 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
/* 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
/* 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 TSqlParser
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 := TSqlParser():New( aSubTokens, ::aParams )
aParsed := oSub:Parse()
RETURN SqlNode( ND_SUB, aParsed, NIL, NIL, NIL )
/* Parse OVER(...) for window functions */
METHOD ParseWindow( cFuncName, aFuncArgs ) CLASS TSqlParser
LOCAL aPartBy := {}, aOrdBy := {}, xExpr, cDir
::nPos++ /* eat OVER */
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. ;
::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 */
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( aOrdBy, { xExpr, cDir } )
IF ::TType( ::nPos ) == TK_COMMA
::nPos++
ELSE
EXIT
ENDIF
ENDDO
ENDIF
IF ::TType( ::nPos ) == TK_RPAR
::nPos++ /* eat ) */
ENDIF
ENDIF
RETURN SqlNode( ND_WINDOW, cFuncName, aFuncArgs, aPartBy, aOrdBy )
/* Parse MERGE INTO ... USING ... ON ... WHEN MATCHED/NOT MATCHED */
METHOD ParseMerge() CLASS TSqlParser
LOCAL h := { => }, cTarget, cSource, cSrcAlias
LOCAL xOnCond, aUpdSet := {}, aInsFlds := {}, aInsVals := {}
LOCAL cCol, xVal
LOCAL lHasMatched := .F., lHasNotMatched := .F.
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
/* WHEN MATCHED THEN UPDATE SET ... */
DO WHILE ::IsKW( ::nPos, "WHEN" )
::nPos++ /* eat WHEN */
IF ::IsKW( ::nPos, "MATCHED" )
::nPos++ /* eat MATCHED */
::EatKW( "THEN" )
::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
ELSEIF ::IsKW( ::nPos, "NOT" )
::nPos++ /* eat NOT */
::EatKW( "MATCHED" )
::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
RETURN h