diff --git a/_FiveSql2/src/TSqlExecutor.prg b/_FiveSql2/src/TSqlExecutor.prg index 67c5101..2084054 100644 --- a/_FiveSql2/src/TSqlExecutor.prg +++ b/_FiveSql2/src/TSqlExecutor.prg @@ -95,6 +95,14 @@ METHOD New( hQuery, aParams ) CLASS TSqlExecutor ::nDepth := 0 ::aOpened := {} ::aTables := {} + /* Explicit fresh initialization — DATA INIT on hash/array literals + * can end up sharing the same instance across New() calls depending + * on the compile path, which would let one query's subquery cache + * leak into the next query's results. */ + ::hSubCorrCache := { => } + ::aSubCacheSlots := {} + ::aSemiJoinSlots := {} + ::nSubCacheSeq := 0 RETURN SELF @@ -1103,8 +1111,26 @@ METHOD RunSelect() CLASS TSqlExecutor LOCAL nEarlyLimit aCols := ::hQuery[ "columns" ] - ::aTables := ::hQuery[ "tables" ] - aJoins := ::hQuery[ "joins" ] + /* Deep-clone tables and joins so cross-run state (alias renames, + * fetch-cache references, etc.) doesn't leak between invocations + * of the same hQuery. A scalar correlated subquery that opens its + * FROM tables gets depth-suffixed temp aliases written back into + * aTables[i][2] and aJoins[i][3]; without this clone, the second + * call inherits the first call's dead alias and the JOIN sync + * loop below fails to match, leaving stale aliases that resolve + * to closed workareas. */ + ::aTables := AClone( ::hQuery[ "tables" ] ) + FOR i := 1 TO Len( ::aTables ) + IF ValType( ::aTables[ i ] ) == "A" + ::aTables[ i ] := AClone( ::aTables[ i ] ) + ENDIF + NEXT + aJoins := AClone( ::hQuery[ "joins" ] ) + FOR i := 1 TO Len( aJoins ) + IF ValType( aJoins[ i ] ) == "A" + aJoins[ i ] := AClone( aJoins[ i ] ) + ENDIF + NEXT xWhere := ::hQuery[ "where" ] aGroupBy := ::hQuery[ "group_by" ] xHaving := ::hQuery[ "having" ] @@ -1259,11 +1285,16 @@ METHOD RunSelect() CLASS TSqlExecutor ENDIF ELSEIF xArgExpr[ 1 ] != ND_COL /* Complex expression (CASE, BIN, etc.) inside aggregate: - * collect all leaf column references and add them as - * hidden result columns so they appear in fetched rows. */ - aLeafCols := SqlCollectCols( xArgExpr, NIL ) + * collect the original ND_COL leaf nodes and add them as + * hidden result columns so they appear in fetched rows. + * Must preserve the qualified name (e.g. "o.qty") so + * subqueries with JOINs resolve to the right workarea. + * Using bare names here used to send `price` to ord in + * a `FROM ord o JOIN prod p` query, silently yielding + * NIL/wrong row data. */ + aLeafCols := SqlCollectColExprs( xArgExpr, NIL ) FOR k := 1 TO Len( aLeafCols ) - cBare := aLeafCols[ k ] + cBare := aLeafCols[ k ][ 2 ] lFound := .F. FOR j := 1 TO Len( aResultExprs ) IF Upper( aResultExprs[ j ][ 2 ] ) == Upper( cBare ) @@ -1272,7 +1303,7 @@ METHOD RunSelect() CLASS TSqlExecutor ENDIF NEXT IF ! lFound - AAdd( aResultExprs, { SqlNode( ND_COL, cBare, NIL, NIL, NIL ), cBare } ) + AAdd( aResultExprs, { aLeafCols[ k ], cBare } ) ENDIF NEXT ENDIF @@ -1284,6 +1315,7 @@ METHOD RunSelect() CLASS TSqlExecutor AAdd( aFieldNames, aResultExprs[ i ][ 2 ] ) NEXT + /* Constant folding */ IF xWhere != NIL xWhere := SqlFoldConst( xWhere ) diff --git a/_FiveSql2/src/TSqlExpr.prg b/_FiveSql2/src/TSqlExpr.prg index 5f00c84..47fb282 100644 --- a/_FiveSql2/src/TSqlExpr.prg +++ b/_FiveSql2/src/TSqlExpr.prg @@ -282,6 +282,60 @@ FUNCTION SqlEvalRowExpr( xExpr, aFN, aRow ) 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 )