Major changes since last commit: - FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS - 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.) - @byref pass-by-reference via RefCell pattern - Mutable closure capture (EnsureLocalRef + RefCell sharing) - RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8) - DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display) - Reserved word guard (39 keywords blocked from function calls) - AEval arg order fix (element before index) - Closure capture redecl fix (unique _cap_ names per block) - Hash/string indexing in ArrayPush/ArrayPop - Harbour compat test suite: 51/51 - 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
337 lines
8.1 KiB
Plaintext
337 lines
8.1 KiB
Plaintext
/*
|
|
* 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
|
|
|
|
|