/* * TSqlAgg.prg — GROUP BY aggregation and HAVING filter * * FiveSql — SQL Engine for Harbour DBF/NTX * * Copyright (c) 2025 Charles KWON (Charles KWON OhJun) * Email: charleskwonohjun@gmail.com * * All rights reserved. */ #include "hbclass.ch" #include "FiveSqlDef.ch" CLASS TSqlAgg METHOD New() CONSTRUCTOR METHOD GroupBy( aRows, aFN, aCols, aGroupBy, xHaving, aTables, aParams ) METHOD ComputeAgg( xE, aGR, aFN ) METHOD FindColIdx( xExpr, aFN ) METHOD FindColIdx2( cN, aFN ) METHOD EvalHaving( xHaving, aNewRow, aCols, aGroupRows, aFN, aParams ) METHOD HasAgg( aCols ) METHOD EvalHavingExpr( xE, aNewRow, aCols, aGR, aFN, aParams ) ENDCLASS METHOD New() CLASS TSqlAgg RETURN SELF METHOD HasAgg( aCols ) CLASS TSqlAgg LOCAL i FOR i := 1 TO Len( aCols ) IF SqlExprHasAgg( aCols[ i ][ 1 ] ) RETURN .T. ENDIF NEXT RETURN .F. METHOD GroupBy( aRows, aFN, aCols, aGroupBy, xHaving, aTables, aParams ) CLASS TSqlAgg LOCAL hGroups := { => } LOCAL i, j, cKey, aGroupRows, aResult := {} LOCAL aNewRow LOCAL nGCol, cN, nCI, lPass /* Aggregate on empty set */ IF Len( aRows ) == 0 .AND. ::HasAgg( aCols ) aNewRow := {} FOR j := 1 TO Len( aCols ) IF SqlExprHasAgg( aCols[ j ][ 1 ] ) AAdd( aNewRow, 0 ) ELSE AAdd( aNewRow, NIL ) ENDIF NEXT RETURN { aNewRow } ENDIF /* Build group buckets */ IF Len( aGroupBy ) == 0 .AND. ::HasAgg( aCols ) hGroups[ "__ALL__" ] := aRows ELSE FOR i := 1 TO Len( aRows ) cKey := "" FOR j := 1 TO Len( aGroupBy ) nGCol := ::FindColIdx( aGroupBy[ j ], aFN ) IF nGCol > 0 .AND. nGCol <= Len( aRows[ i ] ) cKey += SqlValToStr( aRows[ i ][ nGCol ] ) + "|" ENDIF NEXT IF ! hb_HHasKey( hGroups, cKey ) hGroups[ cKey ] := {} ENDIF AAdd( hGroups[ cKey ], aRows[ i ] ) NEXT ENDIF /* Compute aggregates for each group */ FOR EACH aGroupRows IN hb_HValues( hGroups ) aNewRow := {} FOR j := 1 TO Len( aCols ) IF SqlExprHasAgg( aCols[ j ][ 1 ] ) AAdd( aNewRow, ::ComputeAgg( aCols[ j ][ 1 ], aGroupRows, aFN ) ) ELSE cN := SqlExprName( aCols[ j ][ 1 ] ) nCI := ::FindColIdx2( cN, aFN ) IF nCI > 0 .AND. Len( aGroupRows ) > 0 .AND. nCI <= Len( aGroupRows[ 1 ] ) AAdd( aNewRow, aGroupRows[ 1 ][ nCI ] ) ELSE AAdd( aNewRow, NIL ) ENDIF ENDIF NEXT /* HAVING filter */ IF xHaving != NIL lPass := ::EvalHaving( xHaving, aNewRow, aCols, aGroupRows, aFN, aParams ) IF ! lPass LOOP ENDIF ENDIF AAdd( aResult, aNewRow ) NEXT RETURN aResult METHOD FindColIdx( xExpr, aFN ) CLASS TSqlAgg LOCAL cN, i IF xExpr != NIL .AND. xExpr[ 1 ] == ND_COL cN := Upper( xExpr[ 2 ] ) IF "." $ cN cN := SubStr( cN, At( ".", cN ) + 1 ) ENDIF FOR i := 1 TO Len( aFN ) IF Upper( aFN[ i ] ) == cN RETURN i ENDIF NEXT ENDIF RETURN 0 METHOD FindColIdx2( cN, aFN ) CLASS TSqlAgg LOCAL i cN := Upper( cN ) FOR i := 1 TO Len( aFN ) IF Upper( aFN[ i ] ) == cN RETURN i ENDIF NEXT RETURN 0 METHOD ComputeAgg( xE, aGR, aFN ) CLASS TSqlAgg LOCAL cFunc, cArgName, nCol, i, xVal LOCAL nCount := 0, nSum := 0, xMin := NIL, xMax := NIL LOCAL cResult, cSep LOCAL xArg IF xE == NIL .OR. xE[ 1 ] != ND_FN RETURN 0 ENDIF cFunc := Upper( xE[ 2 ] ) IF Len( xE[ 3 ] ) > 0 xArg := xE[ 3 ][ 1 ] IF xArg[ 1 ] == ND_COL .AND. xArg[ 2 ] == "*" IF cFunc == "COUNT" RETURN Len( aGR ) ENDIF RETURN 0 ENDIF cArgName := SqlExprName( xArg ) ELSE IF cFunc == "COUNT" RETURN Len( aGR ) ENDIF RETURN 0 ENDIF nCol := ::FindColIdx2( cArgName, aFN ) IF nCol == 0 .AND. xArg[ 1 ] == ND_COL IF cFunc == "COUNT" RETURN Len( aGR ) ENDIF RETURN 0 ENDIF FOR i := 1 TO Len( aGR ) IF nCol > 0 .AND. nCol <= Len( aGR[ i ] ) xVal := aGR[ i ][ nCol ] ELSEIF nCol == 0 /* Complex expression (CASE, BIN, etc.) inside aggregate: * evaluate the expression tree against the current row data. */ xVal := SqlEvalRowExpr( xArg, aFN, aGR[ i ] ) ELSE xVal := NIL ENDIF IF xVal != NIL nCount++ nSum += SqlCoerceNum( xVal ) IF xMin == NIL .OR. SqlCoerceNum( xVal ) < SqlCoerceNum( xMin ) xMin := xVal ENDIF IF xMax == NIL .OR. SqlCoerceNum( xVal ) > SqlCoerceNum( xMax ) xMax := xVal ENDIF ENDIF NEXT DO CASE CASE cFunc == "COUNT" RETURN nCount CASE cFunc == "SUM" RETURN nSum CASE cFunc == "AVG" RETURN iif( nCount > 0, nSum / nCount, 0 ) CASE cFunc == "MIN" RETURN iif( xMin != NIL, xMin, 0 ) CASE cFunc == "MAX" RETURN iif( xMax != NIL, xMax, 0 ) CASE cFunc == "GROUP_CONCAT" .OR. cFunc == "STRING_AGG" cResult := "" cSep := ", " FOR i := 1 TO Len( aGR ) IF nCol > 0 .AND. nCol <= Len( aGR[ i ] ) xVal := aGR[ i ][ nCol ] ELSEIF nCol == 0 xVal := SqlEvalRowExpr( xArg, aFN, aGR[ i ] ) ELSE xVal := NIL ENDIF IF xVal != NIL IF ! Empty( cResult ) cResult += cSep ENDIF cResult += SqlCoerceStr( xVal ) ENDIF NEXT RETURN cResult ENDCASE RETURN 0 METHOD EvalHaving( xHaving, aNewRow, aCols, aGroupRows, aFN, aParams ) CLASS TSqlAgg LOCAL xResult xResult := ::EvalHavingExpr( xHaving, aNewRow, aCols, aGroupRows, aFN, aParams ) RETURN SqlIsTrue( xResult ) METHOD EvalHavingExpr( xE, aNewRow, aCols, aGR, aFN, aParams ) CLASS TSqlAgg LOCAL xL, xR, cOp, i, nCI, cN IF xE == NIL RETURN NIL ENDIF DO CASE CASE xE[ 1 ] == ND_LIT RETURN xE[ 2 ] CASE xE[ 1 ] == ND_NIL RETURN NIL CASE xE[ 1 ] == ND_COL cN := xE[ 2 ] IF "." $ cN cN := SubStr( cN, At( ".", cN ) + 1 ) ENDIF FOR i := 1 TO Len( aCols ) IF Upper( aCols[ i ][ 2 ] ) == Upper( cN ) .AND. i <= Len( aNewRow ) RETURN aNewRow[ i ] ENDIF NEXT nCI := ::FindColIdx2( cN, aFN ) IF nCI > 0 .AND. Len( aGR ) > 0 .AND. nCI <= Len( aGR[ 1 ] ) RETURN aGR[ 1 ][ nCI ] ENDIF RETURN NIL CASE xE[ 1 ] == ND_FN IF SqlIsAggName( xE[ 2 ] ) RETURN ::ComputeAgg( xE, aGR, aFN ) ENDIF RETURN NIL CASE xE[ 1 ] == ND_BIN cOp := xE[ 2 ] IF cOp == "AND" xL := ::EvalHavingExpr( xE[ 3 ], aNewRow, aCols, aGR, aFN, aParams ) xR := ::EvalHavingExpr( xE[ 4 ], aNewRow, aCols, aGR, aFN, aParams ) RETURN SqlIsTrue( xL ) .AND. SqlIsTrue( xR ) ENDIF IF cOp == "OR" xL := ::EvalHavingExpr( xE[ 3 ], aNewRow, aCols, aGR, aFN, aParams ) xR := ::EvalHavingExpr( xE[ 4 ], aNewRow, aCols, aGR, aFN, aParams ) RETURN SqlIsTrue( xL ) .OR. SqlIsTrue( xR ) ENDIF xL := ::EvalHavingExpr( xE[ 3 ], aNewRow, aCols, aGR, aFN, aParams ) xR := ::EvalHavingExpr( xE[ 4 ], aNewRow, aCols, aGR, aFN, aParams ) xL := SqlCoerceForCmp( xL ) xR := SqlCoerceForCmp( xR ) IF cOp == "=" .OR. cOp == "==" RETURN SqlCmpEq( xL, xR ) ENDIF IF cOp == "<>" .OR. cOp == "!=" RETURN ! SqlCmpEq( xL, xR ) ENDIF IF cOp == ">" RETURN SqlCmpLt( xR, xL ) ENDIF IF cOp == "<" RETURN SqlCmpLt( xL, xR ) ENDIF IF cOp == ">=" RETURN SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xR, xL ) ENDIF IF cOp == "<=" RETURN SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xL, xR ) ENDIF RETURN NIL CASE xE[ 1 ] == ND_UNI IF xE[ 2 ] == "NOT" xL := ::EvalHavingExpr( xE[ 3 ], aNewRow, aCols, aGR, aFN, aParams ) RETURN ! SqlIsTrue( xL ) ENDIF RETURN NIL ENDCASE RETURN NIL