/* * TSqlExpr.prg — Expression AST node constructor * * FiveSql — SQL Engine for Harbour DBF/NTX * * Copyright (c) 2025 Charles KWON (Charles KWON OhJun) * Email: charleskwonohjun@gmail.com * * All rights reserved. */ #include "FiveSqlDef.ch" /* Expression node: { nKind, xValue, xLeft, xRight, xExtra } */ FUNCTION SqlNode( nKind, xVal, xL, xR, xE ) RETURN { nKind, xVal, xL, xR, xE } /* Derive a display name from an expression node */ FUNCTION SqlExprName( xE ) LOCAL cR IF xE == NIL RETURN "?" ENDIF IF xE[ 1 ] == ND_COL cR := xE[ 2 ] IF "." $ cR RETURN SubStr( cR, At( ".", cR ) + 1 ) ENDIF RETURN cR ENDIF IF xE[ 1 ] == ND_FN RETURN xE[ 2 ] + "(...)" ENDIF IF xE[ 1 ] == ND_WINDOW RETURN xE[ 2 ] + "(...)" ENDIF IF xE[ 1 ] == ND_LIT RETURN SqlValToStr( xE[ 2 ] ) ENDIF RETURN "expr" /* SqlExprHasAgg is implemented in Go (hbrtl/sqlexpr.go) — registered * as SQLEXPRHASAGG. The prior PRG recursive walker has been removed * to avoid a name collision with the RTL symbol; behavior is * byte-for-byte identical. See docs/RTL-Go-Native-Migration.md. */ /* SqlExtractWindow: walk xE, find every ND_WINDOW node, replace it * in-place with a synthetic ND_COL pointing at a generated alias, * and append {windowExpr, alias} to aWindows for the caller to * register as hidden SELECT columns. Used by RunSelect so a wrapped * window function (`SUM(x) OVER () + 100`) can flow through the * usual ApplyWindowFunctions path: the inner ND_WINDOW becomes a * hidden top-level ND_WINDOW column, projection evaluates the outer * expression as ND_BIN(ND_COL("__win_..."), 100), and the trim at * RunSelect's tail strips the hidden column back off the result. * * Returns the (possibly mutated) xE. cPrefix scopes alias names per * SELECT column so two wrappers don't collide. */ FUNCTION SqlExtractWindow( xE, aWindows, cPrefix ) LOCAL i, cAlias, xNew IF xE == NIL .OR. ValType( xE ) != "A" .OR. Len( xE ) < 1 RETURN xE ENDIF IF xE[ 1 ] == ND_WINDOW cAlias := cPrefix + "_" + AllTrim( hb_NToS( Len( aWindows ) + 1 ) ) + "__" AAdd( aWindows, { AClone( xE ), cAlias } ) RETURN { ND_COL, cAlias, NIL, NIL, NIL } ENDIF IF xE[ 1 ] == ND_BIN xE[ 3 ] := SqlExtractWindow( xE[ 3 ], aWindows, cPrefix ) xE[ 4 ] := SqlExtractWindow( xE[ 4 ], aWindows, cPrefix ) RETURN xE ENDIF IF xE[ 1 ] == ND_UNI xE[ 3 ] := SqlExtractWindow( xE[ 3 ], aWindows, cPrefix ) RETURN xE ENDIF IF xE[ 1 ] == ND_FN IF ValType( xE[ 3 ] ) == "A" FOR i := 1 TO Len( xE[ 3 ] ) xE[ 3 ][ i ] := SqlExtractWindow( xE[ 3 ][ i ], aWindows, cPrefix ) NEXT ENDIF RETURN xE ENDIF IF xE[ 1 ] == ND_CASE IF ValType( xE[ 2 ] ) == "A" FOR i := 1 TO Len( xE[ 2 ] ) xE[ 2 ][ i ][ 1 ] := SqlExtractWindow( xE[ 2 ][ i ][ 1 ], aWindows, cPrefix ) xE[ 2 ][ i ][ 2 ] := SqlExtractWindow( xE[ 2 ][ i ][ 2 ], aWindows, cPrefix ) NEXT ENDIF IF Len( xE ) >= 3 .AND. xE[ 3 ] != NIL xE[ 3 ] := SqlExtractWindow( xE[ 3 ], aWindows, cPrefix ) ENDIF RETURN xE ENDIF RETURN xE /* SqlIsAggName is implemented in Go (hbrtl/sqlhelpers.go) — registered * as SQLISAGGNAME. Former PRG body: * RETURN ( "," + c + "," ) $ ( "," + AGG_FUNCTIONS + "," ) * Removed to avoid the symbol collision; Go version uses a hash lookup * against aggFuncSet (same AGG_FUNCTIONS list) and skips the substring * scan + string allocations that were ~9% of GROUP BY CPU. */ /* Return .T. if the function name is a recognized scalar */ FUNCTION SqlIsScalarName( c ) RETURN ( "," + c + "," ) $ ( "," + SCALAR_FUNCTIONS + "," ) /* Convert any value to string (NULL-safe) */ FUNCTION SqlValToStr( x ) IF x == NIL RETURN "NULL" ENDIF DO CASE CASE ValType( x ) == "C" ; RETURN AllTrim( x ) CASE ValType( x ) == "N" ; RETURN AllTrim( Str( x ) ) CASE ValType( x ) == "D" ; RETURN DToC( x ) CASE ValType( x ) == "L" ; RETURN iif( x, ".T.", ".F." ) CASE ValType( x ) == "T" ; RETURN hb_TToC( x ) ENDCASE RETURN "" /* Constant folding: pre-compute literal expressions at parse time */ FUNCTION SqlFoldConst( xExpr ) LOCAL xL, xR, cOp, xResult, nPI IF xExpr == NIL RETURN NIL ENDIF DO CASE CASE xExpr[ 1 ] == ND_LIT .OR. xExpr[ 1 ] == ND_NIL .OR. ; xExpr[ 1 ] == ND_COL .OR. xExpr[ 1 ] == ND_PAR .OR. ; xExpr[ 1 ] == ND_SUB RETURN xExpr CASE xExpr[ 1 ] == ND_BIN xExpr[ 3 ] := SqlFoldConst( xExpr[ 3 ] ) xExpr[ 4 ] := SqlFoldConst( xExpr[ 4 ] ) IF xExpr[ 3 ] != NIL .AND. xExpr[ 3 ][ 1 ] == ND_LIT .AND. ; xExpr[ 4 ] != NIL .AND. xExpr[ 4 ][ 1 ] == ND_LIT cOp := xExpr[ 2 ] xL := xExpr[ 3 ][ 2 ] xR := xExpr[ 4 ][ 2 ] xResult := NIL IF cOp == "+" IF ValType( xL ) == "C" .AND. ValType( xR ) == "C" xResult := xL + xR ELSEIF ValType( xL ) == "N" .AND. ValType( xR ) == "N" xResult := xL + xR ENDIF ELSEIF cOp == "-" .AND. ValType( xL ) == "N" .AND. ValType( xR ) == "N" xResult := xL - xR ELSEIF cOp == "*" .AND. ValType( xL ) == "N" .AND. ValType( xR ) == "N" xResult := xL * xR ELSEIF cOp == "/" .AND. ValType( xL ) == "N" .AND. ValType( xR ) == "N" .AND. xR != 0 xResult := xL / xR ELSEIF cOp == "||" .AND. ValType( xL ) == "C" .AND. ValType( xR ) == "C" xResult := xL + xR ENDIF IF xResult != NIL RETURN SqlNode( ND_LIT, xResult, NIL, NIL, NIL ) ENDIF ENDIF RETURN xExpr CASE xExpr[ 1 ] == ND_UNI xExpr[ 3 ] := SqlFoldConst( xExpr[ 3 ] ) IF xExpr[ 3 ] != NIL .AND. xExpr[ 3 ][ 1 ] == ND_LIT IF xExpr[ 2 ] == "-" .AND. ValType( xExpr[ 3 ][ 2 ] ) == "N" RETURN SqlNode( ND_LIT, -xExpr[ 3 ][ 2 ], NIL, NIL, NIL ) ENDIF ENDIF RETURN xExpr CASE xExpr[ 1 ] == ND_FN IF ValType( xExpr[ 3 ] ) == "A" xL := AClone( xExpr[ 3 ] ) FOR nPI := 1 TO Len( xL ) xL[ nPI ] := SqlFoldConst( xL[ nPI ] ) NEXT xExpr[ 3 ] := xL ENDIF RETURN xExpr CASE xExpr[ 1 ] == ND_CASE IF ValType( xExpr[ 2 ] ) == "A" FOR nPI := 1 TO Len( xExpr[ 2 ] ) xExpr[ 2 ][ nPI ][ 1 ] := SqlFoldConst( xExpr[ 2 ][ nPI ][ 1 ] ) xExpr[ 2 ][ nPI ][ 2 ] := SqlFoldConst( xExpr[ 2 ][ nPI ][ 2 ] ) NEXT ENDIF xExpr[ 3 ] := SqlFoldConst( xExpr[ 3 ] ) RETURN xExpr CASE xExpr[ 1 ] == ND_RANGE xExpr[ 3 ] := SqlFoldConst( xExpr[ 3 ] ) xExpr[ 4 ] := SqlFoldConst( xExpr[ 4 ] ) xExpr[ 5 ] := SqlFoldConst( xExpr[ 5 ] ) RETURN xExpr CASE xExpr[ 1 ] == ND_WINDOW /* Window functions cannot be constant-folded */ RETURN xExpr ENDCASE RETURN xExpr /* * Evaluate an expression against an in-memory row (no workarea). * Used for recursive CTE where temp files cause conflicts. */ FUNCTION SqlEvalRowExpr( xExpr, aFN, aRow ) LOCAL xL, xR, cOp, cName, i, aFnArgs IF xExpr == NIL RETURN NIL ENDIF DO CASE CASE xExpr[ 1 ] == ND_LIT RETURN xExpr[ 2 ] CASE xExpr[ 1 ] == ND_NIL RETURN NIL CASE xExpr[ 1 ] == ND_COL cName := Upper( xExpr[ 2 ] ) /* First try qualified name as-is (e.g. "E.MGR_ID") */ FOR i := 1 TO Len( aFN ) IF Upper( AllTrim( aFN[ i ] ) ) == cName .AND. i <= Len( aRow ) RETURN aRow[ i ] ENDIF NEXT /* Fall back: strip alias prefix and match unqualified name */ IF "." $ cName cName := SubStr( cName, At( ".", cName ) + 1 ) FOR i := 1 TO Len( aFN ) IF Upper( AllTrim( aFN[ i ] ) ) == cName .AND. i <= Len( aRow ) RETURN aRow[ i ] ENDIF NEXT ENDIF RETURN NIL CASE xExpr[ 1 ] == ND_BIN cOp := xExpr[ 2 ] xL := SqlEvalRowExpr( xExpr[ 3 ], aFN, aRow ) xR := SqlEvalRowExpr( xExpr[ 4 ], aFN, aRow ) IF cOp == "+" IF ValType( xL ) == "N" .AND. ValType( xR ) == "N" RETURN xL + xR ENDIF /* Date arithmetic: Date + N → Date (N days later). N + Date * is symmetric. Without these branches Date operands collapse * to 0 via SqlCoerceNum and the result is just the integer * offset. Mirrors EvalExpr's same-named fix. */ IF ValType( xL ) == "D" .AND. ValType( xR ) == "N" RETURN xL + xR ENDIF IF ValType( xL ) == "N" .AND. ValType( xR ) == "D" RETURN xR + xL ENDIF RETURN SqlCoerceNum( xL ) + SqlCoerceNum( xR ) ENDIF IF cOp == "-" IF ValType( xL ) == "D" .AND. ValType( xR ) == "N" RETURN xL - xR ENDIF IF ValType( xL ) == "D" .AND. ValType( xR ) == "D" RETURN xL - xR ENDIF RETURN SqlCoerceNum( xL ) - SqlCoerceNum( xR ) ENDIF IF cOp == "*" RETURN SqlCoerceNum( xL ) * SqlCoerceNum( xR ) ENDIF IF cOp == "/" IF SqlCoerceNum( xR ) != 0 RETURN SqlCoerceNum( xL ) / SqlCoerceNum( xR ) ENDIF RETURN 0 ENDIF IF cOp == "||" RETURN SqlCoerceStr( xL ) + SqlCoerceStr( xR ) ENDIF IF cOp == "AND" RETURN SqlIsTrue( xL ) .AND. SqlIsTrue( xR ) ENDIF IF cOp == "OR" RETURN SqlIsTrue( xL ) .OR. SqlIsTrue( xR ) ENDIF IF cOp == "=" RETURN SqlCmpEq( xL, xR ) ENDIF IF cOp == "<" RETURN SqlCmpLt( xL, xR ) ENDIF IF cOp == ">" RETURN SqlCmpLt( xR, xL ) ENDIF IF cOp == "<=" RETURN SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xL, xR ) ENDIF IF cOp == ">=" RETURN SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xR, xL ) ENDIF IF cOp == "<>" .OR. cOp == "!=" RETURN ! SqlCmpEq( xL, xR ) ENDIF RETURN NIL CASE xExpr[ 1 ] == ND_CASE IF ValType( xExpr[ 2 ] ) == "A" FOR i := 1 TO Len( xExpr[ 2 ] ) xL := SqlEvalRowExpr( xExpr[ 2 ][ i ][ 1 ], aFN, aRow ) IF SqlIsTrue( xL ) RETURN SqlEvalRowExpr( xExpr[ 2 ][ i ][ 2 ], aFN, aRow ) ENDIF NEXT ENDIF IF xExpr[ 3 ] != NIL RETURN SqlEvalRowExpr( xExpr[ 3 ], aFN, aRow ) ENDIF RETURN NIL CASE xExpr[ 1 ] == ND_FN IF Len( xExpr[ 3 ] ) > 0 /* Evaluate ALL arguments, not just the first */ aFnArgs := {} FOR i := 1 TO Len( xExpr[ 3 ] ) AAdd( aFnArgs, SqlEvalRowExpr( xExpr[ 3 ][ i ], aFN, aRow ) ) NEXT RETURN SqlEvalFunc( xExpr[ 2 ], aFnArgs ) ENDIF RETURN SqlEvalFunc( xExpr[ 2 ], {} ) CASE xExpr[ 1 ] == ND_UNI xL := SqlEvalRowExpr( xExpr[ 3 ], aFN, aRow ) IF xExpr[ 2 ] == "NOT" RETURN ! SqlIsTrue( xL ) ENDIF IF xExpr[ 2 ] == "-" RETURN -SqlCoerceNum( xL ) ENDIF RETURN xL ENDCASE RETURN NIL /* Collect all ND_COL leaf nodes from an expression tree. * Returns an array of the original ND_COL AST nodes so callers can * re-emit them with their qualified names preserved — e.g. FetchRow * needs "o.qty" / "p.price" rather than the bare "qty" / "price" so * it can route through FindWA to the right workarea in JOIN contexts. */ FUNCTION SqlCollectColExprs( xE, aCols ) LOCAL i IF aCols == NIL aCols := {} ENDIF IF xE == NIL RETURN aCols ENDIF DO CASE CASE xE[ 1 ] == ND_COL IF xE[ 2 ] != "*" AAdd( aCols, xE ) ENDIF CASE xE[ 1 ] == ND_BIN SqlCollectColExprs( xE[ 3 ], aCols ) SqlCollectColExprs( xE[ 4 ], aCols ) CASE xE[ 1 ] == ND_UNI SqlCollectColExprs( xE[ 3 ], aCols ) CASE xE[ 1 ] == ND_FN IF ValType( xE[ 3 ] ) == "A" FOR i := 1 TO Len( xE[ 3 ] ) SqlCollectColExprs( xE[ 3 ][ i ], aCols ) NEXT ENDIF CASE xE[ 1 ] == ND_CASE IF ValType( xE[ 2 ] ) == "A" FOR i := 1 TO Len( xE[ 2 ] ) SqlCollectColExprs( xE[ 2 ][ i ][ 1 ], aCols ) SqlCollectColExprs( xE[ 2 ][ i ][ 2 ], aCols ) NEXT ENDIF IF xE[ 3 ] != NIL SqlCollectColExprs( xE[ 3 ], aCols ) ENDIF ENDCASE RETURN aCols /* Collect all ND_COL leaf column names from an expression tree. * Returns an array of bare (unqualified) column name strings. */ FUNCTION SqlCollectCols( xE, aCols ) LOCAL i IF aCols == NIL aCols := {} ENDIF IF xE == NIL RETURN aCols ENDIF DO CASE CASE xE[ 1 ] == ND_COL IF xE[ 2 ] != "*" AAdd( aCols, SqlExprName( xE ) ) ENDIF CASE xE[ 1 ] == ND_BIN SqlCollectCols( xE[ 3 ], aCols ) SqlCollectCols( xE[ 4 ], aCols ) CASE xE[ 1 ] == ND_UNI SqlCollectCols( xE[ 3 ], aCols ) CASE xE[ 1 ] == ND_FN IF ValType( xE[ 3 ] ) == "A" FOR i := 1 TO Len( xE[ 3 ] ) SqlCollectCols( xE[ 3 ][ i ], aCols ) NEXT ENDIF CASE xE[ 1 ] == ND_CASE IF ValType( xE[ 2 ] ) == "A" FOR i := 1 TO Len( xE[ 2 ] ) SqlCollectCols( xE[ 2 ][ i ][ 1 ], aCols ) SqlCollectCols( xE[ 2 ][ i ][ 2 ], aCols ) NEXT ENDIF SqlCollectCols( xE[ 3 ], aCols ) ENDCASE RETURN aCols