B4 GROUP+HAVING profile showed SqlIsAggName at ~9% of CPU —
SqlEvalFunc checks it for every function in every row, and the
PRG body was two string allocations + a substring scan:
RETURN ("," + c + ",") $ ("," + AGG_FUNCTIONS + ",")
Replace with a hash lookup against the existing aggFuncSet map
in hbrtl/sqlexpr.go (already populated for SqlExprHasAgg, same
AGG_FUNCTIONS list). Upper-casing skips the allocation when the
input is already upper, which it almost always is in practice.
Bench deltas (median of 3 steady runs, 1000 iters):
B4_GROUP_HAVING 447 → 418 us -6.5%
B14_COUNT 252 → 235 us -7%
B15_CTE_WIN_JOIN 1595 → 1577 us -1%
Other benches unchanged (no aggregate calls per row).
FiveSql2 43/43, Harbour compat 56/56.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
395 lines
11 KiB
Plaintext
395 lines
11 KiB
Plaintext
/*
|
|
* 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. */
|
|
|
|
/* 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
|
|
RETURN SqlCoerceNum( xL ) + SqlCoerceNum( xR )
|
|
ENDIF
|
|
IF cOp == "-"
|
|
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
|