/* * 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