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