diff --git a/src/Common/FoxProCmd.xh b/src/Common/FoxProCmd.xh index 67573bd02b..15c605818d 100644 --- a/src/Common/FoxProCmd.xh +++ b/src/Common/FoxProCmd.xh @@ -230,28 +230,61 @@ #command COPY MEMO TO <(file)> [AS ] ; => DbCopyMemo( <"field">, <(file)>, [, ] ) -// REPLACE ... [ADDITIVE] — without IN clause -#command REPLACE <(f1)> WITH [] ; - [, <(fn)> WITH []] ; +// Scoped REPLACE — without IN clause +// 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 ] ; [FOR ] [WHILE ] [NEXT ] [RECORD ] ; [] [] [all] ; => DbEval( ; {|| DbAutoLock(), ; - __FieldSetAdd(<(f1)>, , <.add1.>) ; - [, __FieldSetAdd(<(fn)>, , <.addn.>)], ; + __FieldSet(<(f1)>, ) ; + [, __FieldSet(<(fn)>, )], ; DbAutoUnLock() }, ; <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ; ) -// REPLACE ... [ADDITIVE] IN workarea — defined LAST, tried FIRST -#command REPLACE <(f1)> WITH [] ; - [, <(fn)> WITH []] ; +// Scoped REPLACE, single field with ADDITIVE — without IN clause +#command REPLACE <(f1)> WITH ADDITIVE ; + [FOR ] [WHILE ] [NEXT ] [RECORD ] ; + [] [] [ALL] ; + => DbEval( ; + {|| DbAutoLock(), ; + __FieldSetAdd(<(f1)>, , .T.), ; + DbAutoUnLock() }, ; + <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ; + ) + +// 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() + +// Single record, single field with ADDITIVE — current record only +#command REPLACE <(f1)> WITH ADDITIVE ; + => DbAutoLock(); __FieldSetAdd(<(f1)>, , .T.); DbAutoUnLock() + +// Scoped REPLACE IN workarea +#command REPLACE <(f1)> WITH [, <(fn)> WITH ] ; + [FOR ] [WHILE ] [NEXT ] [RECORD ] ; + [] IN <(a)> [] [ALL] ; + => (<(a)>)->(DbEval( ; + {|| DbAutoLock(), ; + __FieldSet(<(f1)>, ) ; + [, __FieldSet(<(fn)>, )], ; + DbAutoUnLock() }, ; + <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ; + )) + +// Scoped REPLACE, single field with ADDITIVE, IN workarea +#command REPLACE <(f1)> WITH ADDITIVE ; [FOR ] [WHILE ] [NEXT ] [RECORD ] ; [] IN <(a)> [] [all] ; => (<(a)>)->(DbEval( ; {|| DbAutoLock(), ; - __FieldSetAdd(<(f1)>, , <.add1.>) ; - [, __FieldSetAdd(<(fn)>, , <.addn.>)], ; + __FieldSetAdd(<(f1)>, , .T.), ; DbAutoUnLock() }, ; <{lfor}>, <{lwhile}>, , , <.rest.>, <.noopt.> ; )) @@ -311,6 +344,18 @@ IN <(a)> ; => DbAutoLockArea(<(a)>), __FieldSetWa(<(a)>, <(f1)>,) [,__FieldSetWa(<(a)>,<(fn)>,)], DbAutoUnLockArea(<(a)>) +// 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)>) + +// 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)>) + #command DELETE IN <(a)> => (<(a)>)->(__DbDelete()) #command RECALL IN <(a)> => (a)->(__DbRecall()) diff --git a/src/Runtime/XSharp.VFP.Tests/ReplaceScopeTests.prg b/src/Runtime/XSharp.VFP.Tests/ReplaceScopeTests.prg new file mode 100644 index 0000000000..01f86be7dd --- /dev/null +++ b/src/Runtime/XSharp.VFP.Tests/ReplaceScopeTests.prg @@ -0,0 +1,242 @@ +// +// Copyright (c) XSharp B.V. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. +// See License.txt in the project root for license information. +// +USING System +USING System.Collections.Generic +USING System.Text +USING XUnit + +BEGIN NAMESPACE XSharp.VFP.Tests + + CLASS ReplaceScopeTests + + STATIC CONSTRUCTOR + XSharp.RuntimeState.Dialect := XSharpDialect.FoxPro + END CONSTRUCTOR + + PRIVATE METHOD CreateTestCursor() AS VOID + LOCAL i AS LONG + CREATE CURSOR curRepl (id N(4), grupo C(1), nombre C(20), notas M) + FOR i := 1 TO 10 + INSERT INTO curRepl VALUES (i, IIF(i % 2 == 0, "P", "I"), "ORIG", "memo") + NEXT + GO TOP + END METHOD + + PRIVATE METHOD CountName(cValue AS STRING) AS LONG + FIELD nombre + VAR nCount := 0 + VAR nOldRec := RECNO() + COUNT FOR ALLTRIM(nombre) == cValue TO nCount + IF nOldRec <= RECCOUNT() + GO nOldRec + ENDIF + RETURN nCount + END METHOD + + PRIVATE METHOD CountMemo(cValue AS STRING) AS LONG + FIELD notas + LOCAL nCount := 0 AS LONG + VAR nOldRec := RECNO() + COUNT FOR ALLTRIM(notas) == cValue TO nCount + IF nOldRec <= RECCOUNT() + GO nOldRec + ENDIF + RETURN nCount + END METHOD + + // --- REPLACE without scope: current record only (VFP default) --- + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceNoScopeUpdatesOnlyCurrentRecord AS VOID + FIELD nombre + SELF:CreateTestCursor() + GO 3 + REPLACE nombre WITH "X1" + Assert.Equal(1, SELF:CountName("X1")) + Assert.Equal((DWORD)3, RECNO()) + Assert.Equal("X1", ALLTRIM(nombre)) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceNoScopeMultiFieldUpdatesAllFields AS VOID + FIELD nombre, grupo + SELF:CreateTestCursor() + GO 5 + REPLACE nombre WITH "X2", grupo WITH "Z" + Assert.Equal(1, SELF:CountName("X2")) + GO 5 + Assert.Equal("Z", grupo) + GO 6 + Assert.Equal("ORIG", ALLTRIM(nombre)) + END METHOD + + // --- Explicit scopes --- + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceAllUpdatesAllRecords AS VOID + SELF:CreateTestCursor() + GO 3 + REPLACE nombre WITH "X3" ALL + Assert.Equal(10, SELF:CountName("X3")) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceAllPrefixUpdatesAllRecords AS VOID + SELF:CreateTestCursor() + REPLACE ALL nombre WITH "X4" + Assert.Equal(10, SELF:CountName("X4")) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceAllMultiFieldUpdatesAllFields AS VOID + // regression: the second field was silently dropped by the UDC + FIELD grupo + SELF:CreateTestCursor() + LOCAL nCount := 0 AS LONG + REPLACE ALL nombre WITH "X5", grupo WITH "W" + Assert.Equal(10, SELF:CountName("X5")) + COUNT FOR grupo == "W" TO nCount + Assert.Equal(10, nCount) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceForUpdatesMatchingRecords AS VOID + FIELD grupo + SELF:CreateTestCursor() + REPLACE nombre WITH "X6" FOR grupo == "P" + Assert.Equal(5, SELF:CountName("X6")) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceNextUpdatesScopedRecords AS VOID + FIELD nombre + SELF:CreateTestCursor() + GO 4 + REPLACE nombre WITH "X7" NEXT 3 + Assert.Equal(3, SELF:CountName("X7")) + GO 4 + Assert.Equal("X7", ALLTRIM(nombre)) + GO 7 + Assert.Equal("ORIG", ALLTRIM(nombre)) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceRestUpdatesFromCurrentRecord AS VOID + SELF:CreateTestCursor() + GO 8 + REPLACE nombre WITH "X8" REST + Assert.Equal(3, SELF:CountName("X8")) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceRecordUpdatesSpecificRecord AS VOID + FIELD nombre + SELF:CreateTestCursor() + GO 1 + REPLACE nombre WITH "X9" RECORD 7 + Assert.Equal(1, SELF:CountName("X9")) + GO 7 + Assert.Equal("X9", ALLTRIM(nombre)) + END METHOD + + // --- ADDITIVE --- + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceAdditiveAppendsToMemoCurrentRecordOnly AS VOID + FIELD notas + SELF:CreateTestCursor() + GO 2 + REPLACE notas WITH "+X" ADDITIVE + Assert.Equal("memo+X", ALLTRIM(notas)) + Assert.Equal(1, SELF:CountMemo("memo+X")) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceAdditiveOnCharFieldIsIgnored AS VOID + // per VFP spec ADDITIVE only applies to memo fields + FIELD nombre + SELF:CreateTestCursor() + GO 2 + REPLACE nombre WITH "X10" ADDITIVE + Assert.Equal("X10", ALLTRIM(nombre)) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceAdditiveWithScopeAppendsToMatchingRecords AS VOID + FIELD grupo + SELF:CreateTestCursor() + REPLACE notas WITH "+A" ADDITIVE FOR grupo == "P" + Assert.Equal(5, SELF:CountMemo("memo+A")) + END METHOD + + // --- IN workarea --- + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceInUpdatesOnlyCurrentRecordOfAlias AS VOID + FIELD nombre + SELF:CreateTestCursor() + GO 3 + CREATE CURSOR curCtx (dummy C(1)) + INSERT INTO curCtx VALUES ("x") + REPLACE nombre WITH "X11" IN curRepl + Assert.Equal("CURCTX", ALIAS()) + SELECT curRepl + Assert.Equal((DWORD)3, RECNO()) + Assert.Equal(1, SELF:CountName("X11")) + Assert.Equal("X11", ALLTRIM(nombre)) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceInMultiFieldUpdatesAllFields AS VOID + FIELD grupo + SELF:CreateTestCursor() + GO 4 + CREATE CURSOR curCtx (dummy C(1)) + INSERT INTO curCtx VALUES ("x") + REPLACE nombre WITH "X12", grupo WITH "Z" IN curRepl + SELECT curRepl + Assert.Equal(1, SELF:CountName("X12")) + GO 4 + Assert.Equal("Z", grupo) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceAdditiveInAppendsToMemoOfAlias AS VOID + FIELD notas + SELF:CreateTestCursor() + GO 3 + CREATE CURSOR curCtx (dummy C(1)) + INSERT INTO curCtx VALUES ("x") + REPLACE notas WITH "+Y" ADDITIVE IN curRepl + SELECT curRepl + Assert.Equal("memo+Y", ALLTRIM(notas)) + Assert.Equal(1, SELF:CountMemo("memo+Y")) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceAllInUpdatesAllRecordsOfAlias AS VOID + SELF:CreateTestCursor() + CREATE CURSOR curCtx (dummy C(1)) + INSERT INTO curCtx VALUES ("x") + REPLACE nombre WITH "X13" ALL IN curRepl + SELECT curRepl + Assert.Equal(10, SELF:CountName("X13")) + END METHOD + + [Fact, Trait("Category", "Replace")]; + METHOD ReplaceForInUpdatesMatchingRecordsOfAlias AS VOID + FIELD grupo + SELF:CreateTestCursor() + CREATE CURSOR curCtx (dummy C(1)) + INSERT INTO curCtx VALUES ("x") + REPLACE nombre WITH "X14" FOR grupo == "P" IN curRepl + SELECT curRepl + Assert.Equal(5, SELF:CountName("X14")) + END METHOD + + END CLASS + +END NAMESPACE diff --git a/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj b/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj index 85e2de6d35..e3e89c549f 100644 --- a/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj +++ b/src/Runtime/XSharp.VFP.Tests/XSharp.VFP.Tests.xsproj @@ -101,6 +101,7 @@ +