Files
five/_FiveSql2/src/TSqlExpr.prg
CharlesKWON c84cde6175 perf(fivesql2): Go-native SqlIsAggName — drop per-row substring scan
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>
2026-04-18 13:40:19 +09:00

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