浏览代码

fcl-db: bufdataset: implement RevertRecord method for canceling one record cached update.
(There might be problem in special cases when data are saved/restored)

git-svn-id: trunk@33420 -

lacak 9 年之前
父节点
当前提交
68f626f725
共有 2 个文件被更改,包括 205 次插入96 次删除
  1. 123 89
      packages/fcl-db/src/base/bufdataset.pas
  2. 82 7
      packages/fcl-db/tests/testdbbasics.pas

+ 123 - 89
packages/fcl-db/src/base/bufdataset.pas

@@ -89,6 +89,7 @@ type
 }
     BookmarkData       : TBufBookmark;
 {  NextBookMarkData:
+     - If UpdateKind is ukInsert, it contains a bookmark to the record just after the newly crealed record
      - If UpdateKind is ukDelete, it contains a bookmark to the record just after the deleted record
 }
     NextBookmarkData   : TBufBookmark;
@@ -496,6 +497,7 @@ type
     function GetRecordUpdateBuffer(const ABookmark : TBufBookmark; IncludePrior : boolean = false; AFindNext : boolean = false) : boolean;
     function GetRecordUpdateBufferCached(const ABookmark : TBufBookmark; IncludePrior : boolean = false) : boolean;
     function GetActiveRecordUpdateBuffer : boolean;
+    procedure CancelRecordUpdateBuffer(AUpdateBufferIndex: integer; var ABookmark: TBufBookmark);
     procedure ParseFilter(const AFilter: string);
 
     function GetIndexDefs : TIndexDefs;
@@ -575,6 +577,7 @@ type
     procedure ApplyUpdates; virtual; overload;
     procedure ApplyUpdates(MaxErrors: Integer); virtual; overload;
     procedure MergeChangeLog;
+    procedure RevertRecord;
     procedure CancelUpdates; virtual;
     destructor Destroy; override;
     function Locate(const KeyFields: string; const KeyValues: Variant; Options: TLocateOptions) : boolean; override;
@@ -2401,90 +2404,117 @@ begin
   raise EDatabaseError.Create(SApplyRecNotSupported);
 end;
 
-procedure TCustomBufDataset.CancelUpdates;
-var StoreRecBM     : TBufBookmark;
-  procedure CancelUpdBuffer(var AUpdBuffer : TRecUpdateBuffer);
-  var
-    TmpBuf         : TRecordBuffer;
-    StoreUpdBuf    : integer;
-    Bm             : TBufBookmark;
-  begin
-    with AUpdBuffer do
+procedure TCustomBufDataset.CancelRecordUpdateBuffer(AUpdateBufferIndex: integer; var ABookmark: TBufBookmark);
+var
+  ARecordBuffer: TRecordBuffer;
+  i            : integer;
+begin
+  with FUpdateBuffer[AUpdateBufferIndex] do
+    if Assigned(BookmarkData.BookmarkData) then // this is used to exclude buffers which are already handled
       begin
-      if Not assigned(BookmarkData.BookmarkData) then
-        exit;// this is used to exclude buffers which are already handled
-      Case UpdateKind of
-      ukModify:
-        begin
-        FCurrentIndex.GotoBookmark(@BookmarkData);
-        move(TRecordBuffer(OldValuesBuffer)^,TRecordBuffer(FCurrentIndex.CurrentBuffer)^,FRecordSize);
-        FreeRecordBuffer(OldValuesBuffer);
-        end;
-      ukDelete:
-        if (assigned(OldValuesBuffer)) then
+      case UpdateKind of
+        ukModify:
           begin
-          FCurrentIndex.GotoBookmark(@NextBookmarkData);
-          FCurrentIndex.InsertRecordBeforeCurrentRecord(TRecordBuffer(BookmarkData.BookmarkData));
-          FCurrentIndex.ScrollBackward;
-          move(TRecordBuffer(OldValuesBuffer)^,TRecordBuffer(FCurrentIndex.CurrentBuffer)^,FRecordSize);
-
-          {for x := length(FUpdateBuffer)-1 downto 0 do
-            begin
-            if (FUpdateBuffer[x].UpdateKind=ukDelete) and FCurrentIndex.SameBookmarks(@FUpdateBuffer[x].NextBookmarkData,@BookmarkData) then
-              CancelUpdBuffer(FUpdateBuffer[x]);
-            end;}
+          FCurrentIndex.GotoBookmark(@BookmarkData);
+          move(TRecordBuffer(OldValuesBuffer)^, TRecordBuffer(FCurrentIndex.CurrentBuffer)^, FRecordSize);
           FreeRecordBuffer(OldValuesBuffer);
-          inc(FBRecordCount);
-          end  ;
-      ukInsert:
-        begin
-        // Process all update buffers linked to this record before this record is removed
-        StoreUpdBuf:=FCurrentUpdateBuffer;
-        Bm := BookmarkData;
-        BookmarkData.BookmarkData:=nil; // Avoid infinite recursion...
-        if GetRecordUpdateBuffer(Bm,True,False) then
-          begin
-          repeat
-            if (FCurrentUpdateBuffer<>StoreUpdBuf) then
-              CancelUpdBuffer(FUpdateBuffer[FCurrentUpdateBuffer]);
-          until not GetRecordUpdateBuffer(Bm,True,True);
           end;
-        FCurrentUpdateBuffer:=StoreUpdBuf;
-
-        FCurrentIndex.GotoBookmark(@Bm);
-        TmpBuf:=FCurrentIndex.CurrentRecord;
-        // resync won't work if the currentbuffer is freed...
-        if FCurrentIndex.SameBookmarks(@Bm,@StoreRecBM) then with FCurrentIndex do
+        ukDelete:
+          if (assigned(OldValuesBuffer)) then
+            begin
+            FCurrentIndex.GotoBookmark(@NextBookmarkData);
+            FCurrentIndex.InsertRecordBeforeCurrentRecord(TRecordBuffer(BookmarkData.BookmarkData));
+            FCurrentIndex.ScrollBackward;
+            move(TRecordBuffer(OldValuesBuffer)^, TRecordBuffer(FCurrentIndex.CurrentBuffer)^, FRecordSize);
+            FreeRecordBuffer(OldValuesBuffer);
+            inc(FBRecordCount);
+            end;
+        ukInsert:
           begin
-          GotoBookmark(@StoreRecBM);
-          if ScrollForward = grEOF then
-            if ScrollBackward = grBOF then
-              ScrollLast;  // last record will be removed from index, so move to spare record
-          StoreCurrentRecIntoBookmark(@StoreRecBM);
+          FCurrentIndex.GotoBookmark(@BookmarkData);
+          ARecordBuffer := FCurrentIndex.CurrentRecord;
+
+          // Process all update buffers linked to this record before this record is removed
+          //  Deleted records (modified+deleted) can be before this record (inserted are after this record).
+          //  modified record #1, which is later deleted can be linked to another inserted record #2. In this case deleted record #1 precedes inserted #2 in update buffer.
+          //  if we need revert inserted record which is linked from another inserted or deleted record, then we must re-link these records
+          for i:=0 to high(FUpdateBuffer) do
+            if (FUpdateBuffer[i].UpdateKind in [ukInsert, ukDelete]) and
+               (FUpdateBuffer[i].NextBookmarkData.BookmarkData = BookmarkData.BookmarkData) then
+              FUpdateBuffer[i].NextBookmarkData := NextBookmarkData;
+{
+          TmpBookmark := BookmarkData;
+          BookmarkData.BookmarkData := nil; // Avoid infinite recursion...
+          if GetRecordUpdateBuffer(TmpBookmark,True,False) then
+            repeat
+              if (FCurrentUpdateBuffer<>AUpdateBufferIndex) then
+            if IncludePrior then
+              CancelRecordUpdateBuffer(FCurrentUpdateBuffer, ABookmark, IncludePrior)
+            else
+              // re-link update buffers linked to this record to next record, because this record will be removed from update buffers
+              FUpdateBuffer[FCurrentUpdateBuffer].NextBookmarkData := FUpdateBuffer[AUpdateBufferIndex].NextBookmarkData;
+            until not GetRecordUpdateBuffer(TmpBookmark,True,True);
+
+          FCurrentUpdateBuffer := AUpdateBufferIndex;
+          FCurrentIndex.GotoBookmark(@TmpBookmark);
+}
+          // ReSync won't work if the CurrentBuffer is freed ...
+          if FCurrentIndex.SameBookmarks(@BookmarkData,@ABookmark) then with FCurrentIndex do
+            begin
+            GotoBookmark(@ABookmark);
+            if ScrollForward = grEOF then
+              if ScrollBackward = grBOF then
+                ScrollLast;  // last record will be removed from index, so move to spare record
+            StoreCurrentRecIntoBookmark(@ABookmark);
+            end;
+
+          RemoveRecordFromIndexes(BookmarkData);
+          FreeRecordBuffer(ARecordBuffer);
+          dec(FBRecordCount);
           end;
-        RemoveRecordFromIndexes(Bm);
-        FreeRecordBuffer(TmpBuf);
-        dec(FBRecordCount);
-        end;
       end;
-      BookmarkData.BookmarkData:=nil;
+      BookmarkData.BookmarkData := nil;
       end;
-  end;
+end;
+
+procedure TCustomBufDataset.RevertRecord;
+var
+  ABookmark : TBufBookmark;
+begin
+  CheckBrowseMode;
+
+  if GetActiveRecordUpdateBuffer then
+  begin
+    FCurrentIndex.StoreCurrentRecIntoBookmark(@ABookmark);
+
+    CancelRecordUpdateBuffer(FCurrentUpdateBuffer, ABookmark);
+
+    // remove update record of current record from update-buffer array
+    Move(FUpdateBuffer[FCurrentUpdateBuffer+1], FUpdateBuffer[FCurrentUpdateBuffer], (High(FUpdateBuffer)-FCurrentUpdateBuffer)*SizeOf(TRecUpdateBuffer));
+    SetLength(FUpdateBuffer, High(FUpdateBuffer));
 
-var r              : Integer;
+    FCurrentIndex.GotoBookmark(@ABookmark);
 
+    Resync([]);
+  end;
+end;
+
+procedure TCustomBufDataset.CancelUpdates;
+var
+  ABookmark : TBufBookmark;
+  r         : Integer;
 begin
   CheckBrowseMode;
 
   if Length(FUpdateBuffer) > 0 then
     begin
-    FCurrentIndex.StoreCurrentRecIntoBookmark(@StoreRecBM);
-    for r := Length(FUpdateBuffer) - 1 downto 0 do
-      CancelUpdBuffer(FUpdateBuffer[r]);
+    FCurrentIndex.StoreCurrentRecIntoBookmark(@ABookmark);
 
-    SetLength(FUpdateBuffer,0);
+    for r := High(FUpdateBuffer) downto 0 do
+      CancelRecordUpdateBuffer(r, ABookmark);
+    SetLength(FUpdateBuffer, 0);
     
-    FCurrentIndex.GotoBookmark(@StoreRecBM);
+    FCurrentIndex.GotoBookmark(@ABookmark);
     
     Resync([]);
     end;
@@ -2635,7 +2665,7 @@ begin
       FAutoIncField.AsInteger := FAutoIncValue;
       inc(FAutoIncValue);
       end;
-    // The active buffer is the newly created TDataset record,
+    // The active buffer is the newly created TDataSet record,
     // from which the bookmark is set to the record where the new record should be
     // inserted
     ABookmark := PBufBookmark(ActiveBuffer + FRecordSize);
@@ -2653,12 +2683,13 @@ begin
           // insert (before current record)
           FIndexes[i].GotoBookmark(ABookmark);
 
+        // insert new record before current record
         FIndexes[i].InsertRecordBeforeCurrentRecord(ABuff);
         // newly inserted record becomes current record
         FIndexes[i].ScrollBackward;
       end;
 
-    // Link the newly created record buffer to the newly created TDataset record
+    // Link the newly created record buffer to the newly created TDataSet record
     FCurrentIndex.StoreCurrentRecIntoBookmark(ABookmark);
     ABookmark^.BookmarkFlag := bfInserted;
 
@@ -2679,17 +2710,20 @@ begin
 
     if State = dsEdit then
       begin
-      // Create an oldvalues buffer with the old values of the record
-      FUpdateBuffer[FCurrentUpdateBuffer].OldValuesBuffer := IntAllocRecordBuffer;
-      with FCurrentIndex do
-        // Move only the real data
-        move(CurrentBuffer^,FUpdateBuffer[FCurrentUpdateBuffer].OldValuesBuffer^,FRecordSize);
+      // Create an OldValues buffer with the old values of the record
       FUpdateBuffer[FCurrentUpdateBuffer].UpdateKind := ukModify;
+      FUpdateBuffer[FCurrentUpdateBuffer].OldValuesBuffer := IntAllocRecordBuffer;
+      // Move only the real data
+      move(FCurrentIndex.CurrentBuffer^, FUpdateBuffer[FCurrentUpdateBuffer].OldValuesBuffer^, FRecordSize);
       end
     else
       begin
       FUpdateBuffer[FCurrentUpdateBuffer].UpdateKind := ukInsert;
       FUpdateBuffer[FCurrentUpdateBuffer].OldValuesBuffer := nil;
+      // store a bookmark of next record into updatebuffer's nextbookmark
+      FCurrentIndex.DoScrollForward;
+      FCurrentIndex.StoreCurrentRecIntoBookmark(@FUpdateBuffer[FCurrentUpdateBuffer].NextBookmarkData);
+      FCurrentIndex.ScrollBackward;
       end;
     end;
 
@@ -3018,12 +3052,10 @@ procedure TCustomBufDataset.GetDatasetPacket(AWriter: TDataPacketReader);
       begin
       AStoreUpdBuf:=FCurrentUpdateBuffer;
       if GetRecordUpdateBuffer(AUpdBuffer.BookmarkData,True,False) then
-        begin
         repeat
           if FCurrentIndex.SameBookmarks(@FUpdateBuffer[FCurrentUpdateBuffer].NextBookmarkData, @AUpdBuffer.BookmarkData) then
             StoreUpdateBuffer(FUpdateBuffer[FCurrentUpdateBuffer], ARowState);
-        until not GetRecordUpdateBuffer(AUpdBuffer.BookmarkData,True,True)
-        end;
+        until not GetRecordUpdateBuffer(AUpdBuffer.BookmarkData,True,True);
       FCurrentUpdateBuffer:=AStoreUpdBuf;
       AThisRowState := [rsvDeleted];
       end
@@ -3036,16 +3068,16 @@ procedure TCustomBufDataset.GetDatasetPacket(AWriter: TDataPacketReader);
       FDatasetReader.StoreRecord(AThisRowState,FCurrentUpdateBuffer);
   end;
 
-  procedure HandleUpdateBuffersFromRecord(AFirstCall : boolean;ARecBookmark : TBufBookmark; var ARowState: TRowState);
+  procedure HandleUpdateBuffersFromRecord(AFindNext : boolean; ARecBookmark : TBufBookmark; var ARowState: TRowState);
   var StoreUpdBuf1,StoreUpdBuf2 : Integer;
   begin
-    if AFirstCall then ARowState:=[];
-    if GetRecordUpdateBuffer(ARecBookmark,True,not AFirstCall) then
+    if not AFindNext then ARowState:=[];
+    if GetRecordUpdateBuffer(ARecBookmark,True,AFindNext) then
       begin
       if FUpdateBuffer[FCurrentUpdateBuffer].UpdateKind=ukDelete then
         begin
         StoreUpdBuf1:=FCurrentUpdateBuffer;
-        HandleUpdateBuffersFromRecord(False,ARecBookmark,ARowState);
+        HandleUpdateBuffersFromRecord(True,ARecBookmark,ARowState);
         StoreUpdBuf2:=FCurrentUpdateBuffer;
         FCurrentUpdateBuffer:=StoreUpdBuf1;
         StoreUpdateBuffer(FUpdateBuffer[StoreUpdBuf1], ARowState);
@@ -3054,7 +3086,7 @@ procedure TCustomBufDataset.GetDatasetPacket(AWriter: TDataPacketReader);
       else
         begin
         StoreUpdateBuffer(FUpdateBuffer[FCurrentUpdateBuffer], ARowState);
-        HandleUpdateBuffersFromRecord(False,ARecBookmark,ARowState);
+        HandleUpdateBuffersFromRecord(True,ARecBookmark,ARowState);
         end;
       end
   end;
@@ -3078,7 +3110,9 @@ begin
       begin
       RowState:=[];
       FCurrentIndex.StoreCurrentRecIntoBookmark(ABookmark);
-      HandleUpdateBuffersFromRecord(True,ABookmark^,RowState);
+      // updates related to current record are stored first
+      HandleUpdateBuffersFromRecord(False,ABookmark^,RowState);
+      // now store current record
       FFilterBuffer:=FCurrentIndex.CurrentBuffer;
       if RowState=[] then
         FDatasetReader.StoreRecord([])
@@ -3094,7 +3128,7 @@ begin
       end;
     // There could be an update buffer linked to the last (spare) record
     FCurrentIndex.StoreSpareRecIntoBookmark(ABookmark);
-    HandleUpdateBuffersFromRecord(True,ABookmark^,RowState);
+    HandleUpdateBuffersFromRecord(False,ABookmark^,RowState);
 
     RestoreState(SavedState);
 
@@ -3236,7 +3270,7 @@ var SavedState      : TDataSetState;
     AddRecordBuffer : boolean;
     ARowState       : TRowState;
     AUpdOrder       : integer;
-    x               : integer;
+    i               : integer;
 
 begin
   CheckBiDirectional;
@@ -3276,7 +3310,6 @@ begin
       inc(FBRecordCount);
 
       AddRecordBuffer:=False;
-
       end
     else if rsvDeleted in ARowState then
       begin
@@ -3297,9 +3330,9 @@ begin
       FIndexes[0].RemoveRecordFromIndex(FUpdateBuffer[FCurrentUpdateBuffer].BookmarkData);
       FIndexes[0].StoreSpareRecIntoBookmark(@FUpdateBuffer[FCurrentUpdateBuffer].NextBookmarkData);
 
-      for x := FCurrentUpdateBuffer+1 to length(FUpdateBuffer)-1 do
-        if FIndexes[0].SameBookmarks(@FUpdateBuffer[FCurrentUpdateBuffer].BookmarkData,@FUpdateBuffer[x].NextBookmarkData) then
-          FIndexes[0].StoreSpareRecIntoBookmark(@FUpdateBuffer[x].NextBookmarkData);
+      for i := FCurrentUpdateBuffer+1 to high(FUpdateBuffer) do
+        if FIndexes[0].SameBookmarks(@FUpdateBuffer[FCurrentUpdateBuffer].BookmarkData, @FUpdateBuffer[i].NextBookmarkData) then
+          FIndexes[0].StoreSpareRecIntoBookmark(@FUpdateBuffer[i].NextBookmarkData);
 
       AddRecordBuffer:=False;
       end
@@ -3320,6 +3353,7 @@ begin
         FCurrentUpdateBuffer:=AUpdOrder;
         FUpdateBuffer[FCurrentUpdateBuffer].UpdateKind:= ukInsert;
         FIndexes[0].StoreSpareRecIntoBookmark(@FUpdateBuffer[FCurrentUpdateBuffer].BookmarkData);
+        FUpdateBuffer[FCurrentUpdateBuffer].NextBookmarkData.BookmarkData:=nil;
         end;
 
       FIndexes[0].AddRecord;

+ 82 - 7
packages/fcl-db/tests/testdbbasics.pas

@@ -88,6 +88,7 @@ type
     procedure TestMultipleDeleteUpdateBuffer;
     procedure TestDoubleDelete;
     procedure TestMergeChangeLog;
+    procedure TestRevertRecord;
   // index tests
     procedure TestAddIndexInteger;
     procedure TestAddIndexSmallInt;
@@ -1231,6 +1232,7 @@ begin
     begin
     Open;
 
+    // modify records
     for i := 0 to 16 do
       begin
       if i mod 4=0 then
@@ -1242,19 +1244,21 @@ begin
       next;
       end;
 
-    for i := 17 to 20 do
+    // append new records
+    for i := 18 to 21 do
       begin
       append;
-      fieldbyname('id').AsInteger:=i+1;
-      fieldbyname('name').AsString:='TestName'+inttostr(i+1);
+      fieldbyname('id').AsInteger:=i;
+      fieldbyname('name').AsString:='TestName'+inttostr(i);
       post;
       end;
 
+    // delete records #1,5,9,13,17,21 which was modified or appended before
     first;
     for i := 0 to 20 do if i mod 4=0 then
       delete
     else
-       next;
+      next;
 
     First;
     i := 0;
@@ -1279,10 +1283,10 @@ begin
       CancelUpdates;
 
       First;
-      for i := 0 to 16 do
+      for i := 1 to 17 do
         begin
-        CheckEquals(i+1,FieldByName('ID').AsInteger);
-        CheckEquals('TestName'+inttostr(i+1),FieldByName('NAME').AsString);
+        CheckEquals(i, FieldByName('ID').AsInteger);
+        CheckEquals('TestName'+inttostr(i), FieldByName('NAME').AsString);
         next;
         end;
 
@@ -1785,6 +1789,77 @@ begin
     end;
 end;
 
+procedure TTestBufDatasetDBBasics.TestRevertRecord;
+begin
+  with DBConnector.GetNDataset(True,1) as TCustomBufDataset do
+  begin
+    Open;
+    // update value in one record and revert them
+    Edit;
+    FieldByName('ID').AsInteger := 100;
+    Post;
+    CheckEquals(100, FieldByName('ID').AsInteger);
+    RevertRecord;
+    CheckEquals(1, FieldByName('ID').AsInteger, 'Revert modified #1');
+    // append new record and delete prior and revert appended
+    AppendRecord([3,'']);
+    InsertRecord([2,'']);
+    Prior;
+    Delete; // 1st
+    Next;
+    RevertRecord; // 3rd
+    CheckEquals(2, FieldByName('ID').AsInteger, 'Revert inserted #1a');
+    RevertRecord; // 2nd
+    CheckTrue(Eof, 'Revert inserted #1b');
+    CancelUpdates; // restores 1st deleted record
+    CheckEquals(1, FieldByName('ID').AsInteger, 'CancelUpdates #1');
+    Close;
+  end;
+
+  with DBConnector.GetNDataset(False,0) as TCustomBufDataset do
+  begin
+    Open;
+    // insert one record and revert them
+    InsertRecord([1,'']);
+    RevertRecord;
+    CheckTrue(Eof);
+    CheckEquals(0, ChangeCount);
+
+    // insert two records and revert them in inverse order
+    AppendRecord([2,'']);
+    InsertRecord([1,'']); // this record in update-buffer is linked to 2
+    RevertRecord;
+    CheckEquals(2, FieldByName('ID').AsInteger);
+    CheckEquals(1, ChangeCount);
+    RevertRecord;
+    CheckTrue(Eof);
+    CheckEquals(0, ChangeCount);
+
+    // insert more records and some delete and some revert
+    AppendRecord([4,'']);
+    InsertRecord([3,'']);
+    InsertRecord([2,'']);
+    InsertRecord([1,'']);
+    CheckEquals(4, ChangeCount);
+    Delete;  // 1
+    CheckEquals(4, ChangeCount);
+    Next;    // 3
+    RevertRecord;
+    CheckEquals(4, FieldByName('ID').AsInteger);
+    CheckEquals(3, ChangeCount);
+    Prior;   // 2
+    RevertRecord;
+    CheckEquals(4, FieldByName('ID').AsInteger);
+    CheckEquals(2, ChangeCount);
+
+    CancelUpdates;
+    CheckTrue(Eof);
+    CheckEquals(0, ChangeCount);
+
+    Close;
+  end;
+end;
+
 procedure TTestBufDatasetDBBasics.FTestXMLDatasetDefinition(ADataset: TDataset);
 var i : integer;
 begin