/* * TSqlExecutor.prg — Main query executor with index optimization * * 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 "dbstruct.ch" #include "dbinfo.ch" #include "error.ch" #include "FiveSqlDef.ch" STATIC s_aOuterStack := {} STATIC s_hAutoInc := NIL STATIC s_nRCJSeq := 0 CLASS TSqlExecutor DATA hQuery DATA aParams DATA oIndex AS OBJECT DATA oAgg AS OBJECT DATA oSort AS OBJECT DATA oDDL AS OBJECT DATA oTxn AS OBJECT DATA oAlias AS OBJECT DATA nDepth INIT 0 DATA aOpened INIT {} DATA aTables INIT {} CLASSDATA hSubCache INIT { => } SHARED METHOD New( hQuery, aParams ) CONSTRUCTOR METHOD Run() METHOD RunSelect() METHOD RunInsert() METHOD RunUpdate() METHOD RunDelete() METHOD OpenTable( cTable, cAlias ) METHOD CloseOpened() METHOD FetchRow( aExprs ) METHOD EvalExpr( xNode ) METHOD Resolve( cRef ) METHOD FindWA( cAlias ) METHOD JoinRecurse( aJoins, nIdx, xWhere, aRE, aRows, hHashTbl ) METHOD RightJoinPass( aJoins, nIdx, aRE, aRows ) METHOD FetchRowNull( aRE, cInnerAlias ) METHOD ColBelongsTo( cColRef, cAlias ) METHOD PushOuter() METHOD PopOuter() METHOD ResolveFromOuter( cRef, cTblAlias, cField ) METHOD MakeError( nCode, cMsg ) METHOD HashJoin( nInnerWA, cInnerField, cOuterCol, xOnCond, aJoins, nIdx, xWhere, aRE, aRows, hHashTbl ) METHOD CacheSubquery( xSubExpr ) METHOD MaterializeCTE( aCTE ) METHOD MaterializeRecursiveCTE( aCTE ) METHOD ApplyWindowFunctions( aRows, aFN, aCols ) METHOD RunMerge() METHOD RunTruncate() ENDCLASS METHOD New( hQuery, aParams ) CLASS TSqlExecutor ::hQuery := hQuery ::aParams := iif( aParams == NIL, {}, aParams ) ::oIndex := TSqlIndex():New() ::oAgg := TSqlAgg():New() ::oSort := TSqlSort():New() ::oDDL := TSqlDDL():New() ::oTxn := TSqlTxn():New() ::oAlias := TSqlAlias():New() ::nDepth := 0 ::aOpened := {} ::aTables := {} RETURN SELF METHOD MakeError( nCode, cMsg ) CLASS TSqlExecutor RETURN { { "__error__" }, { { nCode, cMsg, "" } } } METHOD Run() CLASS TSqlExecutor LOCAL cType, aT, nP2 IF ::hQuery == NIL RETURN ::MakeError( SQL_ERR_SYNTAX, "Empty or invalid SQL" ) ENDIF cType := ::hQuery[ "type" ] DO CASE CASE cType == "SELECT" RETURN ::RunSelect() CASE cType == "INSERT" RETURN ::RunInsert() CASE cType == "UPDATE" RETURN ::RunUpdate() CASE cType == "DELETE" RETURN ::RunDelete() CASE cType == "CREATE" aT := ::hQuery[ "tokens" ] nP2 := ::hQuery[ "pos" ] IF ::oDDL:DDL_IsKW( aT, nP2, "TABLE" ) RETURN ::oDDL:CreateTable( aT, nP2 ) ELSEIF ::oDDL:DDL_IsKW( aT, nP2, "UNIQUE" ) .OR. ::oDDL:DDL_IsKW( aT, nP2, "INDEX" ) RETURN ::oDDL:CreateIndex( aT, nP2 ) ELSEIF ::oDDL:DDL_IsKW( aT, nP2, "VIEW" ) RETURN ::oDDL:CreateView( aT, nP2 ) ENDIF RETURN ::MakeError( SQL_ERR_UNSUPPORTED, "CREATE: unsupported object" ) CASE cType == "DROP" aT := ::hQuery[ "tokens" ] nP2 := ::hQuery[ "pos" ] IF ::oDDL:DDL_IsKW( aT, nP2, "TABLE" ) RETURN ::oDDL:DropTable( aT, nP2 ) ELSEIF ::oDDL:DDL_IsKW( aT, nP2, "INDEX" ) RETURN ::oDDL:DropIndex( aT, nP2 ) ELSEIF ::oDDL:DDL_IsKW( aT, nP2, "VIEW" ) RETURN ::oDDL:DropView( aT, nP2 ) ENDIF RETURN ::MakeError( SQL_ERR_UNSUPPORTED, "DROP: unsupported object" ) CASE cType == "SET_COLLATION" SqlSetCollation( ::hQuery[ "value" ] ) RETURN { { "result" }, { { "Collation set to " + ::hQuery[ "value" ] } } } CASE cType == "ALTER" aT := ::hQuery[ "tokens" ] nP2 := ::hQuery[ "pos" ] RETURN ::oDDL:AlterTable( aT, nP2 ) CASE cType == "BEGIN" RETURN ::oTxn:Begin() CASE cType == "COMMIT" RETURN ::oTxn:Commit() CASE cType == "ROLLBACK" RETURN ::oTxn:Rollback() CASE cType == "ROLLBACK_TO" RETURN ::oTxn:RollbackTo( ::hQuery[ "savepoint" ] ) CASE cType == "SAVEPOINT" RETURN ::oTxn:SetSavepoint( ::hQuery[ "name" ] ) CASE cType == "TRUNCATE" RETURN ::RunTruncate() CASE cType == "MERGE" RETURN ::RunMerge() ENDCASE RETURN ::MakeError( SQL_ERR_UNSUPPORTED, "Unknown statement type: " + cType ) METHOD OpenTable( cTable, cAlias ) CLASS TSqlExecutor LOCAL nWA, i, lFound nWA := ::oIndex:OpenTable( cTable, cAlias, .T., .T. ) IF nWA > 0 AAdd( ::aOpened, cAlias ) /* Register with alias manager if not already tracked */ lFound := .F. FOR i := 1 TO Len( ::oAlias:aSlots ) IF ::oAlias:aSlots[ i ][ 1 ] == cAlias ::oAlias:aSlots[ i ][ 4 ] := .T. lFound := .T. EXIT ENDIF NEXT IF ! lFound AAdd( ::oAlias:aSlots, { cAlias, Upper( cTable ), Upper( cAlias ), .T. } ) ENDIF ENDIF RETURN nWA METHOD CloseOpened() CLASS TSqlExecutor LOCAL i, nWA FOR i := 1 TO Len( ::aOpened ) nWA := Select( ::aOpened[ i ] ) IF nWA > 0 dbSelectArea( nWA ) dbCloseArea() ENDIF NEXT ::aOpened := {} ::oAlias:aSlots := {} RETURN NIL METHOD PushOuter() CLASS TSqlExecutor AAdd( s_aOuterStack, ::aTables ) RETURN NIL METHOD PopOuter() CLASS TSqlExecutor IF Len( s_aOuterStack ) > 0 ASize( s_aOuterStack, Len( s_aOuterStack ) - 1 ) ENDIF RETURN NIL METHOD FindWA( cAlias ) CLASS TSqlExecutor LOCAL i, nWA, cA, cOrig, cReal nWA := Select( cAlias ) IF nWA > 0 .AND. ( nWA )->( Used() ) RETURN nWA ENDIF FOR i := 1 TO Len( ::aTables ) cA := ::aTables[ i ][ 2 ] IF Empty( cA ) cA := ::aTables[ i ][ 1 ] ENDIF cOrig := "" IF Len( ::aTables[ i ] ) >= 3 cOrig := Upper( ::aTables[ i ][ 3 ] ) ENDIF IF Upper( cA ) == cAlias .OR. cOrig == cAlias .OR. Upper( ::aTables[ i ][ 1 ] ) == cAlias nWA := Select( cA ) IF nWA > 0 .AND. ( nWA )->( Used() ) RETURN nWA ENDIF ENDIF NEXT /* Fallback: check the alias manager for user alias mapping */ cReal := ::oAlias:RealAlias( cAlias ) IF ! Empty( cReal ) nWA := Select( cReal ) IF nWA > 0 .AND. ( nWA )->( Used() ) RETURN nWA ENDIF ENDIF RETURN 0 METHOD Resolve( cRef ) CLASS TSqlExecutor LOCAL cField, cTblAlias, nDot, nWA, nFPos, xVal, nSavedArea LOCAL i, cA LOCAL aCTEInfo, aCTEFN, aCTERows, nCTERow IF cRef == "*" RETURN NIL ENDIF nSavedArea := Select() nDot := At( ".", cRef ) IF nDot > 0 cTblAlias := Upper( Left( cRef, nDot - 1 ) ) cField := Upper( SubStr( cRef, nDot + 1 ) ) ELSE cField := Upper( cRef ) cTblAlias := "" ENDIF /* Qualified reference */ IF ! Empty( cTblAlias ) nWA := ::FindWA( cTblAlias ) IF nWA > 0 dbSelectArea( nWA ) nFPos := FieldPos( cField ) IF nFPos > 0 xVal := FieldGet( nFPos ) dbSelectArea( nSavedArea ) RETURN xVal ENDIF dbSelectArea( nSavedArea ) ENDIF IF Len( s_aOuterStack ) > 0 xVal := ::ResolveFromOuter( cRef, cTblAlias, cField ) IF xVal != NIL dbSelectArea( nSavedArea ) RETURN xVal ENDIF ENDIF dbSelectArea( nSavedArea ) RETURN NIL ENDIF /* Unqualified: search all tables */ FOR i := 1 TO Len( ::aTables ) cA := ::aTables[ i ][ 2 ] IF Empty( cA ) cA := ::aTables[ i ][ 1 ] ENDIF nWA := Select( cA ) IF nWA > 0 dbSelectArea( nWA ) nFPos := FieldPos( cField ) IF nFPos > 0 xVal := FieldGet( nFPos ) dbSelectArea( nSavedArea ) RETURN xVal ENDIF ENDIF NEXT /* Last resort: current workarea */ dbSelectArea( nSavedArea ) nFPos := FieldPos( cField ) IF nFPos > 0 RETURN FieldGet( nFPos ) ENDIF /* Correlated subquery outer context */ IF Len( s_aOuterStack ) > 0 xVal := ::ResolveFromOuter( cRef, cTblAlias, cField ) IF xVal != NIL dbSelectArea( nSavedArea ) RETURN xVal ENDIF ENDIF dbSelectArea( nSavedArea ) RETURN NIL METHOD ResolveFromOuter( cRef, cTblAlias, cField ) CLASS TSqlExecutor LOCAL i, j, aOuterTbls, cA, nWA, nFPos, xVal, nSavedArea nSavedArea := Select() FOR i := Len( s_aOuterStack ) TO 1 STEP -1 aOuterTbls := s_aOuterStack[ i ] FOR j := 1 TO Len( aOuterTbls ) cA := aOuterTbls[ j ][ 2 ] IF Empty( cA ) cA := aOuterTbls[ j ][ 1 ] ENDIF IF ! Empty( cTblAlias ) IF !( Upper( cA ) == cTblAlias .OR. ; Upper( aOuterTbls[ j ][ 1 ] ) == cTblAlias .OR. ; ( Len( aOuterTbls[ j ] ) >= 3 .AND. Upper( aOuterTbls[ j ][ 3 ] ) == cTblAlias ) ) LOOP ENDIF ENDIF nWA := Select( cA ) IF nWA > 0 dbSelectArea( nWA ) nFPos := FieldPos( cField ) IF nFPos > 0 xVal := FieldGet( nFPos ) dbSelectArea( nSavedArea ) RETURN xVal ENDIF ENDIF NEXT NEXT dbSelectArea( nSavedArea ) RETURN NIL METHOD EvalExpr( xNode ) CLASS TSqlExecutor LOCAL xL, xR, cOp, xVal, aArgs, aVals, i, xResult, nPI LOCAL aCases, xElse, xCond LOCAL aSubResult, xHi, nSavedWA IF xNode == NIL RETURN NIL ENDIF DO CASE CASE xNode[ 1 ] == ND_LIT RETURN xNode[ 2 ] CASE xNode[ 1 ] == ND_NIL RETURN NIL CASE xNode[ 1 ] == ND_COL RETURN ::Resolve( xNode[ 2 ] ) CASE xNode[ 1 ] == ND_PAR IF Len( ::aParams ) > 0 /* Use static counter per expression evaluation chain */ xVal := ::aParams[ 1 ] RETURN xVal ENDIF RETURN NIL CASE xNode[ 1 ] == ND_UNI cOp := xNode[ 2 ] xL := ::EvalExpr( xNode[ 3 ] ) IF cOp == "NOT" IF ValType( xL ) == "L" RETURN ! xL ENDIF RETURN .F. ENDIF IF cOp == "-" IF ValType( xL ) == "N" RETURN -xL ENDIF RETURN 0 ENDIF RETURN xL CASE xNode[ 1 ] == ND_BIN cOp := xNode[ 2 ] /* Short-circuit AND */ IF cOp == "AND" xL := ::EvalExpr( xNode[ 3 ] ) IF ValType( xL ) == "L" .AND. ! xL RETURN .F. ENDIF xR := ::EvalExpr( xNode[ 4 ] ) RETURN SqlIsTrue( xL ) .AND. SqlIsTrue( xR ) ENDIF /* Short-circuit OR */ IF cOp == "OR" xL := ::EvalExpr( xNode[ 3 ] ) IF ValType( xL ) == "L" .AND. xL RETURN .T. ENDIF xR := ::EvalExpr( xNode[ 4 ] ) RETURN SqlIsTrue( xL ) .OR. SqlIsTrue( xR ) ENDIF /* IN operator */ IF cOp == "IN" xL := ::EvalExpr( xNode[ 3 ] ) xR := xNode[ 4 ] IF xR != NIL .AND. xR[ 1 ] == ND_LIST aVals := xR[ 2 ] FOR i := 1 TO Len( aVals ) xVal := ::EvalExpr( aVals[ i ] ) IF SqlCmpEq( xL, xVal ) RETURN .T. ENDIF NEXT RETURN .F. ENDIF IF xR != NIL .AND. xR[ 1 ] == ND_SUB .AND. xR[ 2 ] != NIL /* Use subquery cache for non-correlated subqueries */ IF Len( s_aOuterStack ) == 0 aSubResult := ::CacheSubquery( xR[ 2 ] ) ELSE nSavedWA := Select() ::PushOuter() aSubResult := TSqlExecutor():New( xR[ 2 ], ::aParams ):Run() ::PopOuter() dbSelectArea( nSavedWA ) ENDIF IF ValType( aSubResult ) == "A" .AND. Len( aSubResult ) >= 2 .AND. ; ValType( aSubResult[ 2 ] ) == "A" FOR i := 1 TO Len( aSubResult[ 2 ] ) IF Len( aSubResult[ 2 ][ i ] ) > 0 .AND. ; SqlCmpEq( xL, aSubResult[ 2 ][ i ][ 1 ] ) RETURN .T. ENDIF NEXT ENDIF RETURN .F. ENDIF RETURN .F. ENDIF /* IS NULL / IS NOT NULL */ IF cOp == "IS NULL" .OR. cOp == "IS NOT NULL" xL := ::EvalExpr( xNode[ 3 ] ) IF cOp == "IS NULL" RETURN xL == NIL .OR. ( ValType( xL ) == "C" .AND. Empty( AllTrim( xL ) ) ) ELSE RETURN !( xL == NIL .OR. ( ValType( xL ) == "C" .AND. Empty( AllTrim( xL ) ) ) ) ENDIF ENDIF /* Standard binary ops */ xL := ::EvalExpr( xNode[ 3 ] ) xR := ::EvalExpr( xNode[ 4 ] ) 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( 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 == "LIKE" IF xNode[ 5 ] != NIL RETURN SqlLikeMatch( SqlCoerceStr( xL ), SqlCoerceStr( xR ), SqlCoerceStr( ::EvalExpr( xNode[ 5 ] ) ) ) ENDIF RETURN SqlLikeMatch( SqlCoerceStr( xL ), SqlCoerceStr( xR ) ) ENDIF IF cOp == "+" IF ValType( xL ) == "C" .AND. ValType( xR ) == "C" 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 RETURN NIL CASE xNode[ 1 ] == ND_RANGE xL := ::EvalExpr( xNode[ 3 ] ) xR := ::EvalExpr( xNode[ 4 ] ) xHi := ::EvalExpr( xNode[ 5 ] ) xL := SqlCoerceForCmp( xL ) xR := SqlCoerceForCmp( xR ) xHi := SqlCoerceForCmp( xHi ) RETURN ( SqlCmpEq( xL, xR ) .OR. SqlCmpLt( xR, xL ) ) .AND. ( SqlCmpEq( xL, xHi ) .OR. SqlCmpLt( xL, xHi ) ) CASE xNode[ 1 ] == ND_CASE aCases := xNode[ 2 ] xElse := xNode[ 3 ] FOR i := 1 TO Len( aCases ) xCond := ::EvalExpr( aCases[ i ][ 1 ] ) IF SqlIsTrue( xCond ) RETURN ::EvalExpr( aCases[ i ][ 2 ] ) ENDIF NEXT IF xElse != NIL RETURN ::EvalExpr( xElse ) ENDIF RETURN NIL CASE xNode[ 1 ] == ND_FN /* EXISTS must be handled before argument evaluation */ IF xNode[ 2 ] == "EXISTS" .AND. Len( xNode[ 3 ] ) > 0 .AND. ; xNode[ 3 ][ 1 ] != NIL .AND. ValType( xNode[ 3 ][ 1 ] ) == "A" .AND. ; xNode[ 3 ][ 1 ][ 1 ] == ND_SUB .AND. xNode[ 3 ][ 1 ][ 2 ] != NIL nSavedWA := Select() ::PushOuter() aSubResult := TSqlExecutor():New( xNode[ 3 ][ 1 ][ 2 ], ::aParams ):Run() ::PopOuter() dbSelectArea( nSavedWA ) IF ValType( aSubResult ) == "A" .AND. Len( aSubResult ) >= 2 .AND. ; ValType( aSubResult[ 2 ] ) == "A" RETURN Len( aSubResult[ 2 ] ) > 0 ENDIF RETURN .F. ENDIF /* Evaluate arguments */ aArgs := {} FOR i := 1 TO Len( xNode[ 3 ] ) AAdd( aArgs, ::EvalExpr( xNode[ 3 ][ i ] ) ) NEXT RETURN SqlEvalFunc( xNode[ 2 ], aArgs ) 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 IF ValType( aSubResult ) == "A" .AND. Len( aSubResult ) >= 2 .AND. ; ValType( aSubResult[ 2 ] ) == "A" .AND. Len( aSubResult[ 2 ] ) > 0 .AND. ; Len( aSubResult[ 2 ][ 1 ] ) > 0 RETURN aSubResult[ 2 ][ 1 ][ 1 ] ENDIF ENDIF RETURN NIL CASE xNode[ 1 ] == ND_WINDOW /* Window functions are evaluated post-fetch, return placeholder */ RETURN 0 ENDCASE RETURN NIL METHOD FetchRow( aExprs ) CLASS TSqlExecutor LOCAL aRow := {}, i, xVal LOCAL xE, cRef, nDot, nWA, nFPos, cField, cTblAlias, cA FOR i := 1 TO Len( aExprs ) xE := aExprs[ i ][ 1 ] /* Fast path for column references */ IF xE[ 1 ] == ND_COL .AND. xE[ 2 ] != "*" .AND. Len( ::aTables ) > 0 cRef := xE[ 2 ] nDot := At( ".", cRef ) IF nDot > 0 cTblAlias := Upper( Left( cRef, nDot - 1 ) ) cField := Upper( SubStr( cRef, nDot + 1 ) ) nWA := ::FindWA( cTblAlias ) ELSE cField := Upper( cRef ) cA := ::aTables[ 1 ][ 2 ] IF Empty( cA ) cA := ::aTables[ 1 ][ 1 ] ENDIF nWA := Select( cA ) ENDIF IF nWA > 0 dbSelectArea( nWA ) nFPos := FieldPos( cField ) IF nFPos > 0 xVal := FieldGet( nFPos ) IF ValType( xVal ) == "C" xVal := AllTrim( xVal ) ENDIF AAdd( aRow, xVal ) LOOP ENDIF ENDIF ENDIF /* General expression evaluation path */ xVal := ::EvalExpr( xE ) IF ValType( xVal ) == "C" xVal := AllTrim( xVal ) ENDIF AAdd( aRow, xVal ) NEXT RETURN aRow METHOD ColBelongsTo( cColRef, cAlias ) CLASS TSqlExecutor LOCAL cPrefix, nDot, i, cA, cOrig nDot := At( ".", cColRef ) IF nDot == 0 RETURN .F. ENDIF cPrefix := Upper( Left( cColRef, nDot - 1 ) ) IF cPrefix == Upper( cAlias ) RETURN .T. ENDIF FOR i := 1 TO Len( ::aTables ) cA := Upper( ::aTables[ i ][ 2 ] ) IF Empty( cA ) cA := Upper( ::aTables[ i ][ 1 ] ) ENDIF cOrig := "" IF Len( ::aTables[ i ] ) >= 3 cOrig := Upper( ::aTables[ i ][ 3 ] ) ENDIF IF cA == Upper( cAlias ) .OR. cOrig == Upper( cAlias ) IF cPrefix == cA .OR. cPrefix == cOrig .OR. cPrefix == Upper( ::aTables[ i ][ 1 ] ) RETURN .T. ENDIF ENDIF NEXT RETURN .F. METHOD FetchRowNull( aRE, cInnerAlias ) CLASS TSqlExecutor LOCAL aRow := {}, i, xVal LOCAL cColRef, lIsInner, nWA, nFPos, cBareField FOR i := 1 TO Len( aRE ) cColRef := "" IF aRE[ i ][ 1 ] != NIL .AND. aRE[ i ][ 1 ][ 1 ] == ND_COL cColRef := Upper( aRE[ i ][ 1 ][ 2 ] ) ENDIF lIsInner := .F. IF ! Empty( cColRef ) IF ::ColBelongsTo( cColRef, cInnerAlias ) lIsInner := .T. ELSEIF ! ( "." $ cColRef ) nWA := Select( cInnerAlias ) IF nWA > 0 dbSelectArea( nWA ) nFPos := FieldPos( cColRef ) IF nFPos > 0 lIsInner := .T. IF Len( ::aTables ) > 0 cBareField := cColRef nWA := ::FindWA( ::aTables[ 1 ][ 2 ] ) IF nWA == 0 nWA := ::FindWA( ::aTables[ 1 ][ 1 ] ) ENDIF IF nWA > 0 dbSelectArea( nWA ) IF FieldPos( cBareField ) > 0 lIsInner := .F. ENDIF ENDIF ENDIF ENDIF ENDIF ENDIF ENDIF IF lIsInner AAdd( aRow, NIL ) ELSE xVal := ::EvalExpr( aRE[ i ][ 1 ] ) IF ValType( xVal ) == "C" xVal := AllTrim( xVal ) ENDIF AAdd( aRow, xVal ) ENDIF NEXT RETURN aRow METHOD JoinRecurse( aJoins, nIdx, xWhere, aRE, aRows, hHashTbl ) CLASS TSqlExecutor LOCAL cJAlias, xOnCond, nWA, aRow LOCAL lJoinMatch LOCAL cOuterCol, cInnerCol, cInnerField, xSeekVal, cSeekStr LOCAL lUseIndex, lFound, nPI LOCAL cJoinType, lHadMatch LOCAL nRecCount, lUseHash IF hHashTbl == NIL hHashTbl := { => } ENDIF IF nIdx > Len( aJoins ) IF xWhere == NIL .OR. SqlIsTrue( ::EvalExpr( xWhere ) ) aRow := ::FetchRow( aRE ) AAdd( aRows, aRow ) ENDIF RETURN NIL ENDIF cJoinType := Upper( aJoins[ nIdx ][ 1 ] ) cJAlias := aJoins[ nIdx ][ 3 ] IF Empty( cJAlias ) cJAlias := aJoins[ nIdx ][ 2 ] ENDIF xOnCond := aJoins[ nIdx ][ 4 ] nWA := Select( cJAlias ) IF nWA == 0 /* Try the join table name directly (handles CTE alias mismatch) */ nWA := Select( Upper( aJoins[ nIdx ][ 2 ] ) ) ENDIF IF nWA == 0 RETURN NIL ENDIF /* CROSS JOIN */ IF cJoinType == "CROSS" dbSelectArea( nWA ) dbGoTop() WHILE ! Eof() ::JoinRecurse( aJoins, nIdx + 1, xWhere, aRE, @aRows, hHashTbl ) dbSelectArea( nWA ) dbSkip() ENDDO RETURN NIL ENDIF lHadMatch := .F. lUseIndex := .F. lUseHash := .F. cOuterCol := "" cInnerCol := "" cInnerField := "" /* Analyze ON condition for index or hash join optimization */ IF xOnCond != NIL .AND. xOnCond[ 1 ] == ND_BIN .AND. xOnCond[ 2 ] == "=" IF xOnCond[ 3 ] != NIL .AND. xOnCond[ 3 ][ 1 ] == ND_COL .AND. ; xOnCond[ 4 ] != NIL .AND. xOnCond[ 4 ][ 1 ] == ND_COL IF ::ColBelongsTo( xOnCond[ 4 ][ 2 ], cJAlias ) cOuterCol := xOnCond[ 3 ][ 2 ] cInnerCol := xOnCond[ 4 ][ 2 ] ELSEIF ::ColBelongsTo( xOnCond[ 3 ][ 2 ], cJAlias ) cOuterCol := xOnCond[ 4 ][ 2 ] cInnerCol := xOnCond[ 3 ][ 2 ] ENDIF ENDIF IF ! Empty( cInnerCol ) IF "." $ cInnerCol cInnerField := Upper( SubStr( cInnerCol, At( ".", cInnerCol ) + 1 ) ) ELSE cInnerField := Upper( cInnerCol ) ENDIF dbSelectArea( nWA ) lUseIndex := ( ::oIndex:FindBestTag( nWA, cInnerField ) > 0 ) /* SQLite strategy: always use hash join for equi-joins when no index. * Build ephemeral hash table on first probe, O(m) build + O(1) lookup. * No threshold — even small tables benefit from avoiding repeated scans. */ IF ! lUseIndex .AND. ! Empty( cOuterCol ) lUseHash := .T. ENDIF ENDIF ENDIF IF lUseIndex xSeekVal := ::EvalExpr( SqlNode( ND_COL, cOuterCol, NIL, NIL, NIL ) ) dbSelectArea( nWA ) cSeekStr := ::oIndex:BuildKey( nWA, xSeekVal ) lFound := dbSeek( cSeekStr ) WHILE lFound .AND. ! Eof() lJoinMatch := SqlIsTrue( ::EvalExpr( xOnCond ) ) IF ! lJoinMatch EXIT ENDIF lHadMatch := .T. ::JoinRecurse( aJoins, nIdx + 1, xWhere, aRE, @aRows, hHashTbl ) dbSelectArea( nWA ) dbSkip() IF Eof() EXIT ENDIF ENDDO ELSEIF lUseHash /* Hash join path for equi-joins on large tables without index */ lHadMatch := ::HashJoin( nWA, cInnerField, cOuterCol, xOnCond, ; aJoins, nIdx, xWhere, aRE, @aRows, @hHashTbl ) ELSE dbSelectArea( nWA ) dbGoTop() WHILE ! Eof() lJoinMatch := .T. IF xOnCond != NIL lJoinMatch := SqlIsTrue( ::EvalExpr( xOnCond ) ) ENDIF IF lJoinMatch lHadMatch := .T. ::JoinRecurse( aJoins, nIdx + 1, xWhere, aRE, @aRows, hHashTbl ) ENDIF dbSelectArea( nWA ) dbSkip() ENDDO ENDIF /* LEFT JOIN NULL fill */ IF ! lHadMatch .AND. ( cJoinType == "LEFT" .OR. cJoinType == "FULL" ) IF nIdx >= Len( aJoins ) aRow := ::FetchRowNull( aRE, cJAlias ) IF xWhere == NIL .OR. SqlIsTrue( ::EvalExpr( xWhere ) ) AAdd( aRows, aRow ) ENDIF ENDIF ENDIF RETURN NIL METHOD RightJoinPass( aJoins, nJIdx, aRE, aRows ) CLASS TSqlExecutor LOCAL cJAlias, xOnCond, nWA, nOuterWA, cOuterAlias LOCAL lMatched, aRow, j LOCAL cColRef cJAlias := aJoins[ nJIdx ][ 3 ] IF Empty( cJAlias ) cJAlias := aJoins[ nJIdx ][ 2 ] ENDIF xOnCond := aJoins[ nJIdx ][ 4 ] nWA := Select( cJAlias ) IF nWA == 0 RETURN NIL ENDIF cOuterAlias := "" IF Len( ::aTables ) > 0 cOuterAlias := ::aTables[ 1 ][ 2 ] IF Empty( cOuterAlias ) cOuterAlias := ::aTables[ 1 ][ 1 ] ENDIF ENDIF nOuterWA := Select( cOuterAlias ) IF nOuterWA == 0 RETURN NIL ENDIF dbSelectArea( nWA ) dbGoTop() WHILE ! Eof() lMatched := .F. dbSelectArea( nOuterWA ) dbGoTop() WHILE ! Eof() IF xOnCond != NIL .AND. SqlIsTrue( ::EvalExpr( xOnCond ) ) lMatched := .T. EXIT ENDIF dbSelectArea( nOuterWA ) dbSkip() ENDDO IF ! lMatched dbSelectArea( nWA ) aRow := {} FOR j := 1 TO Len( aRE ) cColRef := "" IF aRE[ j ][ 1 ] != NIL .AND. aRE[ j ][ 1 ][ 1 ] == ND_COL cColRef := Upper( aRE[ j ][ 1 ][ 2 ] ) ENDIF IF ! Empty( cColRef ) .AND. ::ColBelongsTo( cColRef, cOuterAlias ) AAdd( aRow, NIL ) ELSE AAdd( aRow, ::EvalExpr( aRE[ j ][ 1 ] ) ) ENDIF NEXT AAdd( aRows, aRow ) ENDIF dbSelectArea( nWA ) dbSkip() ENDDO RETURN NIL METHOD RunSelect() CLASS TSqlExecutor LOCAL aCols, aJoins, xWhere, aGroupBy, xHaving, aOrderBy LOCAL nTop, nLimit, nOffset, lDistinct, hUnion LOCAL aFieldNames := {}, aRows := {}, aRow LOCAL aSavedAreas := {} LOCAL cTable, cAlias, nWA, i, j LOCAL aResultExprs LOCAL xExpr, cColAlias, cFN LOCAL nMaxRows LOCAL aU, lAll LOCAL xArgExpr, cBare, lFound, aLeafCols, k LOCAL hJoinHash LOCAL lIndexUsed, aTmp aCols := ::hQuery[ "columns" ] ::aTables := ::hQuery[ "tables" ] aJoins := ::hQuery[ "joins" ] xWhere := ::hQuery[ "where" ] aGroupBy := ::hQuery[ "group_by" ] xHaving := ::hQuery[ "having" ] aOrderBy := ::hQuery[ "order_by" ] nTop := ::hQuery[ "top" ] nLimit := ::hQuery[ "limit" ] nOffset := iif( hb_HHasKey( ::hQuery, "offset" ), ::hQuery[ "offset" ], 0 ) lDistinct := ::hQuery[ "distinct" ] hUnion := ::hQuery[ "union" ] AAdd( aSavedAreas, Select() ) ::nDepth++ /* Materialize CTEs if present */ IF hb_HHasKey( ::hQuery, "cte" ) .AND. ValType( ::hQuery[ "cte" ] ) == "A" IF hb_HHasKey( ::hQuery, "cte_recursive" ) .AND. ::hQuery[ "cte_recursive" ] ::MaterializeRecursiveCTE( ::hQuery[ "cte" ] ) ELSE ::MaterializeCTE( ::hQuery[ "cte" ] ) ENDIF ENDIF /* Handle derived tables */ FOR i := 1 TO Len( ::aTables ) IF ::aTables[ i ][ 1 ] == "__SUBQUERY__" .AND. ; ValType( ::aTables[ i ][ 3 ] ) == "A" .AND. ; ::aTables[ i ][ 3 ][ 1 ] == ND_SUB cAlias := ::aTables[ i ][ 2 ] IF Empty( cAlias ) cAlias := ::oAlias:AcquireTemp( "DRV" ) ENDIF ::aTables[ i ] := SqlMaterializeSubquery( ::aTables[ i ][ 3 ], cAlias, ::aParams ) ENDIF NEXT /* Open all referenced tables */ FOR i := 1 TO Len( ::aTables ) cTable := ::aTables[ i ][ 1 ] cAlias := ::aTables[ i ][ 2 ] IF Empty( cAlias ) cAlias := cTable ENDIF IF Len( cAlias ) <= 1 ::aTables[ i ][ 3 ] := cAlias ENDIF IF Len( cAlias ) <= 1 .OR. ::nDepth > 1 cAlias := ::oAlias:AcquireTemp( Upper( cTable ) ) ::aTables[ i ][ 2 ] := cAlias ENDIF nWA := Select( cAlias ) IF nWA == 0 nWA := ::OpenTable( cTable, cAlias ) IF nWA == 0 /* Table file not found; check if a CTE temp file exists for this * table name and open it instead. This handles sub-executors * (UNION, recursive) that reference a CTE by its original name. */ IF hb_FileExists( "__cte_" + Lower( cTable ) + ".dbf" ) BEGIN SEQUENCE dbUseArea( .T., "DBFNTX", "__cte_" + Lower( cTable ) + ".dbf", ; cAlias, .T., .T. ) nWA := Select( cAlias ) AAdd( ::aOpened, cAlias ) AAdd( ::oAlias:aSlots, { cAlias, Upper( cTable ), Upper( cTable ), .T. } ) RECOVER nWA := 0 END SEQUENCE ENDIF ENDIF IF nWA == -1 ::nDepth-- IF Len( aSavedAreas ) > 0 dbSelectArea( aSavedAreas[ 1 ] ) ENDIF RETURN ::MakeError( SQL_ERR_LOCKED, ; "Table '" + cTable + "' is open EXCLUSIVE. " + ; "Close it or reopen with SHARED access before running SQL queries." ) ENDIF ENDIF NEXT /* Synchronize join aliases with the aTables entries that were * potentially renamed by the alias manager above. */ FOR i := 1 TO Len( aJoins ) IF Empty( aJoins[ i ][ 3 ] ) aJoins[ i ][ 3 ] := aJoins[ i ][ 2 ] ENDIF /* Find matching aTables entry and adopt its (possibly renamed) alias */ FOR j := 1 TO Len( ::aTables ) IF Upper( ::aTables[ j ][ 1 ] ) == Upper( aJoins[ i ][ 2 ] ) .AND. ; ( Upper( ::aTables[ j ][ 3 ] ) == Upper( aJoins[ i ][ 3 ] ) .OR. ; ::aTables[ j ][ 2 ] == aJoins[ i ][ 3 ] .OR. ; Upper( ::aTables[ j ][ 1 ] ) == Upper( aJoins[ i ][ 3 ] ) ) aJoins[ i ][ 3 ] := ::aTables[ j ][ 2 ] EXIT ENDIF NEXT NEXT /* Build result column names and expression trees */ aResultExprs := {} FOR i := 1 TO Len( aCols ) xExpr := aCols[ i ][ 1 ] cColAlias := aCols[ i ][ 2 ] IF Empty( cColAlias ) cColAlias := SqlExprName( xExpr ) ENDIF AAdd( aResultExprs, { xExpr, cColAlias } ) /* Expand SELECT * */ IF xExpr[ 1 ] == ND_COL .AND. xExpr[ 2 ] == "*" aResultExprs := {} aFieldNames := {} IF Len( ::aTables ) > 0 cAlias := ::aTables[ 1 ][ 2 ] IF Empty( cAlias ) cAlias := ::aTables[ 1 ][ 1 ] ENDIF nWA := Select( cAlias ) IF nWA > 0 dbSelectArea( nWA ) FOR j := 1 TO FCount() cFN := Upper( AllTrim( FieldName( j ) ) ) AAdd( aResultExprs, { SqlNode( ND_COL, cFN, NIL, NIL, NIL ), cFN } ) NEXT ENDIF ENDIF EXIT ENDIF NEXT /* Add hidden columns for aggregate source fields */ FOR i := 1 TO Len( aCols ) IF SqlExprHasAgg( aCols[ i ][ 1 ] ) IF aCols[ i ][ 1 ][ 1 ] == ND_FN .AND. Len( aCols[ i ][ 1 ][ 3 ] ) > 0 xArgExpr := aCols[ i ][ 1 ][ 3 ][ 1 ] IF xArgExpr[ 1 ] == ND_COL .AND. xArgExpr[ 2 ] != "*" cBare := SqlExprName( xArgExpr ) lFound := .F. FOR j := 1 TO Len( aResultExprs ) IF Upper( aResultExprs[ j ][ 2 ] ) == Upper( cBare ) lFound := .T. EXIT ENDIF NEXT IF ! lFound AAdd( aResultExprs, { xArgExpr, cBare } ) 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 ) FOR k := 1 TO Len( aLeafCols ) cBare := aLeafCols[ k ] lFound := .F. FOR j := 1 TO Len( aResultExprs ) IF Upper( aResultExprs[ j ][ 2 ] ) == Upper( cBare ) lFound := .T. EXIT ENDIF NEXT IF ! lFound AAdd( aResultExprs, { SqlNode( ND_COL, cBare, NIL, NIL, NIL ), cBare } ) ENDIF NEXT ENDIF ENDIF ENDIF NEXT FOR i := 1 TO Len( aResultExprs ) AAdd( aFieldNames, aResultExprs[ i ][ 2 ] ) NEXT /* Constant folding */ IF xWhere != NIL xWhere := SqlFoldConst( xWhere ) ENDIF FOR i := 1 TO Len( aResultExprs ) aResultExprs[ i ][ 1 ] := SqlFoldConst( aResultExprs[ i ][ 1 ] ) NEXT SET DELETED ON /* SELECT without FROM */ IF Len( ::aTables ) == 0 aRow := ::FetchRow( aResultExprs ) AAdd( aRows, aRow ) ENDIF /* Scan primary table */ IF Len( ::aTables ) > 0 cAlias := ::aTables[ 1 ][ 2 ] IF Empty( cAlias ) cAlias := ::aTables[ 1 ][ 1 ] ENDIF nWA := Select( cAlias ) IF nWA > 0 dbSelectArea( nWA ) lIndexUsed := .F. IF Len( aJoins ) == 0 .AND. xWhere != NIL lIndexUsed := ::oIndex:TryIndexScan( nWA, xWhere, xWhere, ; ::aTables, ::aParams, aResultExprs, @aRows ) ELSEIF Len( aJoins ) > 0 .AND. xWhere != NIL lIndexUsed := ::oIndex:TryIndexJoinScan( nWA, xWhere, ; ::aTables, ::aParams, aResultExprs, @aRows, aJoins ) ENDIF IF ! lIndexUsed dbSelectArea( nWA ) dbGoTop() hJoinHash := { => } WHILE ! Eof() IF Len( aJoins ) > 0 ::JoinRecurse( aJoins, 1, xWhere, aResultExprs, @aRows, hJoinHash ) dbSelectArea( nWA ) ELSE IF xWhere == NIL .OR. SqlIsTrue( ::EvalExpr( xWhere ) ) aRow := ::FetchRow( aResultExprs ) AAdd( aRows, aRow ) ENDIF ENDIF dbSelectArea( nWA ) dbSkip() ENDDO ENDIF ENDIF ENDIF /* GROUP BY */ IF Len( aGroupBy ) > 0 .OR. ::oAgg:HasAgg( aCols ) aRows := ::oAgg:GroupBy( aRows, aFieldNames, aCols, aGroupBy, xHaving, ::aTables, ::aParams ) aFieldNames := {} FOR i := 1 TO Len( aCols ) IF ! Empty( aCols[ i ][ 2 ] ) AAdd( aFieldNames, aCols[ i ][ 2 ] ) ELSE AAdd( aFieldNames, SqlExprName( aCols[ i ][ 1 ] ) ) ENDIF NEXT ENDIF /* Window functions */ ::ApplyWindowFunctions( @aRows, aFieldNames, aCols ) /* ORDER BY */ IF Len( aOrderBy ) > 0 IF ! ( nWA > 0 .AND. ::oIndex:MatchOrderByTag( nWA, aOrderBy, aFieldNames ) ) aRows := ::oSort:OrderBy( aRows, aFieldNames, aOrderBy, ::aTables, ::aParams ) ENDIF ENDIF /* DISTINCT */ IF lDistinct aRows := ::oSort:Distinct( aRows ) ENDIF /* OFFSET */ IF nOffset > 0 .AND. nOffset < Len( aRows ) aTmp := {} FOR i := nOffset + 1 TO Len( aRows ) AAdd( aTmp, aRows[ i ] ) NEXT aRows := aTmp ELSEIF nOffset >= Len( aRows ) aRows := {} ENDIF /* TOP / LIMIT */ nMaxRows := 0 IF nTop > 0 nMaxRows := nTop ENDIF IF nLimit > 0 nMaxRows := nLimit ENDIF IF nMaxRows > 0 .AND. Len( aRows ) > nMaxRows ASize( aRows, nMaxRows ) ENDIF /* RIGHT JOIN second pass */ IF Len( aJoins ) > 0 FOR i := 1 TO Len( aJoins ) IF Upper( aJoins[ i ][ 1 ] ) == "RIGHT" .OR. Upper( aJoins[ i ][ 1 ] ) == "FULL" ::RightJoinPass( aJoins, i, aResultExprs, @aRows ) ENDIF NEXT ENDIF /* UNION / INTERSECT / EXCEPT */ IF hUnion != NIL aU := TSqlExecutor():New( hUnion, ::aParams ):Run() IF hb_HHasKey( hUnion, "set_op" ) IF hUnion[ "set_op" ] == "INTERSECT" aRows := SqlDoIntersect( aRows, aU[ 2 ] ) ELSEIF hUnion[ "set_op" ] == "EXCEPT" aRows := SqlDoExcept( aRows, aU[ 2 ] ) ENDIF ELSE lAll := .F. IF hb_HHasKey( hUnion, "union_all" ) lAll := hUnion[ "union_all" ] ENDIF FOR i := 1 TO Len( aU[ 2 ] ) AAdd( aRows, aU[ 2 ][ i ] ) NEXT IF ! lAll aRows := ::oSort:Distinct( aRows ) ENDIF ENDIF ENDIF /* Close opened tables */ ::CloseOpened() /* Clean up CTE temp DBF files */ IF hb_HHasKey( ::hQuery, "cte" ) .AND. ValType( ::hQuery[ "cte" ] ) == "A" FOR i := 1 TO Len( ::hQuery[ "cte" ] ) cTable := Upper( ::hQuery[ "cte" ][ i ][ 1 ] ) /* Close the CTE name alias workarea if still open */ nWA := Select( cTable ) IF nWA > 0 dbSelectArea( nWA ) dbCloseArea() ENDIF cTable := "__cte_" + Lower( ::hQuery[ "cte" ][ i ][ 1 ] ) IF hb_FileExists( cTable + ".dbf" ) FErase( cTable + ".dbf" ) ENDIF NEXT ENDIF ::nDepth-- IF Len( aSavedAreas ) > 0 dbSelectArea( aSavedAreas[ 1 ] ) ENDIF RETURN { aFieldNames, aRows } /* Hash join: build hash table from inner table, probe with outer key */ METHOD HashJoin( nInnerWA, cInnerField, cOuterCol, xOnCond, aJoins, nIdx, xWhere, aRE, aRows, hHashTbl ) CLASS TSqlExecutor LOCAL cHashKey, aMatches, xOuterVal, xInnerVal, cValKey LOCAL nFPos, nSavedRec, i, lHadMatch lHadMatch := .F. /* Build hash table once per join (keyed by join index) */ cHashKey := "HJ_" + hb_ntos( nIdx ) + "_" + cInnerField IF ! hb_HHasKey( hHashTbl, cHashKey ) hHashTbl[ cHashKey ] := { => } dbSelectArea( nInnerWA ) nFPos := FieldPos( cInnerField ) IF nFPos > 0 dbGoTop() WHILE ! Eof() xInnerVal := FieldGet( nFPos ) cValKey := SqlValToStr( xInnerVal ) IF ! hb_HHasKey( hHashTbl[ cHashKey ], cValKey ) hHashTbl[ cHashKey ][ cValKey ] := {} ENDIF AAdd( hHashTbl[ cHashKey ][ cValKey ], RecNo() ) dbSkip() ENDDO ENDIF ENDIF /* Probe hash with outer row join key value */ xOuterVal := ::EvalExpr( SqlNode( ND_COL, cOuterCol, NIL, NIL, NIL ) ) cValKey := SqlValToStr( xOuterVal ) IF hb_HHasKey( hHashTbl[ cHashKey ], cValKey ) aMatches := hHashTbl[ cHashKey ][ cValKey ] FOR i := 1 TO Len( aMatches ) dbSelectArea( nInnerWA ) dbGoto( aMatches[ i ] ) /* Hash key already matched — skip redundant ON re-evaluation for * simple equi-joins (SQLite: ephemeral table probe is sufficient). */ lHadMatch := .T. ::JoinRecurse( aJoins, nIdx + 1, xWhere, aRE, @aRows, hHashTbl ) NEXT ENDIF RETURN lHadMatch /* Subquery result cache for non-correlated subqueries */ METHOD CacheSubquery( xSubExpr ) CLASS TSqlExecutor LOCAL cKey, aSubResult, nSavedWA, oSub /* Build cache key from subquery tokens */ cKey := SqlSubqueryKey( xSubExpr ) IF hb_HHasKey( ::hSubCache, cKey ) RETURN ::hSubCache[ cKey ] ENDIF /* Execute and cache the result. * Inherit current depth so the subquery opens tables with a * depth-suffixed alias, avoiding workarea collisions with * the outer query (e.g. scalar subquery on the same table). */ nSavedWA := Select() oSub := TSqlExecutor():New( xSubExpr, ::aParams ) oSub:nDepth := ::nDepth aSubResult := oSub:Run() dbSelectArea( nSavedWA ) ::hSubCache[ cKey ] := aSubResult RETURN aSubResult /* Materialize CTE definitions into temporary DBF tables */ METHOD MaterializeCTE( aCTE ) CLASS TSqlExecutor LOCAL i, cName, xSubQ, aSub, aFN, aDataRows LOCAL j, k, lReplaced, xVal LOCAL aStruct, cTmpFile, nExistWA, cPopAlias LOCAL cType, nWidth, nDec, nScan FOR i := 1 TO Len( aCTE ) cName := Upper( aCTE[ i ][ 1 ] ) xSubQ := aCTE[ i ][ 2 ] /* Execute the CTE subquery */ IF ValType( xSubQ ) == "A" .AND. xSubQ[ 1 ] == ND_SUB .AND. xSubQ[ 2 ] != NIL aSub := TSqlExecutor():New( xSubQ[ 2 ], ::aParams ):Run() ELSE aSub := NIL ENDIF IF ValType( aSub ) != "A" .OR. Len( aSub ) < 2 LOOP ENDIF aFN := aSub[ 1 ] aDataRows := aSub[ 2 ] /* Apply CTE column aliases if specified: WITH name(col1,col2) AS ... */ IF Len( aCTE[ i ] ) >= 3 .AND. ValType( aCTE[ i ][ 3 ] ) == "A" .AND. Len( aCTE[ i ][ 3 ] ) > 0 FOR j := 1 TO Min( Len( aCTE[ i ][ 3 ] ), Len( aFN ) ) aFN[ j ] := Upper( aCTE[ i ][ 3 ][ j ] ) NEXT ENDIF /* Build structure for temp DBF */ aStruct := {} FOR j := 1 TO Len( aFN ) cType := "C" ; nWidth := 40 ; nDec := 0 xVal := NIL FOR nScan := 1 TO Min( Len( aDataRows ), 50 ) IF j <= Len( aDataRows[ nScan ] ) .AND. aDataRows[ nScan ][ j ] != NIL xVal := aDataRows[ nScan ][ j ] ; EXIT ENDIF NEXT IF xVal != NIL IF ValType( xVal ) == "N" ; cType := "N" ; nWidth := 18 ; nDec := 4 ELSEIF ValType( xVal ) == "D" ; cType := "D" ; nWidth := 8 ELSEIF ValType( xVal ) == "L" ; cType := "L" ; nWidth := 1 ENDIF ENDIF AAdd( aStruct, { PadR( Upper( aFN[ j ] ), 10 ), cType, nWidth, nDec } ) NEXT cTmpFile := "__cte_" + Lower( cName ) cPopAlias := "__CTE" + hb_ntos( i ) + "__" nExistWA := Select( cName ) IF nExistWA > 0 dbSelectArea( nExistWA ) dbCloseArea() ENDIF nExistWA := Select( cPopAlias ) IF nExistWA > 0 dbSelectArea( nExistWA ) dbCloseArea() ENDIF IF hb_FileExists( cTmpFile + ".dbf" ) FErase( cTmpFile + ".dbf" ) ENDIF BEGIN SEQUENCE dbCreate( cTmpFile + ".dbf", aStruct ) RECOVER LOOP END SEQUENCE USE ( cTmpFile + ".dbf" ) NEW EXCLUSIVE ALIAS ( cPopAlias ) FOR j := 1 TO Len( aDataRows ) dbAppend() FOR k := 1 TO Min( Len( aStruct ), Len( aDataRows[ j ] ) ) IF aDataRows[ j ][ k ] != NIL FieldPut( k, aDataRows[ j ][ k ] ) ENDIF NEXT NEXT dbCommit() dbSelectArea( Select( cPopAlias ) ) dbCloseArea() USE ( cTmpFile + ".dbf" ) NEW SHARED ALIAS ( cName ) /* Replace existing table entry */ lReplaced := .F. NEXT RETURN NIL METHOD RunInsert() CLASS TSqlExecutor LOCAL cTable, aFields, aValExprs, cAlias, nWA, i, nFPos, xVal LOCAL aAutoInc, nAutoVal cTable := ::hQuery[ "table" ] aFields := ::hQuery[ "fields" ] aValExprs := ::hQuery[ "values" ] cAlias := cTable aAutoInc := SqlGetAutoIncFields( cTable ) nWA := Select( cAlias ) IF nWA == 0 BEGIN SEQUENCE dbUseArea( .T., "DBFNTX", Lower( cTable ) + ".dbf", cAlias, .F., .F. ) RECOVER dbUseArea( .T., "DBFNTX", cTable + ".dbf", cAlias, .F., .F. ) END SEQUENCE ELSE dbSelectArea( nWA ) ENDIF /* Transaction logging */ ::oTxn:LogRecord( cAlias, RecNo(), "INSERT" ) dbAppend() IF Len( aFields ) > 0 FOR i := 1 TO Min( Len( aFields ), Len( aValExprs ) ) nFPos := FieldPos( aFields[ i ] ) IF nFPos > 0 xVal := ::EvalExpr( aValExprs[ i ] ) FieldPut( nFPos, xVal ) ENDIF NEXT ELSE FOR i := 1 TO Min( FCount(), Len( aValExprs ) ) xVal := ::EvalExpr( aValExprs[ i ] ) FieldPut( i, xVal ) NEXT ENDIF /* Auto-increment */ FOR i := 1 TO Len( aAutoInc ) nFPos := FieldPos( aAutoInc[ i ] ) IF nFPos > 0 xVal := FieldGet( nFPos ) IF ValType( xVal ) == "N" .AND. xVal == 0 nAutoVal := SqlGetMaxFieldVal( cAlias, aAutoInc[ i ] ) + 1 FieldPut( nFPos, nAutoVal ) ENDIF ENDIF NEXT /* Validate CHECK constraints against current record values */ IF ! SqlValidateCheckRecord( cTable ) dbDelete() dbCommit() IF nWA == 0 dbCloseArea() ENDIF RETURN ::MakeError( SQL_ERR_GRAMMAR, "CHECK constraint violation on " + cTable ) ENDIF /* Validate FOREIGN KEY constraints */ IF Len( aFields ) > 0 FOR i := 1 TO Len( aFields ) nFPos := FieldPos( aFields[ i ] ) IF nFPos > 0 IF ! SqlValidateFKRecord( cTable, aFields[ i ], FieldGet( nFPos ) ) dbDelete() dbCommit() IF nWA == 0 dbCloseArea() ENDIF RETURN ::MakeError( SQL_ERR_GRAMMAR, ; "FOREIGN KEY violation: " + aFields[ i ] + " references missing parent" ) ENDIF ENDIF NEXT ENDIF dbCommit() IF nWA == 0 dbCloseArea() ENDIF RETURN { { "affected_rows" }, { { 1 } } } METHOD RunUpdate() CLASS TSqlExecutor LOCAL cTable, aSet, xWhere, cAlias, nWA, i, nFPos, xVal LOCAL nAffected := 0 cTable := ::hQuery[ "table" ] aSet := ::hQuery[ "set" ] xWhere := ::hQuery[ "where" ] cAlias := cTable ::aTables := { { cTable, cAlias, "" } } nWA := Select( cAlias ) IF nWA == 0 BEGIN SEQUENCE dbUseArea( .T., "DBFNTX", Lower( cTable ) + ".dbf", cAlias, .F., .F. ) RECOVER dbUseArea( .T., "DBFNTX", cTable + ".dbf", cAlias, .F., .F. ) END SEQUENCE ELSE dbSelectArea( nWA ) ENDIF dbGoTop() WHILE ! Eof() IF xWhere == NIL .OR. SqlIsTrue( ::EvalExpr( xWhere ) ) IF dbRLock( RecNo() ) ::oTxn:LogRecord( cAlias, RecNo(), "UPDATE" ) FOR i := 1 TO Len( aSet ) nFPos := FieldPos( aSet[ i ][ 1 ] ) IF nFPos > 0 xVal := ::EvalExpr( aSet[ i ][ 2 ] ) FieldPut( nFPos, xVal ) ENDIF NEXT dbRUnlock( RecNo() ) nAffected++ ENDIF ENDIF dbSkip() ENDDO dbCommit() IF nWA == 0 dbCloseArea() ENDIF RETURN { { "affected_rows" }, { { nAffected } } } METHOD RunDelete() CLASS TSqlExecutor LOCAL cTable, xWhere, cAlias, nWA LOCAL nAffected := 0 cTable := ::hQuery[ "table" ] xWhere := ::hQuery[ "where" ] cAlias := cTable ::aTables := { { cTable, cAlias, "" } } nWA := Select( cAlias ) IF nWA == 0 BEGIN SEQUENCE dbUseArea( .T., "DBFNTX", Lower( cTable ) + ".dbf", cAlias, .F., .F. ) RECOVER dbUseArea( .T., "DBFNTX", cTable + ".dbf", cAlias, .F., .F. ) END SEQUENCE ELSE dbSelectArea( nWA ) ENDIF SET DELETED ON dbGoTop() WHILE ! Eof() IF xWhere == NIL .OR. SqlIsTrue( ::EvalExpr( xWhere ) ) IF dbRLock( RecNo() ) dbDelete() dbRUnlock( RecNo() ) nAffected++ ENDIF ENDIF dbSkip() ENDDO dbCommit() IF nWA == 0 dbCloseArea() ENDIF RETURN { { "affected_rows" }, { { nAffected } } } /* ====================================================================== * Standalone helper functions called by TSqlIndex * ====================================================================== */ /* Evaluate expression node for index scan operations */ FUNCTION SqlEvalExprNode( xNode, aTables, aParams, nPI ) LOCAL oExec oExec := TSqlExecutor():New( { => }, aParams ) oExec:aTables := aTables RETURN oExec:EvalExpr( xNode ) /* Fetch a row array for index scan operations */ FUNCTION SqlFetchRowArr( aRE, aTables, aParams ) LOCAL oExec oExec := TSqlExecutor():New( { => }, aParams ) oExec:aTables := aTables RETURN oExec:FetchRow( aRE ) /* Join recurse called from TSqlIndex */ FUNCTION SqlJoinRecurse( aJoins, nIdx, aTables, xWhere, aRE, aRows, aParams, oIndex ) LOCAL oExec oExec := TSqlExecutor():New( { => }, aParams ) oExec:aTables := aTables oExec:oIndex := oIndex oExec:JoinRecurse( aJoins, nIdx, xWhere, aRE, @aRows, NIL ) RETURN NIL /* INTERSECT: keep only rows in both sets */ FUNCTION SqlDoIntersect( aRows1, aRows2 ) LOCAL aResult := {}, hKeys2 := { => }, i, cKey LOCAL oSort := TSqlSort():New() FOR i := 1 TO Len( aRows2 ) cKey := oSort:RowKey( aRows2[ i ] ) hKeys2[ cKey ] := .T. NEXT FOR i := 1 TO Len( aRows1 ) cKey := oSort:RowKey( aRows1[ i ] ) IF hb_HHasKey( hKeys2, cKey ) AAdd( aResult, aRows1[ i ] ) ENDIF NEXT RETURN aResult /* EXCEPT: keep only rows in first that are not in second */ FUNCTION SqlDoExcept( aRows1, aRows2 ) LOCAL aResult := {}, hKeys2 := { => }, i, cKey LOCAL oSort := TSqlSort():New() FOR i := 1 TO Len( aRows2 ) cKey := oSort:RowKey( aRows2[ i ] ) hKeys2[ cKey ] := .T. NEXT FOR i := 1 TO Len( aRows1 ) cKey := oSort:RowKey( aRows1[ i ] ) IF ! hb_HHasKey( hKeys2, cKey ) AAdd( aResult, aRows1[ i ] ) ENDIF NEXT RETURN aResult /* Materialize a subquery into a temp DBF */ FUNCTION SqlMaterializeSubquery( xSubQ, cAlias, aParams ) LOCAL aSub, aFN, aRows2, aStruct, cTmpFile, i, j LOCAL cType, nWidth, nDec, xVal aSub := TSqlExecutor():New( xSubQ[ 2 ], aParams ):Run() IF ValType( aSub ) != "A" .OR. Len( aSub ) < 2 RETURN { "__EMPTY__", cAlias, "" } ENDIF aFN := aSub[ 1 ] aRows2 := aSub[ 2 ] aStruct := {} FOR i := 1 TO Len( aFN ) cType := "C" nWidth := 40 nDec := 0 IF Len( aRows2 ) > 0 .AND. i <= Len( aRows2[ 1 ] ) xVal := aRows2[ 1 ][ i ] IF ValType( xVal ) == "N" cType := "N" nWidth := 18 nDec := 4 ELSEIF ValType( xVal ) == "D" cType := "D" nWidth := 8 ELSEIF ValType( xVal ) == "L" cType := "L" nWidth := 1 ELSEIF ValType( xVal ) == "T" cType := "T" nWidth := 8 ENDIF ENDIF AAdd( aStruct, { PadR( Upper( aFN[ i ] ), 10 ), cType, nWidth, nDec } ) NEXT cTmpFile := "__drv_" + Lower( cAlias ) dbCreate( cTmpFile + ".dbf", aStruct ) USE ( cTmpFile + ".dbf" ) NEW EXCLUSIVE ALIAS __DRVTMP FOR i := 1 TO Len( aRows2 ) dbAppend() FOR j := 1 TO Min( Len( aStruct ), Len( aRows2[ i ] ) ) IF aRows2[ i ][ j ] != NIL FieldPut( j, aRows2[ i ][ j ] ) ENDIF NEXT NEXT dbCommit() CLOSE __DRVTMP RETURN { cTmpFile, cAlias, "" } /* Auto-increment support */ FUNCTION SqlSetAutoInc( cTable, cField ) LOCAL cKey cKey := Upper( cTable ) IF s_hAutoInc == NIL s_hAutoInc := { => } ENDIF IF ! hb_HHasKey( s_hAutoInc, cKey ) s_hAutoInc[ cKey ] := {} ENDIF AAdd( s_hAutoInc[ cKey ], Upper( cField ) ) RETURN NIL FUNCTION SqlGetAutoIncFields( cTable ) LOCAL cKey cKey := Upper( cTable ) IF s_hAutoInc == NIL RETURN {} ENDIF IF hb_HHasKey( s_hAutoInc, cKey ) RETURN s_hAutoInc[ cKey ] ENDIF RETURN {} FUNCTION SqlGetMaxFieldVal( cAlias, cField ) LOCAL nWA, nSaved, nFPos, nMax := 0, xVal LOCAL nSavedRec nSaved := Select() nWA := Select( cAlias ) IF nWA > 0 dbSelectArea( nWA ) nSavedRec := RecNo() nFPos := FieldPos( cField ) IF nFPos > 0 dbGoTop() WHILE ! Eof() xVal := FieldGet( nFPos ) IF ValType( xVal ) == "N" .AND. xVal > nMax nMax := xVal ENDIF dbSkip() ENDDO ENDIF dbGoto( nSavedRec ) ENDIF dbSelectArea( nSaved ) RETURN nMax /* Build a unique cache key from a subquery hash structure */ FUNCTION SqlSubqueryKey( hSub ) LOCAL cKey, cType, i, aCols, aTbls IF ValType( hSub ) != "H" RETURN "??" ENDIF cKey := "" IF hb_HHasKey( hSub, "type" ) cKey += hSub[ "type" ] ENDIF IF hb_HHasKey( hSub, "tables" ) aTbls := hSub[ "tables" ] IF ValType( aTbls ) == "A" FOR i := 1 TO Len( aTbls ) IF ValType( aTbls[ i ] ) == "A" .AND. Len( aTbls[ i ] ) >= 1 cKey += "|T:" + aTbls[ i ][ 1 ] ENDIF NEXT ENDIF ENDIF IF hb_HHasKey( hSub, "columns" ) aCols := hSub[ "columns" ] IF ValType( aCols ) == "A" FOR i := 1 TO Len( aCols ) IF ValType( aCols[ i ] ) == "A" .AND. Len( aCols[ i ] ) >= 1 cKey += "|C:" + SqlExprName( aCols[ i ][ 1 ] ) ENDIF NEXT ENDIF ENDIF IF hb_HHasKey( hSub, "where" ) .AND. hSub[ "where" ] != NIL cKey += "|W:" + SqlExprName( hSub[ "where" ] ) ENDIF RETURN cKey /* ====================================================================== * Recursive CTE materialization (SQL:1999) * ====================================================================== */ /* * Materialize recursive CTEs using an in-memory working table approach. * * Algorithm: * 1. Execute anchor query → get initial rows * 2. Write initial rows to temp DBF * 3. Loop: scan temp DBF rows, for each row evaluate recursive expression * to produce new rows. Append new rows to temp DBF. * 4. Stop when no new rows are produced or max iterations reached. * 5. Leave temp DBF open for the main query to use. * * Key insight: instead of creating a new TSqlExecutor for the recursive part * (which causes workarea conflicts), we iterate the temp DBF directly and * build new rows by evaluating the recursive SELECT columns. */ METHOD MaterializeRecursiveCTE( aCTE ) CLASS TSqlExecutor LOCAL i, cName, xSubQ, hSubQ LOCAL aSub, aFN, aDataRows, aStruct LOCAL cTmpFile, cAlias, j, k, nIter LOCAL cType, nWidth, nDec, xVal LOCAL nExistWA, lReplaced LOCAL aNewRows, lHasUnionAll LOCAL hRecQuery, aPrevRows, nPrevCount LOCAL aOneRow, aOneRow2, lPass, nPI, aNewRow, aCols, nPI2, nStart, xWR, xCV FOR i := 1 TO Len( aCTE ) cName := Upper( aCTE[ i ][ 1 ] ) xSubQ := aCTE[ i ][ 2 ] IF ValType( xSubQ ) != "A" .OR. xSubQ[ 1 ] != ND_SUB .OR. xSubQ[ 2 ] == NIL LOOP ENDIF hSubQ := xSubQ[ 2 ] /* Check if the CTE subquery has a UNION ALL (signals recursion) */ lHasUnionAll := .F. IF ValType( hSubQ ) == "H" .AND. hb_HHasKey( hSubQ, "union" ) .AND. hSubQ[ "union" ] != NIL IF hb_HHasKey( hSubQ[ "union" ], "union_all" ) .AND. hSubQ[ "union" ][ "union_all" ] lHasUnionAll := .T. ENDIF ENDIF IF ! lHasUnionAll /* Not actually recursive, use normal CTE */ ::MaterializeCTE( { aCTE[ i ] } ) LOOP ENDIF /* Save and detach the recursive (UNION ALL) part so the anchor * query does not attempt to open the CTE table that has not been * materialised yet. */ hRecQuery := hSubQ[ "union" ] hSubQ[ "union" ] := NIL /* Execute anchor query (the first SELECT before UNION ALL) */ aSub := TSqlExecutor():New( hSubQ, ::aParams ):Run() /* Restore the union reference for later use */ hSubQ[ "union" ] := hRecQuery IF ValType( aSub ) != "A" .OR. Len( aSub ) < 2 LOOP ENDIF aFN := aSub[ 1 ] aDataRows := aSub[ 2 ] /* Apply CTE column aliases if specified: WITH RECURSIVE seq(n) AS ... */ IF Len( aCTE[ i ] ) >= 3 .AND. ValType( aCTE[ i ][ 3 ] ) == "A" .AND. Len( aCTE[ i ][ 3 ] ) > 0 FOR j := 1 TO Min( Len( aCTE[ i ][ 3 ] ), Len( aFN ) ) aFN[ j ] := Upper( aCTE[ i ][ 3 ][ j ] ) NEXT ENDIF /* Build structure from anchor result */ aStruct := {} FOR j := 1 TO Len( aFN ) cType := "C" nWidth := 40 nDec := 0 IF Len( aDataRows ) > 0 .AND. j <= Len( aDataRows[ 1 ] ) xVal := aDataRows[ 1 ][ j ] IF ValType( xVal ) == "N" cType := "N" nWidth := 18 nDec := 4 ELSEIF ValType( xVal ) == "D" cType := "D" nWidth := 8 ELSEIF ValType( xVal ) == "L" cType := "L" nWidth := 1 ENDIF ENDIF AAdd( aStruct, { PadR( Upper( aFN[ j ] ), 10 ), cType, nWidth, nDec } ) NEXT /* * Pure in-memory recursive iteration. * No temp DBF for the recursive loop — just arrays. * At the end, write ALL accumulated rows to temp DBF once. */ hRecQuery := hSubQ[ "union" ] nIter := 0 aPrevRows := AClone( aDataRows ) WHILE nIter < 50 .AND. Len( aPrevRows ) > 0 nIter++ aNewRows := {} IF hRecQuery != NIL .AND. hb_HHasKey( hRecQuery, "columns" ) aCols := hRecQuery[ "columns" ] /* * Check if this recursive part has a JOIN (FROM clause with tables). * If so, perform an in-memory nested-loop JOIN between the external * table(s) and aPrevRows (the CTE working set from last iteration). * * Example: SELECT e.id, e.name FROM employees e JOIN org o ON e.mgr_id = o.id * "employees" is a real DBF table; "org" is aPrevRows from previous iteration. */ IF hb_HHasKey( hRecQuery, "joins" ) .AND. hRecQuery[ "joins" ] != NIL .AND. ; Len( hRecQuery[ "joins" ] ) > 0 aNewRows := RecCteJoin( hRecQuery, aFN, aPrevRows, cName ) ELSE /* Simple recursive step (no JOIN): evaluate expressions * directly against each row in aPrevRows */ FOR j := 1 TO Len( aPrevRows ) lPass := .T. IF hb_HHasKey( hRecQuery, "where" ) .AND. hRecQuery[ "where" ] != NIL xWR := SqlEvalRowExpr( hRecQuery[ "where" ], aFN, aPrevRows[ j ] ) lPass := SqlIsTrue( xWR ) ENDIF IF lPass aNewRow := {} FOR k := 1 TO Len( aCols ) xCV := SqlEvalRowExpr( aCols[ k ][ 1 ], aFN, aPrevRows[ j ] ) AAdd( aNewRow, xCV ) NEXT AAdd( aNewRows, aNewRow ) ENDIF NEXT ENDIF ENDIF IF Len( aNewRows ) == 0 EXIT ENDIF /* Accumulate new rows */ FOR j := 1 TO Len( aNewRows ) AAdd( aDataRows, aNewRows[ j ] ) NEXT /* New rows become the working set for next iteration */ aPrevRows := AClone( aNewRows ) ENDDO /* Write ALL accumulated rows to temp DBF once */ cTmpFile := "__cte_" + Lower( cName ) cAlias := Upper( cName ) nExistWA := Select( cAlias ) IF nExistWA > 0 dbSelectArea( nExistWA ) dbCloseArea() ENDIF IF hb_FileExists( cTmpFile + ".dbf" ) FErase( cTmpFile + ".dbf" ) ENDIF BEGIN SEQUENCE dbCreate( cTmpFile + ".dbf", aStruct ) RECOVER END SEQUENCE BEGIN SEQUENCE USE ( cTmpFile + ".dbf" ) NEW ALIAS ( cAlias ) FOR j := 1 TO Len( aDataRows ) dbAppend() FOR k := 1 TO Min( Len( aStruct ), Len( aDataRows[ j ] ) ) IF aDataRows[ j ][ k ] != NIL FieldPut( k, aDataRows[ j ][ k ] ) ENDIF NEXT NEXT dbCommit() RECOVER END SEQUENCE /* Replace table entry to reference CTE temp file. * Keep alias = cName so the main query finds it by original name. */ lReplaced := .F. FOR j := 1 TO Len( ::aTables ) IF Upper( ::aTables[ j ][ 1 ] ) == cName ::aTables[ j ][ 1 ] := cTmpFile IF Empty( ::aTables[ j ][ 2 ] ) ::aTables[ j ][ 2 ] := cName ENDIF lReplaced := .T. EXIT ENDIF NEXT IF ! lReplaced AAdd( ::aTables, { cTmpFile, cName, "" } ) ENDIF NEXT RETURN NIL /* ====================================================================== * Window function evaluation (SQL:2003) * ====================================================================== */ METHOD ApplyWindowFunctions( aRows, aFN, aCols ) CLASS TSqlExecutor LOCAL i, j, k, nColIdx, xExpr LOCAL cFunc, aPartBy, aOrdBy, aFuncArgs LOCAL hPartitions, cPartKey, aPartIdx LOCAL aSorted, aIdxMap, nPartCol LOCAL nRank, nDenseRank, nRowNum LOCAL xPrev, xCurr, nTies LOCAL nLagLead, nArgCol, xDefault LOCAL nRunSum, nRunCount LOCAL aWinCols, nWC /* Scan for window function columns */ aWinCols := {} FOR i := 1 TO Len( aCols ) xExpr := aCols[ i ][ 1 ] IF ValType( xExpr ) == "A" .AND. xExpr[ 1 ] == ND_WINDOW AAdd( aWinCols, i ) ENDIF NEXT IF Len( aWinCols ) == 0 .OR. Len( aRows ) == 0 RETURN NIL ENDIF FOR nWC := 1 TO Len( aWinCols ) nColIdx := aWinCols[ nWC ] xExpr := aCols[ nColIdx ][ 1 ] cFunc := Upper( xExpr[ 2 ] ) aFuncArgs := xExpr[ 3 ] aPartBy := xExpr[ 4 ] aOrdBy := xExpr[ 5 ] /* Build partition groups as arrays of row indices */ hPartitions := { => } FOR i := 1 TO Len( aRows ) cPartKey := "" IF ValType( aPartBy ) == "A" FOR j := 1 TO Len( aPartBy ) nPartCol := SqlFindColIdx( aPartBy[ j ], aFN ) IF nPartCol == 0 nPartCol := SqlFindColIdx2( SqlExprName( aPartBy[ j ] ), aFN ) ENDIF IF nPartCol > 0 .AND. nPartCol <= Len( aRows[ i ] ) cPartKey += SqlValToStr( aRows[ i ][ nPartCol ] ) + "|" ENDIF NEXT ENDIF IF ! hb_HHasKey( hPartitions, cPartKey ) hPartitions[ cPartKey ] := {} ENDIF AAdd( hPartitions[ cPartKey ], i ) NEXT /* Process each partition */ FOR EACH aPartIdx IN hb_HValues( hPartitions ) /* Sort partition indices by ORDER BY columns */ IF ValType( aOrdBy ) == "A" .AND. Len( aOrdBy ) > 0 ASort( aPartIdx,,, {|a, b| SqlWinRowCmp( aRows, a, b, aOrdBy, aFN ) < 0 } ) ENDIF /* Compute window function for each row in the partition */ DO CASE CASE cFunc == "ROW_NUMBER" FOR k := 1 TO Len( aPartIdx ) IF nColIdx <= Len( aRows[ aPartIdx[ k ] ] ) aRows[ aPartIdx[ k ] ][ nColIdx ] := k ENDIF NEXT CASE cFunc == "RANK" nRank := 1 FOR k := 1 TO Len( aPartIdx ) IF k > 1 IF ! SqlWinRowsEqual( aRows, aPartIdx[ k ], aPartIdx[ k - 1 ], aOrdBy, aFN ) nRank := k ENDIF ENDIF IF nColIdx <= Len( aRows[ aPartIdx[ k ] ] ) aRows[ aPartIdx[ k ] ][ nColIdx ] := nRank ENDIF NEXT CASE cFunc == "DENSE_RANK" nDenseRank := 1 FOR k := 1 TO Len( aPartIdx ) IF k > 1 IF ! SqlWinRowsEqual( aRows, aPartIdx[ k ], aPartIdx[ k - 1 ], aOrdBy, aFN ) nDenseRank++ ENDIF ENDIF IF nColIdx <= Len( aRows[ aPartIdx[ k ] ] ) aRows[ aPartIdx[ k ] ][ nColIdx ] := nDenseRank ENDIF NEXT CASE cFunc == "LAG" nLagLead := 1 IF Len( aFuncArgs ) >= 2 .AND. aFuncArgs[ 2 ][ 1 ] == ND_LIT nLagLead := Int( SqlCoerceNum( aFuncArgs[ 2 ][ 2 ] ) ) ENDIF nArgCol := 0 IF Len( aFuncArgs ) >= 1 nArgCol := SqlFindColIdx( aFuncArgs[ 1 ], aFN ) IF nArgCol == 0 nArgCol := SqlFindColIdx2( SqlExprName( aFuncArgs[ 1 ] ), aFN ) ENDIF ENDIF xDefault := NIL IF Len( aFuncArgs ) >= 3 .AND. aFuncArgs[ 3 ][ 1 ] == ND_LIT xDefault := aFuncArgs[ 3 ][ 2 ] ENDIF FOR k := 1 TO Len( aPartIdx ) IF nColIdx <= Len( aRows[ aPartIdx[ k ] ] ) IF k - nLagLead >= 1 .AND. nArgCol > 0 .AND. ; nArgCol <= Len( aRows[ aPartIdx[ k - nLagLead ] ] ) aRows[ aPartIdx[ k ] ][ nColIdx ] := aRows[ aPartIdx[ k - nLagLead ] ][ nArgCol ] ELSE aRows[ aPartIdx[ k ] ][ nColIdx ] := xDefault ENDIF ENDIF NEXT CASE cFunc == "LEAD" nLagLead := 1 IF Len( aFuncArgs ) >= 2 .AND. aFuncArgs[ 2 ][ 1 ] == ND_LIT nLagLead := Int( SqlCoerceNum( aFuncArgs[ 2 ][ 2 ] ) ) ENDIF nArgCol := 0 IF Len( aFuncArgs ) >= 1 nArgCol := SqlFindColIdx( aFuncArgs[ 1 ], aFN ) IF nArgCol == 0 nArgCol := SqlFindColIdx2( SqlExprName( aFuncArgs[ 1 ] ), aFN ) ENDIF ENDIF xDefault := NIL IF Len( aFuncArgs ) >= 3 .AND. aFuncArgs[ 3 ][ 1 ] == ND_LIT xDefault := aFuncArgs[ 3 ][ 2 ] ENDIF FOR k := 1 TO Len( aPartIdx ) IF nColIdx <= Len( aRows[ aPartIdx[ k ] ] ) IF k + nLagLead <= Len( aPartIdx ) .AND. nArgCol > 0 .AND. ; nArgCol <= Len( aRows[ aPartIdx[ k + nLagLead ] ] ) aRows[ aPartIdx[ k ] ][ nColIdx ] := aRows[ aPartIdx[ k + nLagLead ] ][ nArgCol ] ELSE aRows[ aPartIdx[ k ] ][ nColIdx ] := xDefault ENDIF ENDIF NEXT CASE cFunc == "SUM" nArgCol := 0 IF Len( aFuncArgs ) >= 1 nArgCol := SqlFindColIdx( aFuncArgs[ 1 ], aFN ) IF nArgCol == 0 nArgCol := SqlFindColIdx2( SqlExprName( aFuncArgs[ 1 ] ), aFN ) ENDIF ENDIF nRunSum := 0 FOR k := 1 TO Len( aPartIdx ) IF nArgCol > 0 .AND. nArgCol <= Len( aRows[ aPartIdx[ k ] ] ) nRunSum += SqlCoerceNum( aRows[ aPartIdx[ k ] ][ nArgCol ] ) ENDIF IF nColIdx <= Len( aRows[ aPartIdx[ k ] ] ) aRows[ aPartIdx[ k ] ][ nColIdx ] := nRunSum ENDIF NEXT CASE cFunc == "AVG" nArgCol := 0 IF Len( aFuncArgs ) >= 1 nArgCol := SqlFindColIdx( aFuncArgs[ 1 ], aFN ) IF nArgCol == 0 nArgCol := SqlFindColIdx2( SqlExprName( aFuncArgs[ 1 ] ), aFN ) ENDIF ENDIF nRunSum := 0 nRunCount := 0 FOR k := 1 TO Len( aPartIdx ) IF nArgCol > 0 .AND. nArgCol <= Len( aRows[ aPartIdx[ k ] ] ) nRunSum += SqlCoerceNum( aRows[ aPartIdx[ k ] ][ nArgCol ] ) nRunCount++ ENDIF IF nColIdx <= Len( aRows[ aPartIdx[ k ] ] ) IF nRunCount > 0 aRows[ aPartIdx[ k ] ][ nColIdx ] := nRunSum / nRunCount ELSE aRows[ aPartIdx[ k ] ][ nColIdx ] := 0 ENDIF ENDIF NEXT ENDCASE NEXT NEXT RETURN NIL /* ====================================================================== * TRUNCATE TABLE executor * ====================================================================== */ METHOD RunTruncate() CLASS TSqlExecutor LOCAL cTable, nWA cTable := ::hQuery[ "table" ] /* Close if open */ nWA := Select( cTable ) IF nWA > 0 dbSelectArea( nWA ) dbCloseArea() ENDIF BEGIN SEQUENCE USE ( Lower( cTable ) + ".dbf" ) NEW EXCLUSIVE dbGoTop() WHILE ! Eof() dbDelete() dbSkip() ENDDO dbCloseArea() RECOVER RETURN ::MakeError( SQL_ERR_LOCKED, "TRUNCATE TABLE failed: " + cTable ) END SEQUENCE RETURN { { "result" }, { { "Table " + cTable + " truncated" } } } /* ====================================================================== * MERGE (UPSERT) executor (SQL:2003) * ====================================================================== */ METHOD RunMerge() CLASS TSqlExecutor LOCAL cTarget, cSource, cSrcAlias, xOnCond LOCAL aUpdSet, aInsFlds, aInsVals LOCAL lHasMatched, lHasNotMatched LOCAL nSrcWA, nTgtWA, nSaved, nAffected LOCAL lMatched, i, nFPos, xVal cTarget := ::hQuery[ "target" ] cSource := ::hQuery[ "source" ] cSrcAlias := "" IF hb_HHasKey( ::hQuery, "source_alias" ) cSrcAlias := ::hQuery[ "source_alias" ] ENDIF xOnCond := ::hQuery[ "on" ] aUpdSet := ::hQuery[ "update_set" ] aInsFlds := ::hQuery[ "insert_fields" ] aInsVals := ::hQuery[ "insert_values" ] lHasMatched := ::hQuery[ "has_matched" ] lHasNotMatched := ::hQuery[ "has_not_matched" ] nAffected := 0 ::aTables := { { cTarget, cTarget, "" }, { cSource, iif( Empty( cSrcAlias ), cSource, cSrcAlias ), "" } } /* Open source */ nSrcWA := Select( iif( Empty( cSrcAlias ), cSource, cSrcAlias ) ) IF nSrcWA == 0 BEGIN SEQUENCE dbUseArea( .T., "DBFNTX", Lower( cSource ) + ".dbf", ; iif( Empty( cSrcAlias ), cSource, cSrcAlias ), .T., .T. ) RECOVER RETURN ::MakeError( SQL_ERR_NO_TABLE, "MERGE: cannot open source " + cSource ) END SEQUENCE ENDIF nSrcWA := Select( iif( Empty( cSrcAlias ), cSource, cSrcAlias ) ) /* Open target */ nTgtWA := Select( cTarget ) IF nTgtWA == 0 BEGIN SEQUENCE dbUseArea( .T., "DBFNTX", Lower( cTarget ) + ".dbf", cTarget, .F., .F. ) RECOVER RETURN ::MakeError( SQL_ERR_NO_TABLE, "MERGE: cannot open target " + cTarget ) END SEQUENCE ENDIF nTgtWA := Select( cTarget ) nSaved := Select() /* For each source row */ dbSelectArea( nSrcWA ) dbGoTop() DO WHILE ! Eof() lMatched := .F. /* Scan target for match */ dbSelectArea( nTgtWA ) dbGoTop() DO WHILE ! Eof() IF SqlIsTrue( ::EvalExpr( xOnCond ) ) lMatched := .T. EXIT ENDIF dbSkip() ENDDO IF lMatched .AND. lHasMatched /* UPDATE matched row */ dbSelectArea( nTgtWA ) IF dbRLock( RecNo() ) FOR i := 1 TO Len( aUpdSet ) nFPos := FieldPos( aUpdSet[ i ][ 1 ] ) IF nFPos > 0 xVal := ::EvalExpr( aUpdSet[ i ][ 2 ] ) FieldPut( nFPos, xVal ) ENDIF NEXT dbRUnlock( RecNo() ) nAffected++ ENDIF ELSEIF ! lMatched .AND. lHasNotMatched /* INSERT new row */ dbSelectArea( nTgtWA ) dbAppend() IF Len( aInsFlds ) > 0 FOR i := 1 TO Min( Len( aInsFlds ), Len( aInsVals ) ) nFPos := FieldPos( aInsFlds[ i ] ) IF nFPos > 0 xVal := ::EvalExpr( aInsVals[ i ] ) FieldPut( nFPos, xVal ) ENDIF NEXT ELSE FOR i := 1 TO Min( FCount(), Len( aInsVals ) ) xVal := ::EvalExpr( aInsVals[ i ] ) FieldPut( i, xVal ) NEXT ENDIF nAffected++ ENDIF dbSelectArea( nSrcWA ) dbSkip() ENDDO dbSelectArea( nTgtWA ) dbCommit() dbSelectArea( nSaved ) RETURN { { "affected_rows" }, { { nAffected } } } /* ====================================================================== * Window function helper: compare two rows by ORDER BY columns * ====================================================================== */ FUNCTION SqlWinRowCmp( aRows, nIdxA, nIdxB, aOrdBy, aFN ) LOCAL i, nCol, cDir, xA, xB FOR i := 1 TO Len( aOrdBy ) nCol := SqlFindColIdx( aOrdBy[ i ][ 1 ], aFN ) IF nCol == 0 nCol := SqlFindColIdx2( SqlExprName( aOrdBy[ i ][ 1 ] ), aFN ) ENDIF cDir := aOrdBy[ i ][ 2 ] IF nCol > 0 .AND. nCol <= Len( aRows[ nIdxA ] ) .AND. nCol <= Len( aRows[ nIdxB ] ) xA := aRows[ nIdxA ][ nCol ] xB := aRows[ nIdxB ][ nCol ] IF xA == NIL .AND. xB == NIL LOOP ENDIF IF xA == NIL RETURN iif( cDir == "DESC", -1, 1 ) ENDIF IF xB == NIL RETURN iif( cDir == "DESC", 1, -1 ) ENDIF IF ValType( xA ) == ValType( xB ) IF xA < xB RETURN iif( cDir == "DESC", 1, -1 ) ELSEIF xA > xB RETURN iif( cDir == "DESC", -1, 1 ) ENDIF ENDIF ENDIF NEXT RETURN 0 /* Check if two rows have equal values for ORDER BY columns */ FUNCTION SqlWinRowsEqual( aRows, nIdxA, nIdxB, aOrdBy, aFN ) LOCAL i, nCol, xA, xB FOR i := 1 TO Len( aOrdBy ) nCol := SqlFindColIdx( aOrdBy[ i ][ 1 ], aFN ) IF nCol == 0 nCol := SqlFindColIdx2( SqlExprName( aOrdBy[ i ][ 1 ] ), aFN ) ENDIF IF nCol > 0 .AND. nCol <= Len( aRows[ nIdxA ] ) .AND. nCol <= Len( aRows[ nIdxB ] ) xA := aRows[ nIdxA ][ nCol ] xB := aRows[ nIdxB ][ nCol ] IF ! SqlCmpEq( xA, xB ) RETURN .F. ENDIF ENDIF NEXT RETURN .T. /* * RecCteJoin — In-memory nested-loop JOIN for recursive CTE. * * The recursive part of a CTE may reference both a real DBF table and the * CTE itself. Example: * SELECT e.id, e.name FROM employees e JOIN org o ON e.mgr_id = o.id * * "employees" (alias e) is a DBF table on disk. * "org" (alias o) is the CTE — represented by aPrevRows from the previous iteration. * * This function: * 1. Identifies which FROM table is the CTE and which is the real DBF * 2. Opens the DBF and reads all records into memory * 3. Performs a nested-loop JOIN: for each DBF row x CTE row, checks ON condition * 4. Evaluates SELECT columns for matching pairs * 5. Returns the result rows */ STATIC FUNCTION RecCteJoin( hRecQuery, aFN, aPrevRows, cCteName ) LOCAL aCols, aFrom, aJoinFN, aJoinRows LOCAL aResult, aNewRow LOCAL i, j, k, nF LOCAL cTblName, cTblAlias, cCteAlias, cWAAlias LOCAL nSaveWA, cDbfFile LOCAL xLeft, xRight, lMatch LOCAL aJoinOn, aJ LOCAL xCV LOCAL aCombFN, aCombRow aCols := hRecQuery[ "columns" ] aResult := {} /* Identify the real table and the CTE reference. * tables[]: { tableName, alias, "" } * joins[]: { joinType, tableName, alias, onCondExpr } */ cTblName := "" cTblAlias := "" cCteAlias := "" IF hb_HHasKey( hRecQuery, "tables" ) aFrom := hRecQuery[ "tables" ] FOR i := 1 TO Len( aFrom ) IF Upper( aFrom[ i ][ 1 ] ) == Upper( cCteName ) cCteAlias := Upper( aFrom[ i ][ 2 ] ) IF Empty( cCteAlias ) cCteAlias := Upper( cCteName ) ENDIF ELSE cTblName := aFrom[ i ][ 1 ] cTblAlias := Upper( aFrom[ i ][ 2 ] ) IF Empty( cTblAlias ) cTblAlias := Upper( cTblName ) ENDIF ENDIF NEXT ENDIF /* Also check the joins array for the CTE or real table */ aJoinOn := NIL IF hb_HHasKey( hRecQuery, "joins" ) FOR i := 1 TO Len( hRecQuery[ "joins" ] ) aJ := hRecQuery[ "joins" ][ i ] /* aJ = { joinType, tableName, alias, onCondExpr } */ IF Upper( aJ[ 2 ] ) == Upper( cCteName ) IF ! Empty( aJ[ 3 ] ) cCteAlias := Upper( aJ[ 3 ] ) ELSE cCteAlias := Upper( cCteName ) ENDIF ELSE IF Empty( cTblName ) cTblName := aJ[ 2 ] cTblAlias := Upper( aJ[ 3 ] ) IF Empty( cTblAlias ) cTblAlias := Upper( cTblName ) ENDIF ENDIF ENDIF IF Len( aJ ) >= 4 .AND. aJ[ 4 ] != NIL aJoinOn := aJ[ 4 ] ENDIF NEXT ENDIF IF Empty( cTblName ) RETURN aResult ENDIF /* Read all records from the real DBF table into memory */ aJoinRows := {} aJoinFN := {} nSaveWA := Select() /* Always open the table fresh to avoid workarea conflicts. * The anchor query may have closed it. */ cDbfFile := Lower( cTblName ) IF ! ( ".dbf" $ cDbfFile ) cDbfFile := cDbfFile + ".dbf" ENDIF s_nRCJSeq++ cWAAlias := "RCJ_" + hb_ntos( s_nRCJSeq ) BEGIN SEQUENCE USE ( cDbfFile ) NEW SHARED ALIAS ( cWAAlias ) RECOVER dbSelectArea( nSaveWA ) RETURN aResult END SEQUENCE /* Collect field names */ FOR nF := 1 TO FCount() AAdd( aJoinFN, Upper( FieldName( nF ) ) ) NEXT /* Read all records */ dbGoTop() WHILE ! Eof() aNewRow := {} FOR nF := 1 TO FCount() AAdd( aNewRow, FieldGet( nF ) ) NEXT AAdd( aJoinRows, aNewRow ) dbSkip() ENDDO /* Build combined field name list: * [tblAlias.field1, tblAlias.field2, ..., cteAlias.field1, cteAlias.field2, ...] * Then also plain names for expression resolution */ aCombFN := {} FOR nF := 1 TO Len( aJoinFN ) AAdd( aCombFN, cTblAlias + "." + aJoinFN[ nF ] ) NEXT FOR nF := 1 TO Len( aFN ) AAdd( aCombFN, cCteAlias + "." + Upper( aFN[ nF ] ) ) NEXT /* Also add unqualified names for both sides */ FOR nF := 1 TO Len( aJoinFN ) IF AScan( aCombFN, {|x| x == aJoinFN[ nF ] } ) == 0 AAdd( aCombFN, aJoinFN[ nF ] ) ENDIF NEXT FOR nF := 1 TO Len( aFN ) IF AScan( aCombFN, {|x| x == Upper( aFN[ nF ] ) } ) == 0 AAdd( aCombFN, Upper( aFN[ nF ] ) ) 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 ) ENDIF 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 /* Close the workarea we opened */ IF ! Empty( cWAAlias ) dbSelectArea( Select( cWAAlias ) ) dbCloseArea() ENDIF dbSelectArea( nSaveWA ) RETURN aResult