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>
1174 lines
28 KiB
Plaintext
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
|