Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2 SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved as a single checkpoint before refactoring the parser to delegate xBase command translation to the preprocessor. Highlights: FiveSql2 engine (_FiveSql2/src/) - prefix-glob index attach -> explicit convention (<table>_pk.ntx, <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop - DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt) - COUNT(DISTINCT col) parsed + aggregated via hSeen hash - UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent) - DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT) - Derived table FROM (SELECT...) + JOIN right-side derived - Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect - LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs) - DATE literal round-trip validation (Feb 29 non-leap rejected) - CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists - AlterTable type dispatcher comma-wrapped (1-char type "A" no longer matches CHARACTER) Compiler / runtime - gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity) - gengo split: emit_block.go, emit_stmt.go, folding.go extracted - parser/stmtreg.go nudges - hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*), windows debug stubs collapsed - thread/vm/value/class/pcinterp tightening from panic traces RDD layer (hbrdd/) - dbf: null bitmap support (null.go + null_test.go), mmap split (mmap_posix.go / mmap_windows.go), byte-level numeric parse - ntx/cdx: windows mmap parity - workarea + mem RDD: cross-area state-bleed fixes RTL (hbrtl/) - errorlog rewrite with platform-specific FD (errorlog_fd_unix / errorlog_fd_other) - sqlscan, sqlhelpers, indexrtl, datetime extensions Gates green at checkpoint: - go test ./... : PASS - FiveSql2 SQL:1999 : 43/43 - Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2392 lines
70 KiB
Plaintext
2392 lines
70 KiB
Plaintext
/*
|
|
* 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 ... <SELECT|INSERT|UPDATE|DELETE>.
|
|
* 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
|