diff --git a/_FiveSql2/src/TSqlExecutor.prg b/_FiveSql2/src/TSqlExecutor.prg index 76e142c..a163d0d 100644 --- a/_FiveSql2/src/TSqlExecutor.prg +++ b/_FiveSql2/src/TSqlExecutor.prg @@ -596,16 +596,17 @@ METHOD EvalExpr( xNode ) CLASS TSqlExecutor CASE xNode[ 1 ] == ND_SUB IF xNode[ 2 ] != NIL - /* Use subquery cache for non-correlated subqueries */ - IF Len( s_aOuterStack ) == 0 - aSubResult := ::CacheSubquery( xNode[ 2 ] ) - ELSE - nSavedWA := Select() - ::PushOuter() - aSubResult := TSqlExecutor():New( xNode[ 2 ], ::aParams ):Run() - ::PopOuter() - dbSelectArea( nSavedWA ) - ENDIF + /* Subqueries are evaluated per outer row with outer context + * pushed so ::Resolve() can see parent aliases. The previous + * implementation only used this path when s_aOuterStack was + * already non-empty and cached the result at the top level — + * which silently broke correlated subqueries (they got the + * first row's result reused for every subsequent row). */ + nSavedWA := Select() + ::PushOuter() + aSubResult := TSqlExecutor():New( xNode[ 2 ], ::aParams ):Run() + ::PopOuter() + dbSelectArea( nSavedWA ) IF ValType( aSubResult ) == "A" .AND. Len( aSubResult ) >= 2 .AND. ; ValType( aSubResult[ 2 ] ) == "A" .AND. Len( aSubResult[ 2 ] ) > 0 .AND. ; Len( aSubResult[ 2 ][ 1 ] ) > 0 @@ -2741,6 +2742,8 @@ STATIC FUNCTION RecCteJoin( hRecQuery, aFN, aPrevRows, cCteName ) LOCAL aJoinOn, aJ LOCAL xCV LOCAL aCombFN, aCombRow + LOCAL cDbfKeyCol, cCteKeyCol, nDbfKeyIdx, nCteKeyIdx + LOCAL hCteHash, cKey, aMatches, m aCols := hRecQuery[ "columns" ] aResult := {} @@ -2862,43 +2865,140 @@ STATIC FUNCTION RecCteJoin( hRecQuery, aFN, aPrevRows, cCteName ) ENDIF NEXT - /* Nested-loop JOIN: dbfRow x cteRow */ - FOR i := 1 TO Len( aJoinRows ) - FOR j := 1 TO Len( aPrevRows ) - - /* Build combined row: [dbf fields..., cte fields..., dbf unqualified..., cte unqualified...] */ - aCombRow := {} - FOR nF := 1 TO Len( aJoinFN ) - AAdd( aCombRow, aJoinRows[ i ][ nF ] ) - NEXT - FOR nF := 1 TO Len( aFN ) - AAdd( aCombRow, aPrevRows[ j ][ nF ] ) - NEXT - FOR nF := 1 TO Len( aJoinFN ) - AAdd( aCombRow, aJoinRows[ i ][ nF ] ) - NEXT - FOR nF := 1 TO Len( aFN ) - AAdd( aCombRow, aPrevRows[ j ][ nF ] ) - NEXT - - /* Evaluate JOIN ON condition */ - lMatch := .T. - IF aJoinOn != NIL - xLeft := SqlEvalRowExpr( aJoinOn, aCombFN, aCombRow ) - lMatch := SqlIsTrue( xLeft ) + /* Try to extract a simple equi-join key from aJoinOn so we can use + * hash probing instead of O(m*n) nested loops. This is the dominant + * cost for WITH RECURSIVE hierarchy traversals where aJoinRows is + * the full DBF (hundreds/thousands of rows) and aPrevRows is the + * current frontier set. + * + * Looks for ON condition of shape `dbfAlias.col = cteAlias.col` or + * the reverse — anything more complex falls through to nested loop. */ + cDbfKeyCol := "" + cCteKeyCol := "" + IF aJoinOn != NIL .AND. ValType( aJoinOn ) == "A" .AND. Len( aJoinOn ) >= 4 .AND. ; + aJoinOn[ 1 ] == ND_BIN .AND. aJoinOn[ 2 ] == "=" .AND. ; + aJoinOn[ 3 ] != NIL .AND. aJoinOn[ 3 ][ 1 ] == ND_COL .AND. ; + aJoinOn[ 4 ] != NIL .AND. aJoinOn[ 4 ][ 1 ] == ND_COL + /* Split alias.col on both sides */ + cKey := Upper( aJoinOn[ 3 ][ 2 ] ) + IF "." $ cKey .AND. Left( cKey, At( ".", cKey ) - 1 ) == cCteAlias + cCteKeyCol := SubStr( cKey, At( ".", cKey ) + 1 ) + cKey := Upper( aJoinOn[ 4 ][ 2 ] ) + IF "." $ cKey + cDbfKeyCol := SubStr( cKey, At( ".", cKey ) + 1 ) + ELSE + cDbfKeyCol := cKey ENDIF + ELSE + cKey := Upper( aJoinOn[ 4 ][ 2 ] ) + IF "." $ cKey .AND. Left( cKey, At( ".", cKey ) - 1 ) == cCteAlias + cCteKeyCol := SubStr( cKey, At( ".", cKey ) + 1 ) + cKey := Upper( aJoinOn[ 3 ][ 2 ] ) + IF "." $ cKey + cDbfKeyCol := SubStr( cKey, At( ".", cKey ) + 1 ) + ELSE + cDbfKeyCol := cKey + ENDIF + ENDIF + ENDIF + ENDIF + + nDbfKeyIdx := 0 + nCteKeyIdx := 0 + IF ! Empty( cDbfKeyCol ) .AND. ! Empty( cCteKeyCol ) + FOR nF := 1 TO Len( aJoinFN ) + IF aJoinFN[ nF ] == cDbfKeyCol + nDbfKeyIdx := nF + EXIT + ENDIF + NEXT + FOR nF := 1 TO Len( aFN ) + IF Upper( aFN[ nF ] ) == cCteKeyCol + nCteKeyIdx := nF + EXIT + ENDIF + NEXT + ENDIF + + IF nDbfKeyIdx > 0 .AND. nCteKeyIdx > 0 + /* Hash-probe path: build hash on aPrevRows keyed by cte column, + * then scan aJoinRows and probe. Sub-linear vs nested loop. */ + hCteHash := { => } + FOR j := 1 TO Len( aPrevRows ) + cKey := SqlValToStr( aPrevRows[ j ][ nCteKeyIdx ] ) + IF ! hb_HHasKey( hCteHash, cKey ) + hCteHash[ cKey ] := {} + ENDIF + AAdd( hCteHash[ cKey ], j ) + NEXT + + FOR i := 1 TO Len( aJoinRows ) + cKey := SqlValToStr( aJoinRows[ i ][ nDbfKeyIdx ] ) + IF ! hb_HHasKey( hCteHash, cKey ) + LOOP + ENDIF + aMatches := hCteHash[ cKey ] + FOR m := 1 TO Len( aMatches ) + j := aMatches[ m ] + + aCombRow := {} + FOR nF := 1 TO Len( aJoinFN ) + AAdd( aCombRow, aJoinRows[ i ][ nF ] ) + NEXT + FOR nF := 1 TO Len( aFN ) + AAdd( aCombRow, aPrevRows[ j ][ nF ] ) + NEXT + FOR nF := 1 TO Len( aJoinFN ) + AAdd( aCombRow, aJoinRows[ i ][ nF ] ) + NEXT + FOR nF := 1 TO Len( aFN ) + AAdd( aCombRow, aPrevRows[ j ][ nF ] ) + NEXT - IF lMatch - /* Evaluate SELECT columns */ aNewRow := {} FOR k := 1 TO Len( aCols ) xCV := SqlEvalRowExpr( aCols[ k ][ 1 ], aCombFN, aCombRow ) AAdd( aNewRow, xCV ) NEXT AAdd( aResult, aNewRow ) - ENDIF + NEXT NEXT - NEXT + ELSE + /* Fallback: nested-loop JOIN for complex ON predicates */ + FOR i := 1 TO Len( aJoinRows ) + FOR j := 1 TO Len( aPrevRows ) + + aCombRow := {} + FOR nF := 1 TO Len( aJoinFN ) + AAdd( aCombRow, aJoinRows[ i ][ nF ] ) + NEXT + FOR nF := 1 TO Len( aFN ) + AAdd( aCombRow, aPrevRows[ j ][ nF ] ) + NEXT + FOR nF := 1 TO Len( aJoinFN ) + AAdd( aCombRow, aJoinRows[ i ][ nF ] ) + NEXT + FOR nF := 1 TO Len( aFN ) + AAdd( aCombRow, aPrevRows[ j ][ nF ] ) + NEXT + + lMatch := .T. + IF aJoinOn != NIL + xLeft := SqlEvalRowExpr( aJoinOn, aCombFN, aCombRow ) + lMatch := SqlIsTrue( xLeft ) + ENDIF + + IF lMatch + aNewRow := {} + FOR k := 1 TO Len( aCols ) + xCV := SqlEvalRowExpr( aCols[ k ][ 1 ], aCombFN, aCombRow ) + AAdd( aNewRow, xCV ) + NEXT + AAdd( aResult, aNewRow ) + ENDIF + NEXT + NEXT + ENDIF /* Close the workarea we opened */ IF ! Empty( cWAAlias )