fix(FiveSql2): last 3 — RIGHT JOIN O(N), counter wrap, implicit alias

--- #15 RIGHT JOIN O(N*M) → O(N+M) via matched RecNo set ---
--- #19 s_nRCJSeq modular counter (% 100000) ---
--- #20 Implicit column alias without AS keyword ---

Validation: 43/43 + 51/51 + go test ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 23:09:07 +09:00
parent e754aaac3f
commit c869a08365
2 changed files with 52 additions and 26 deletions

View File

@@ -39,6 +39,7 @@ CLASS TSqlExecutor
DATA aSubCacheSlots INIT {} /* list of {xSubNode, {id, aFreeVars}} */
DATA nSubCacheSeq INIT 0 /* monotonic ID for subqueries */
DATA aSemiJoinSlots INIT {} /* list of {xSubNode, semiJoinData | "NO"} */
DATA hRightMatched /* RecNo sets for RIGHT JOIN pass */
DATA hSubCache
@@ -104,6 +105,7 @@ METHOD New( hQuery, aParams ) CLASS TSqlExecutor
::aSubCacheSlots := {}
::aSemiJoinSlots := {}
::nSubCacheSeq := 0
::hRightMatched := { => }
RETURN SELF
@@ -885,7 +887,7 @@ METHOD JoinRecurse( aJoins, nIdx, xWhere, aRE, aRows, hHashTbl ) CLASS TSqlExecu
LOCAL lUseIndex, lFound, nPI
LOCAL cJoinType, lHadMatch
LOCAL nRecCount, lUseHash
LOCAL xProbe
LOCAL xProbe, cRMKey
IF hHashTbl == NIL
hHashTbl := { => }
@@ -930,6 +932,8 @@ METHOD JoinRecurse( aJoins, nIdx, xWhere, aRE, aRows, hHashTbl ) CLASS TSqlExecu
lHadMatch := .F.
lUseIndex := .F.
lUseHash := .F.
/* Track matched inner RecNos for RIGHT/FULL JOIN pass */
cRMKey := "__RIGHT_" + Upper( cJAlias )
cOuterCol := ""
cInnerCol := ""
cInnerField := ""
@@ -1015,6 +1019,11 @@ METHOD JoinRecurse( aJoins, nIdx, xWhere, aRE, aRows, hHashTbl ) CLASS TSqlExecu
ENDIF
IF lJoinMatch
lHadMatch := .T.
/* Record match for RIGHT JOIN pass */
IF ! hb_HHasKey( ::hRightMatched, cRMKey )
::hRightMatched[ cRMKey ] := { => }
ENDIF
::hRightMatched[ cRMKey ][ RecNo() ] := .T.
::JoinRecurse( aJoins, nIdx + 1, xWhere, aRE, @aRows, hHashTbl )
ENDIF
dbSelectArea( nWA )
@@ -1046,17 +1055,23 @@ METHOD JoinRecurse( aJoins, nIdx, xWhere, aRE, aRows, hHashTbl ) CLASS TSqlExecu
RETURN NIL
/* RightJoinPass — emit inner rows that had no match during the main
* join pass (for RIGHT/FULL joins). Outer columns are NIL.
*
* Previous O(N*M) approach rescanned the outer table for every inner
* row to detect unmatched ones. Now uses ::hRightMatched (populated
* during the main join) as a RecNo set — O(N) inner scan + O(1)
* hash probe per row.
*/
METHOD RightJoinPass( aJoins, nJIdx, aRE, aRows ) CLASS TSqlExecutor
LOCAL cJAlias, xOnCond, nWA, nOuterWA, cOuterAlias
LOCAL lMatched, aRow, j
LOCAL cColRef
LOCAL cJAlias, nWA, cOuterAlias
LOCAL aRow, j, cColRef, cMatchKey, nRec
cJAlias := aJoins[ nJIdx ][ 3 ]
IF Empty( cJAlias )
cJAlias := aJoins[ nJIdx ][ 2 ]
ENDIF
xOnCond := aJoins[ nJIdx ][ 4 ]
nWA := Select( cJAlias )
IF nWA == 0
@@ -1071,28 +1086,17 @@ METHOD RightJoinPass( aJoins, nJIdx, aRE, aRows ) CLASS TSqlExecutor
ENDIF
ENDIF
nOuterWA := Select( cOuterAlias )
IF nOuterWA == 0
RETURN NIL
ENDIF
cMatchKey := "__RIGHT_" + Upper( cJAlias )
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 )
nRec := RecNo()
IF hb_HHasKey( ::hRightMatched, cMatchKey ) .AND. ;
hb_HHasKey( ::hRightMatched[ cMatchKey ], nRec )
/* Matched during main join — skip */
ELSE
/* Unmatched inner row — emit with NULLs for outer columns */
aRow := {}
FOR j := 1 TO Len( aRE )
cColRef := ""
@@ -1107,7 +1111,6 @@ METHOD RightJoinPass( aJoins, nJIdx, aRE, aRows ) CLASS TSqlExecutor
NEXT
AAdd( aRows, aRow )
ENDIF
dbSelectArea( nWA )
dbSkip()
ENDDO
@@ -1599,7 +1602,7 @@ METHOD HashJoin( nInnerWA, cInnerField, cOuterCol, xOnCond, aJoins, nIdx, xWhere
LOCAL cHashKey, aMatches, xOuterVal, xInnerVal, cValKey
LOCAL nFPos, nSavedRec, i, lHadMatch
LOCAL lCompound
LOCAL lCompound, cHJRMKey
lHadMatch := .F.
@@ -1633,6 +1636,8 @@ METHOD HashJoin( nInnerWA, cInnerField, cOuterCol, xOnCond, aJoins, nIdx, xWhere
/* Base-case inline: if the next recursion would just be FetchRow,
* skip the method-dispatch overhead and build the row directly.
* 50k inner matches × Send() dispatch was the 3-way join bottleneck. */
/* Track inner matched RecNos for RIGHT JOIN pass */
cHJRMKey := "__RIGHT_" + Upper( Alias( nInnerWA ) )
IF nIdx + 1 > Len( aJoins )
FOR i := 1 TO Len( aMatches )
dbSelectArea( nInnerWA )
@@ -1641,6 +1646,10 @@ METHOD HashJoin( nInnerWA, cInnerField, cOuterCol, xOnCond, aJoins, nIdx, xWhere
LOOP
ENDIF
lHadMatch := .T.
IF ! hb_HHasKey( ::hRightMatched, cHJRMKey )
::hRightMatched[ cHJRMKey ] := { => }
ENDIF
::hRightMatched[ cHJRMKey ][ aMatches[ i ] ] := .T.
IF xWhere == NIL .OR. SqlIsTrue( ::EvalExpr( xWhere ) )
AAdd( aRows, ::FetchRow( aRE ) )
ENDIF
@@ -1653,6 +1662,10 @@ METHOD HashJoin( nInnerWA, cInnerField, cOuterCol, xOnCond, aJoins, nIdx, xWhere
LOOP
ENDIF
lHadMatch := .T.
IF ! hb_HHasKey( ::hRightMatched, cHJRMKey )
::hRightMatched[ cHJRMKey ] := { => }
ENDIF
::hRightMatched[ cHJRMKey ][ aMatches[ i ] ] := .T.
::JoinRecurse( aJoins, nIdx + 1, xWhere, aRE, @aRows, hHashTbl )
NEXT
ENDIF
@@ -3511,7 +3524,7 @@ STATIC FUNCTION RecCteJoin( hRecQuery, aFN, aPrevRows, cCteName )
cDbfFile := cDbfFile + ".dbf"
ENDIF
s_nRCJSeq++
s_nRCJSeq := ( s_nRCJSeq + 1 ) % 100000
cWAAlias := "RCJ_" + hb_ntos( s_nRCJSeq )
BEGIN SEQUENCE

View File

@@ -777,6 +777,19 @@ METHOD ParseColumnList() CLASS TSqlParser2
::nPos++
cAlias := ::TVal( ::nPos )
::nPos++
/* Implicit alias: `SELECT salary total, ...` where `total` is
* a non-keyword identifier followed by comma or clause keyword.
* SQL standard allows omitting AS for column aliases. */
ELSEIF ::TType( ::nPos ) == TK_NAME .AND. ! ::IsFromKW( ::TVal( ::nPos ) ) .AND. ;
! ::IsKW( ::nPos, "FROM" ) .AND. ! ::IsKW( ::nPos, "WHERE" ) .AND. ;
! ::IsKW( ::nPos, "GROUP" ) .AND. ! ::IsKW( ::nPos, "ORDER" ) .AND. ;
! ::IsKW( ::nPos, "HAVING" ) .AND. ! ::IsKW( ::nPos, "LIMIT" ) .AND. ;
! ::IsKW( ::nPos, "UNION" ) .AND. ! ::IsKW( ::nPos, "INTERSECT" ) .AND. ;
! ::IsKW( ::nPos, "EXCEPT" ) .AND. ! ::IsKW( ::nPos, "WINDOW" ) .AND. ;
! ::IsKW( ::nPos, "OFFSET" ) .AND. ! ::IsKW( ::nPos, "FETCH" ) .AND. ;
! ::IsKW( ::nPos, "INTO" ) .AND. ! ::IsKW( ::nPos, "FOR" )
cAlias := ::TVal( ::nPos )
::nPos++
ENDIF
AAdd( aCols, { xExpr, cAlias } )
IF ::TType( ::nPos ) == TK_COMMA