diff --git a/src/Common/FoxProCmd.xh b/src/Common/FoxProCmd.xh index 8255345429..f11855394f 100644 --- a/src/Common/FoxProCmd.xh +++ b/src/Common/FoxProCmd.xh @@ -198,6 +198,49 @@ [] ; => := __VfpCount( <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ) +// SUM / AVERAGE — the VO rules in dbcmd.xh never update _TALLY. Copied here +// because a UDC cannot wrap another one; the only change is the last line. +#command SUM [, ] ; + [FOR ] ; + [WHILE ] ; + [NEXT ] ; + [RECORD ] ; + [] ; + [] ; + [ALL] ; + TO [, ] ; + ; + => := [ := ] 0 ; + ; DbEval( ; + {|| += [, += ]}, ; + <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.>; + ) ; + ; _TALLY := RuntimeState.Tally + +// The rule in dbcmd.xh wraps the whole command in one optional group, which +// never matches: AVERAGE TO does not even translate today. Declared +// here with the same shape as SUM, so the command works and updates _TALLY. +#command AVERAGE [, ] ; + [FOR ] ; + [WHILE ] ; + [NEXT ] ; + [RECORD ] ; + [] ; + [] ; + [ALL] ; + TO [, ] ; + ; + => M->__Avg := := [ := ] 0 ; + ; + ; DbEval( ; + {|| M->__Avg += 1, ; + += [, += ] }, ; + <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.>; + ) ; + ; + ; /= M->__Avg [; /= M->__Avg ] ; + ; _TALLY := RuntimeState.Tally + // WAIT #xcommand WAIT [ [AT , ]] [] [] [TIMEOUT ] ; => __VfpWait(, <.win.>, , , <.now.>, <.nclr.>, ) @@ -270,7 +313,7 @@ => (<(a)>)->(DbEval( ; {||DbAutoLock(), __FieldSet(<(f1)>,) [, __FieldSet(<(fn)>, )], DbAutoUnLock()}, ; <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.>; - )) + )); _TALLY := RuntimeState.Tally // This variant moves the ALL clause to the end. This is needed because the WITH clause is declared as non optional #command REPLACE <*clauses*> ALL <*moreclauses*> => REPLACE ALL @@ -293,7 +336,7 @@ [, __FieldSet(<(fn)>, )], ; DbAutoUnLock() }, ; <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ; - ) + ); _TALLY := RuntimeState.Tally // Scoped REPLACE, single field with ADDITIVE — without IN clause #command REPLACE <(f1)> WITH ADDITIVE ; @@ -304,18 +347,18 @@ __FieldSetAdd(<(f1)>, , .T.), ; DbAutoUnLock() }, ; <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ; - ) + ); _TALLY := RuntimeState.Tally // Single record REPLACE — no scope clauses: current record only (VFP default) // Note: no nested optional ADDITIVE inside the repeated clause: the preprocessor // drops the whole output repetition when the nested marker has no matches #command REPLACE <(f1)> WITH [, <(fn)> WITH ] ; => DbAutoLock(); __FieldSet(<(f1)>, ) ; - [; __FieldSet(<(fn)>, )]; DbAutoUnLock() + [; __FieldSet(<(fn)>, )]; DbAutoUnLock(); _TALLY := 1 // Single record, single field with ADDITIVE — current record only #command REPLACE <(f1)> WITH ADDITIVE ; - => DbAutoLock(); __FieldSetAdd(<(f1)>, , .T.); DbAutoUnLock() + => DbAutoLock(); __FieldSetAdd(<(f1)>, , .T.); DbAutoUnLock(); _TALLY := 1 // Scoped REPLACE IN workarea #command REPLACE <(f1)> WITH [, <(fn)> WITH ] ; @@ -327,7 +370,7 @@ [, __FieldSet(<(fn)>, )], ; DbAutoUnLock() }, ; <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ; - )) + )); _TALLY := RuntimeState.Tally // Scoped REPLACE, single field with ADDITIVE, IN workarea #command REPLACE <(f1)> WITH ADDITIVE ; @@ -338,7 +381,7 @@ __FieldSetAdd(<(f1)>, , .T.), ; DbAutoUnLock() }, ; <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ; - )) + )); _TALLY := RuntimeState.Tally #command DELETE ; @@ -346,15 +389,21 @@ [WHILE ] ; [NEXT ] ; [RECORD ] ; - [] ; + [] ; + [] ; + [ALL] ; + => __VfpDelete( <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ) + +#command DELETE ; + [FOR ] ; + [WHILE ] ; + [NEXT ] ; + [RECORD ] ; + [] ; [] ; [ALL] ; IN <(a)> ; - ; - => (<(a)>)->(DbEval( ; - {|| __DbDelete()}, ; - <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.>; - )) + => (<(a)>)->(__VfpDelete( <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> )) #command RECALL ; @@ -362,17 +411,21 @@ [WHILE ] ; [NEXT ] ; [RECORD ] ; - [] ; + [] ; + [] ; + [ALL] ; + => __VfpRecall( <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ) + +#command RECALL ; + [FOR ] ; + [WHILE ] ; + [NEXT ] ; + [RECORD ] ; + [] ; [] ; [ALL] ; IN <(a)> ; - ; - => BEGIN SCOPE ; VAR __saveDeleted := SetDeleted(); ; - IF RuntimeState.Dialect == XSharpDialect.FoxPro; SetDeleted(FALSE); ENDIF ;; - (<(a)>)->(DbEval( ; - {|| __DbRecall()}, ; - <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.>; - )) ;SetDeleted(__saveDeleted); END SCOPE + => (<(a)>)->(__VfpRecall( <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> )) #command LOCATE ; @@ -387,29 +440,31 @@ ; => (<(a)>)->(DbLocate( <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> )) -#command CONTINUE IN <(a)> => (a)->(DbContinue()) +#command CONTINUE IN <(a)> => (<(a)>)->(DbContinue()) // Single record operations with IN Clause #command REPLACE <(f1)> WITH [, <(fN)> WITH ] ; IN <(a)> ; - => DbAutoLockArea(<(a)>), __FieldSetWa(<(a)>, <(f1)>,) [,__FieldSetWa(<(a)>,<(fn)>,)], DbAutoUnLockArea(<(a)>) + => DbAutoLockArea(<(a)>), __FieldSetWa(<(a)>, <(f1)>,) [,__FieldSetWa(<(a)>,<(fn)>,)], DbAutoUnLockArea(<(a)>); _TALLY := 1 // Single record REPLACE IN workarea — current record only (VFP default) #command REPLACE <(f1)> WITH [, <(fn)> WITH ] IN <(a)> ; => DbAutoLockArea(<(a)>), ; (<(a)>)->(__FieldSet(<(f1)>, ) [, __FieldSet(<(fn)>, )]), ; - DbAutoUnLockArea(<(a)>) + DbAutoUnLockArea(<(a)>); _TALLY := 1 // Single record, single field with ADDITIVE, IN workarea — current record only #command REPLACE <(f1)> WITH ADDITIVE IN <(a)> ; => DbAutoLockArea(<(a)>), ; (<(a)>)->(__FieldSetAdd(<(f1)>, , .T.)), ; - DbAutoUnLockArea(<(a)>) + DbAutoUnLockArea(<(a)>); _TALLY := 1 -#command DELETE IN <(a)> => (<(a)>)->(__DbDelete()) +#command DELETE IN <(a)> => (<(a)>)->(__VfpDeleteRecord()) +#command DELETE => __VfpDeleteRecord() -#command RECALL IN <(a)> => (a)->(__DbRecall()) +#command RECALL IN <(a)> => (<(a)>)->(__VfpRecallRecord()) +#command RECALL => __VfpRecallRecord() // Set Relation command with IN clause diff --git a/src/Common/FoxProSql.xh b/src/Common/FoxProSql.xh index b75f776cbf..9b26654fb2 100644 --- a/src/Common/FoxProSql.xh +++ b/src/Common/FoxProSql.xh @@ -58,8 +58,14 @@ // SQL Delete, mapped to normal delete #command DELETE [<*clauses*>] WHERE [<*moreclauses*>] => DELETE FOR -#command DELETE <*target*> FROM <(a)> [<*clauses*>] => DELETE IN -#command DELETE FROM <(a)> [<*clauses*>] => DELETE IN +#command DELETE <*target*> FROM <(a)> [<*clauses*>] => DELETE IN ; _TALLY := RuntimeState.Tally +#command DELETE FROM <(a)> [<*clauses*>] => DELETE IN ; _TALLY := RuntimeState.Tally + +// Temporary: DELETE FROM without WHERE deletes only the current record +// (VFP deletes them all). Until that is fixed the command must not overwrite +// _TALLY with a stale RuntimeState.Tally, since no DbEval() runs. Remove this +// rule together with that fix. +#command DELETE FROM <(a)> => DELETE IN // Delete with Join or Subselect is not supported #command DELETE [<*clauses*>] <*moreclauses*> => __NOFOXPROSQL__ <(udc)> diff --git a/src/Runtime/XSharp.VFP.Tests/TallyTests.prg b/src/Runtime/XSharp.VFP.Tests/TallyTests.prg index feb55054c6..8fe391833d 100644 --- a/src/Runtime/XSharp.VFP.Tests/TallyTests.prg +++ b/src/Runtime/XSharp.VFP.Tests/TallyTests.prg @@ -14,16 +14,6 @@ BEGIN NAMESPACE XSharp.VFP.Tests XSharp.RuntimeState.Dialect := XSharpDialect.FoxPro END CONSTRUCTOR - PRIVATE METHOD CreateTestCursor() AS VOID - CREATE CURSOR curtally (f I) - INSERT INTO curtally VALUES (1) - INSERT INTO curtally VALUES (2) - INSERT INTO curtally VALUES (3) - INSERT INTO curtally VALUES (4) - INSERT INTO curtally VALUES (5) - GO TOP - END METHOD - [Fact, Trait("Category", "Tally")]; METHOD CountWithoutToClauseSetsTally AS VOID TRY @@ -131,5 +121,473 @@ BEGIN NAMESPACE XSharp.VFP.Tests END TRY END METHOD + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceOnCurrentRecordSetsTallyToOne AS VOID + TRY + SELF:CreateTestCursor() + GO TOP + _TALLY := -1 + REPLACE f WITH 99 + Assert.Equal(1, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceMultipleFieldsOnCurrentRecordSetsTallyToOne AS VOID + TRY + SELF:CreateTwoFieldCursor() + GO TOP + _TALLY := -1 + REPLACE f WITH 99, c WITH "zz" + Assert.Equal(1, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceAllSetsTally AS VOID + TRY + SELF:CreateTestCursor() + _TALLY := -1 + REPLACE ALL f WITH 99 + Assert.Equal(5, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceForSetsTally AS VOID + TRY + SELF:CreateTestCursor() + _TALLY := -1 + REPLACE ALL f WITH 99 FOR curtally->f > 3 + Assert.Equal(2, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceWithoutMatchesResetsTallyToZero AS VOID + TRY + SELF:CreateTestCursor() + _TALLY := -1 + REPLACE ALL f WITH 99 FOR curtally->f > 99 + Assert.Equal(0, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceNextSetsTally AS VOID + TRY + SELF:CreateTestCursor() + GO TOP + _TALLY := -1 + REPLACE f WITH 99 NEXT 3 + Assert.Equal(3, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceNextBeyondEofCountsOnlyTheRemainingRecords AS VOID + TRY + SELF:CreateTestCursor() + GO 3 + _TALLY := -1 + REPLACE f WITH 99 NEXT 10 + Assert.Equal(3, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceRestSetsTally AS VOID + TRY + SELF:CreateTestCursor() + GO 4 + _TALLY := -1 + REPLACE f WITH 99 REST + Assert.Equal(2, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ReplaceInAliasSetsTallyToOne AS VOID + TRY + SELF:CreateTestCursor() + SELF:CreateOtherCursor() + _TALLY := -1 + REPLACE f WITH 99 IN curtally + Assert.Equal(1, (INT) _TALLY) + FINALLY + USE IN curother + USE IN curtally + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD DeleteOnCurrentRecordSetsTallyToOne AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO TOP + _TALLY := -1 + DELETE + Assert.Equal(1, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD DeleteOnAnAlreadyDeletedRecordSetsTallyToZero AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO TOP + DELETE + GO TOP + _TALLY := -1 + DELETE + Assert.Equal(0, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD DeleteAllSetsTally AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + _TALLY := -1 + DELETE ALL + Assert.Equal(5, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD DeleteAllCountsAffectedRecordsNotVisitedOnes AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO 5 + DELETE + GO TOP + _TALLY := -1 + DELETE ALL + // one record was already deleted: VFP reports 4, not 5 + Assert.Equal(4, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD DeleteForSetsTally AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + _TALLY := -1 + DELETE ALL FOR curtally->f > 3 + Assert.Equal(2, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD DeleteNextSetsTally AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO TOP + _TALLY := -1 + DELETE NEXT 2 + Assert.Equal(2, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD RecallOnADeletedRecordSetsTallyToOne AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO 2 + DELETE + GO 2 + _TALLY := -1 + RECALL + Assert.Equal(1, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD RecallOnARecordThatIsNotDeletedSetsTallyToZero AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO 2 + _TALLY := -1 + RECALL + Assert.Equal(0, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD RecallAllCountsOnlyTheDeletedRecords AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO 2 + DELETE + _TALLY := -1 + RECALL ALL + // 5 records visited, only 1 was deleted + Assert.Equal(1, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD RecallAllWithNothingDeletedSetsTallyToZero AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + _TALLY := -1 + RECALL ALL + Assert.Equal(0, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD RecallAllIgnoresSetDeletedOn AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + DELETE ALL FOR curtally->f > 3 + SetDeleted(TRUE) + _TALLY := -1 + RECALL ALL + // regression: RECALL has to see the deleted records even with + // SET DELETED ON (Peter Stephan, RECALL + SET DELETED ON) + Assert.Equal(2, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD RecallForSetsTally AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + DELETE ALL FOR curtally->f > 3 + _TALLY := -1 + RECALL ALL FOR curtally->f > 3 + Assert.Equal(2, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD RecallInAliasResolvesTheAlias AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO 2 + DELETE + GO 2 + SELF:CreateOtherCursor() + _TALLY := -1 + // regression: the rule emitted (a)-> and did not even compile + RECALL IN curtally + Assert.Equal(1, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE IN curother + USE IN curtally + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD ContinueInAliasResolvesTheAlias AS VOID + TRY + SELF:CreateTestCursor() + LOCATE FOR curtally->f > 3 + SELF:CreateOtherCursor() + // regression: same (a)-> bug as RECALL IN + CONTINUE IN curtally + Assert.Equal(5, (INT) curtally->f) + FINALLY + USE IN curother + USE IN curtally + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD SumSetsTally AS VOID + LOCAL n AS USUAL + TRY + SELF:CreateTestCursor() + _TALLY := -1 + SUM curtally->f TO n + Assert.Equal(5, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD SumWithForSetsTally AS VOID + LOCAL n AS USUAL + TRY + SELF:CreateTestCursor() + _TALLY := -1 + SUM curtally->f TO n FOR curtally->f > 3 + Assert.Equal(2, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD AverageSetsTally AS VOID + LOCAL n AS USUAL + TRY + SELF:CreateTestCursor() + _TALLY := -1 + AVERAGE curtally->f TO n + Assert.Equal(5, (INT) _TALLY) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD AverageComputesTheMean AS VOID + LOCAL n AS USUAL + TRY + SELF:CreateTestCursor() + AVERAGE curtally->f TO n + // regression: the rule in dbcmd.xh never matched, so AVERAGE + // did not even translate + Assert.Equal(3, (INT) n) + FINALLY + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD DeleteSqlSetsTally AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + _TALLY := -1 + DELETE FROM curtally WHERE curtally->f > 3 + Assert.Equal(2, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + [Fact, Trait("Category", "Tally")]; + METHOD DeleteSqlCountsMatchedRowsNotAffectedOnes AS VOID + LOCAL lOld AS LOGIC + TRY + lOld := SetDeleted(FALSE) + SELF:CreateTestCursor() + GO 5 + DELETE + GO TOP + _TALLY := -1 + DELETE FROM curtally WHERE curtally->f > 0 + // DELETE - SQL counts the rows matching the WHERE clause, so + // the already deleted record still counts: 5, where the xBase + // DELETE ALL FOR over the same records reports 4 + Assert.Equal(5, (INT) _TALLY) + FINALLY + SetDeleted(lOld) + USE + END TRY + END METHOD + + PRIVATE METHOD CreateTestCursor() AS VOID + CREATE CURSOR curtally (f I) + INSERT INTO curtally VALUES (1) + INSERT INTO curtally VALUES (2) + INSERT INTO curtally VALUES (3) + INSERT INTO curtally VALUES (4) + INSERT INTO curtally VALUES (5) + GO TOP + END METHOD + + PRIVATE METHOD CreateTwoFieldCursor() AS VOID + CREATE CURSOR curtally (f I, c C(10)) + INSERT INTO curtally VALUES(1, "a") + INSERT INTO curtally VALUES(2, "b") + INSERT INTO curtally VALUES(3, "c") + INSERT INTO curtally VALUES(4, "d") + INSERT INTO curtally VALUES(5, "e") + GO TOP + END METHOD + + PRIVATE METHOD CreateOtherCursor() AS VOID + SELECT 0 + CREATE CURSOR curother (g I) + INSERT INTO curother VALUES(1) + GO TOP + END METHOD + END CLASS END NAMESPACE diff --git a/src/Runtime/XSharp.VFP/Commands.prg b/src/Runtime/XSharp.VFP/Commands.prg index 2a023dfe2a..eb9ba09b07 100644 --- a/src/Runtime/XSharp.VFP/Commands.prg +++ b/src/Runtime/XSharp.VFP/Commands.prg @@ -230,3 +230,49 @@ FUNCTION __VfpCount(cbFor, cbWhile, nNext, nRecord, lRest, lNoOpt) AS LONG CLIPP DbEval({||TRUE}, cbFor, cbWhile, nNext, nRecord, lRest, lNoOpt) _TALLY := RuntimeState.Tally RETURN _TALLY + +/// +FUNCTION __VfpDelete(cbFor, cbWhile, nNext, nRecord, lRest, lNoOpt) AS LONG CLIPPER + LOCAL nCount := 0 AS LONG + // _TALLY counts the records actually deleted: a record that was already + // deleted is visited but not counted, so RuntimeState.Tally (which counts + // every record matching the scope) cannot be used here + DbEval({|| nCount += IIF(Deleted(), 0, 1), __DbDelete()}, ; + cbFor, cbWhile, nNext, nRecord, lRest, lNoOpt) + _TALLY := nCount + RETURN nCount + +/// +FUNCTION __VfpDeleteRecord() AS LOGIC + IF Deleted() + _TALLY := 0 + ELSE + _TALLY := 1 + ENDIF + RETURN __DbDelete() + +/// +FUNCTION __VfpRecall(cbFor, cbWhile, nNext, nRecord, lRest, lNoOpt) AS LONG CLIPPER + LOCAL nCount := 0 AS LONG + LOCAL lOldDeleted AS LOGIC + // RECALL has to visit the deleted records, which DbEval() skips when + // SET DELETED is ON. Only the scan is affected: the caller has already + // positioned the cursor, and VFP does honour SET DELETED for that + lOldDeleted := SetDeleted(FALSE) + TRY + DbEval({|| nCount += IIF(Deleted(), 1, 0), __DbRecall()}, ; + cbFor, cbWhile, nNext, nRecord, lRest, lNoOpt) + FINALLY + SetDeleted(lOldDeleted) + END TRY + _TALLY := nCount + RETURN nCount + +/// +FUNCTION __VfpRecallRecord() AS LOGIC + IF Deleted() + _TALLY := 1 + ELSE + _TALLY := 0 + ENDIF + RETURN __DbRecall()