/* * 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 /* SQL:2003 allows WITH ... . * Older code only accepted SELECT — UPDATE/DELETE that * referenced a CTE silently mis-parsed and surfaced as * "status: TG" (the table name leaking out as the result * envelope's [1][1]). Dispatch on the trailing keyword and * stash aCTE on whatever DML hash we get back. */ DO CASE CASE ::IsKW( ::nPos, "SELECT" ) ::nPos++ h := ::ParseSelect() CASE ::IsKW( ::nPos, "INSERT" ) ::nPos++ h := ::ParseInsert() CASE ::IsKW( ::nPos, "UPDATE" ) ::nPos++ h := ::ParseUpdate() CASE ::IsKW( ::nPos, "DELETE" ) ::nPos++ h := ::ParseDelete() OTHERWISE RETURN NIL ENDCASE 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 := NIL, 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++ /* Implicit alias: `SELECT salary total, ...` where `total` is * a non-keyword identifier followed by comma or clause keyword. * SQL standard allows omitting AS for column aliases. */ ELSEIF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) ) .AND. ; ! ::IsKW( ::nPos, "FROM" ) .AND. ! ::IsKW( ::nPos, "WHERE" ) .AND. ; ! ::IsKW( ::nPos, "GROUP" ) .AND. ! ::IsKW( ::nPos, "ORDER" ) .AND. ; ! ::IsKW( ::nPos, "HAVING" ) .AND. ! ::IsKW( ::nPos, "LIMIT" ) .AND. ; ! ::IsKW( ::nPos, "UNION" ) .AND. ! ::IsKW( ::nPos, "INTERSECT" ) .AND. ; ! ::IsKW( ::nPos, "EXCEPT" ) .AND. ! ::IsKW( ::nPos, "WINDOW" ) .AND. ; ! ::IsKW( ::nPos, "OFFSET" ) .AND. ! ::IsKW( ::nPos, "FETCH" ) .AND. ; ! ::IsKW( ::nPos, "INTO" ) .AND. ! ::IsKW( ::nPos, "FOR" ) 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 /* Derived table on the right side of a JOIN: `JOIN (SELECT...) AS x ON ...` */ 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 cTable := iif( lLateral, "__LATERAL__", "__SUBQUERY__" ) AAdd( aTables, { cTable, cAlias, xSubQ } ) ELSE 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, "" } ) ENDIF 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 := {}, aRows := {}, aTuple, 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 — SQL:2003 allows multiple row constructors: * VALUES (a, b, c), (d, e, f), ... * Each (...) tuple yields one INSERT. Older code produced a flat * expression list which limited us to the first tuple — second * and later tuples' values ended up as residual tokens and were * silently dropped. h["rows"] is always an array of tuples; * single-row INSERT produces a one-element outer array. */ IF ::IsKW( ::nPos, "VALUES" ) ::nPos++ DO WHILE ::TType( ::nPos ) == TK_LPAR ::nPos++ aTuple := {} DO WHILE ::TType( ::nPos ) != TK_RPAR .AND. ::TType( ::nPos ) != TK_END xE := ::ParseExpr() AAdd( aTuple, xE ) IF ::TType( ::nPos ) == TK_COMMA ::nPos++ ENDIF ENDDO IF ::TType( ::nPos ) == TK_RPAR ::nPos++ ENDIF AAdd( aRows, aTuple ) IF ::TType( ::nPos ) == TK_COMMA ::nPos++ ELSE EXIT ENDIF ENDDO ELSEIF ::IsKW( ::nPos, "SELECT" ) /* INSERT INTO t [(cols)] SELECT ... — capture the subquery plan * so RunInsert can materialize it as the driving tuple list. * ParseSelect expects the position to be at the first token * AFTER `SELECT`, so consume the keyword here. */ ::EatKW( "SELECT" ) h[ "select" ] := ::ParseSelect() ENDIF h[ "rows" ] := aRows 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, xTest LOCAL lDistinct LOCAL xDate, cNorm 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 /* Logical literals — SQL's TRUE/FALSE and the Harbour `.T.`/`.F.` * forms that the lexer rewrites to the same tokens. Without this * path INSERTing a bool value into an L column silently stored * NIL (lexer emitted an unknown keyword, parser fell through to * the identifier case and then Resolve() returned NIL because * no field / alias was named `TRUE`). */ IF ::IsKW( ::nPos, "TRUE" ) ::nPos++ RETURN SqlNode( ND_LIT, .T., NIL, NIL, NIL ) ENDIF IF ::IsKW( ::nPos, "FALSE" ) ::nPos++ RETURN SqlNode( ND_LIT, .F., NIL, NIL, NIL ) ENDIF /* DATE 'YYYY-MM-DD' or DATE 'YYYYMMDD' literal — SQL standard * explicit-type literal. Rebuilds a date value at parse time * (via CToD with ISO pre-pass) so downstream evaluation sees a * real Date, not a string needing late coercion. CToD silently * rolls invalid dates (`2025-02-29` → 2025-03-01) per xBase * convention; verify the round-trip and emit NIL for invalid * literals so callers see a clean NULL instead of a corrupt * neighbor-day. */ IF ::IsKW( ::nPos, "DATE" ) .AND. ::TType( ::nPos + 1 ) == TK_TEXT ::nPos++ cVal := ::TVal( ::nPos ) ::nPos++ xDate := CToD( cVal ) IF ValType( xDate ) == "D" .AND. ! Empty( xDate ) /* Compare DToS round-trip to the original digits — strip * separators on the input first (`2025-02-29` → `20250229`). */ cNorm := StrTran( StrTran( StrTran( cVal, "-", "" ), "/", "" ), ".", "" ) IF DToS( xDate ) != cNorm xDate := NIL /* invalid date, surface as NULL */ ENDIF ENDIF RETURN SqlNode( ND_LIT, xDate, 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: searched form `CASE WHEN cond THEN ... END` and simple * form `CASE expr WHEN val THEN ... END` — both per SQL standard. * Simple form was previously parsed as searched (skipping the * test expression), which left the parser at the wrong token and * the executor returned a single row of NILs. Simple form is * desugared into searched: each `WHEN val` becomes ND_BIN(=, * test_expr_clone, val). */ IF ::IsKW( ::nPos, "CASE" ) ::nPos++ aCases := {} xElse := NIL xTest := NIL IF ! ::IsKW( ::nPos, "WHEN" ) /* Simple form: peek a test expression before the first WHEN. */ xTest := ::ParseExpr() ENDIF DO WHILE ::IsKW( ::nPos, "WHEN" ) ::nPos++ xCond := ::ParseExpr() IF xTest != NIL xCond := SqlNode( ND_BIN, "=", AClone( xTest ), xCond, NIL ) ENDIF ::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( [DISTINCT|ALL] args ) */ IF ::TType( ::nPos ) == TK_LPAR ::nPos++ aArgs := {} lDistinct := .F. /* SQL aggregate modifier: `COUNT(DISTINCT col)`, `SUM(DISTINCT * col)`, etc. Without this, the keyword fell through to * ParseExpr as an identifier and the aggregate computed over * all values (or returned 0 because the arg resolved to * nothing). Both DISTINCT and the explicit ALL (the default) * are accepted; ALL is a no-op. */ IF ::IsKW( ::nPos, "DISTINCT" ) ::nPos++ lDistinct := .T. ELSEIF ::IsKW( ::nPos, "ALL" ) ::nPos++ ENDIF 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 /* slot 5 carries the DISTINCT modifier for aggregate dedup. */ RETURN SqlNode( ND_FN, cName, aArgs, NIL, lDistinct ) 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, xWinNode ::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() /* Store frame spec (aSpec[3]) in a 6th slot on the node — can't fit * in the 5-slot SqlNode, so AAdd post-construction. */ xWinNode := SqlNode( ND_WINDOW, cFuncName, aFuncArgs, aSpec[ 1 ], aSpec[ 2 ] ) AAdd( xWinNode, aSpec[ 3 ] ) RETURN xWinNode /* 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