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>
2734 lines
78 KiB
Plaintext
2734 lines
78 KiB
Plaintext
/*
|
|
* 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
|