unit DataFilUnit; {TP6, TP7+BP7} (* This is an EASY TO USE unit for data files with "homebrew" records. It is not compatible to ANY kind of database standards, but it is useful for smaller Pascal programs, which need some data handling on files with records. Data files consist of records, as defined in the Type declaration of the Pascal program. The record #0 is used internally, it contains only 1 longint. The remaining space in record #0 is filled with 00 bytes and can be used otherwise. The payload records are numbered from 1 to nnnnn? Important: Your record must have a longint at the first 4 bytes. This is 0 on valid records and non-0 on erased records. It is maintained by the methods automatically, you need not modify it. You can test it for a valid (non-erased) record. The type of the used record need not be "known" in this unit, so there is no problem with the units tree. The DataFileUnit can be invoked first or very early in the uses clause of the main program. The unit can be used for a multiple of different data files, if different instances of PDataFile are made. It is suggested that the close procedure is programmed in the data handling units with the ExitProc method, such that all files are closed properly even in case of a program crash. This makes it necessary that the PDataFile pointers are maintained in static DS: RAM, not in the TApplication. In most cases it is not necessary that the pointer is global, it can be declared below the implementation keyword in the data handling unit. Some procedures need an auxiliary record. This is not defined as a local variable, to avoid stack overflow with large records. But the programmer shall make sure that enough heap space is still available for the auxiliary record. The unit allows append and delete of records. The latest deleted record will be used first, and only if all deleted records are re-used, the filesize will be increased (appended). The flush procedure shall be called frequently. You can interrogate the FlushCnt variable, it displays how many records were appended since the last flush. It is allowed to flush following each AddRecord call. The unit has no provisions for indexing or sorting. This must be done with an index file (not recommended for simple applications) or better with a TSortedCollection in RAM. The methods are written as Functions with an integer result. It shall be 0 normally, else it displays the IOresult or -1 for Block-IO error. The critical variables are declared private, so they cannot be modified by an outside unit. There is no Constructor method, so you are responsible to call the Open or Create method immediately after you "New" the object. It is easy to use even for beginners, the usage of the object oriented approach is displayed in the example application below. The Filemode can be selected conforming to the rules of DOS, for Network access etc. But this simple object has no record locking mechanisms for concurrent access. Note: The records on file are NOT sorted. They are placed on file in a "random" manner. It is up to the program to maintain a sorted list of keys to manage the data access, or a btree index or another mechanism. In the appendix below you see an example of a TSortedCollection for this purpose. A TListBox can be made to display the key records on screen and get a selection bar. --> This unit is especially dedicated to Pascal beginners, who shall --> be forced to save their data professionally on file instead of --> holding all records in a memory array. NO GUARANTEE! The unit was extracted from a bigger unit, the code here is not fully tested for completeness. Franz Glaser *) interface uses DOS; type RecordBuffer = Record Case Boolean of false : (N : Longint; d8 : Array[4..11] of Byte; Sz : Word); {compatible with Turbo Access Toolbox TP4} true : (Rc : Array[0..$FFF0] of Byte); {dummy} End; PDataFile = ^TDataFile; TDataFile = Object {no virtual methods, static object} F : file; {could be declared in the private section!} IO,BlockIO : Integer; FlushCnt : Integer; {is incremented whenever new recs are appended} LastRecord : Longint; {convenience only, can be checked everywhere} FN : PathStr; {convenience only} Function Open(AFN:PathStr; ARecSiz:Word; OpenFM:Integer):Integer; Function Create(AFN:PathStr; ARecSiz:Word; OpenFM:Integer):Integer; Function Close : Integer; {must be called on program termination} Function AddRecord(Var B; Var Rec:Longint):Integer; Function DelRecord(Rec:Longint):Integer; Function PutRecord(Var B; Rec:Longint):Integer; Function GetRecord(Var B; Rec:Longint):Integer; Function UnErase(Var B; Var Rec : Longint):Integer; Function Flush:Integer; {shall be called after any nn addrecords} Function GetRecSize : Word; {to hide critical variables} Function GetLastErased : Longint; Function GetOpenMode : Integer; Private {variables can not be modified by other units} RecSize : Word; OpenMode : Integer; AuxRec : ^RecordBuffer; {never used outside} LastErased : Longint; end; Implementation {Important: The TDataFile is a static object. It does not use virtual methods. So it also has no .Init constructor. The programmer must call the .Open immediately after New(...) to establish all necessary setups to the variables. The Open method (and Create) perform the setups} Function TDataFile.Open(AFN:PathStr;ARecSiz:Word;OpenFM:Integer):Integer; Var OldFM : Integer; Begin RecSize := 0; Open := -1; if ARecSiz < 16 then Exit; FN := FExpand(AFN); {convenience data, for error msg etc.} Assign(F,FN); OpenMode := OpenFM; OldFM := FileMode; FileMode := OpenMode; {02H or 00H or 42H or 40H} {$I-} Reset(F,ARecSiz); {variable Recsize is not yet setup here} {$I+} FileMode := OldFM; IO := IOresult; Open := IO; if IO <> 0 then Exit; {if IO-error, then result of .Open > 0} GetMem(AuxRec,ARecSiz); {auxiliary record for internal use} BlockRead(F,AuxRec^,1,BlockIO); {record #0} if BlockIO <> 1 then Begin Open := -1; FreeMem(AuxRec,ARecSiz); Exit; End; LastErased := AuxRec^.N; {this was what I wanted to know} if AuxRec^.Sz <> ARecSiz then Begin Open := -2; FreeMem(AuxRec,ARecSiz); {the file was created} Exit; { with another recsize} End; if LastErased=0 then {can be -1 or 1...nn} Begin LastErased := -1; AuxRec^.N := LastErased; Seek(F,0); BlockWrite(F,AuxRec^,1,BlockIO); {or optionally exit, some error occurred yesterday!} End; FreeMem(AuxRec,ARecSiz); RecSize := ARecSiz; {mark as "open"} FlushCnt := 0; LastRecord := -1; End; {You shall not create a new file automatically whenever the original file was not found. First ask the user, probably he/she has a backup copy...} Function TDataFile.Create(AFN:PathStr;ARecSiz:Word;OpenFM:Integer):Integer; Var OldFM : Integer; Begin RecSize := 0; Create := -1; if ARecSiz < 4 then Exit; FN := FExpand(AFN); Assign(F,FN); {$I-} Rewrite(F,ARecSiz); {$I+} IO := IOresult; Create := IO; if IO <> 0 then Exit; GetMem(AuxRec,ARecSiz); Fillchar(AuxRec^,ARecSiz,#0); AuxRec^.N := -1; AuxRec^.Sz:= ARecSiz; BlockWrite(F,AuxRec^,1,BlockIO); FreeMem(AuxRec,ARecSiz); {$I-} System.Close(F); {on many OS the filemode does not work properly} {$I+} {on new files. So it must be closed and re-opened} IO := IOresult; Create := IO; if IO <> 0 then Exit; if BlockIO <> 1 then Begin Create := -1; Exit; End; OpenMode := OpenFM; OldFM := FileMode; FileMode := OpenMode; {$I-} Reset(F,ARecSiz); {$I+} FileMode := OldFM; IO := IOresult; Create := IO; if IO <> 0 then Exit; RecSize := ARecSiz; FlushCnt := 0; LastErased := -1; LastRecord := -1; End; {the close procedure does not write to the file, so it does not need an AuxRec to be obtained from the heap. This makes it especially useful for the ExitProc approach} Function TDataFile.Close : Integer; Begin Close := -1; if RecSize = 0 then Exit; {$I-} System.Close(F); {$I+} IO := IOresult; Close := IO; RecSize := 0; {mark as "not open"} End; {Add a new record to the file. The procedure decides where to write it and returns the record number used} Function TDataFile.AddRecord(Var B;Var Rec:Longint):Integer; {note: Rec is a result value here} Var Buf : RecordBuffer absolute B; {"dirty" but powerful...} Begin AddRecord := -1; {preset as error} if RecSize = 0 then Exit; if (OpenMode and 2) = 0 then Exit; {opened in ReadOnly mode} if LastErased = -1 then Begin {no erased record available} Rec := FileSize(F); {append} Seek(F,Rec); Buf.N := 0; {mark as "not erased" record} BlockWrite(F,B,1,BlockIO); if BlockIO = 1 then AddRecord := 0; {else -1 from above} Inc(FlushCnt); {convenience counter} LastRecord := Rec; End else Begin {use latest erased record} AddRecord := 0; Rec := LastErased; if (Rec < 1) or (Rec > FileSize(F)) then Begin {obviously corrupted data area in the object} AddRecord := -1; Exit; End; Seek(F,Rec); {read the erased record to obtain the old link} GetMem(AuxRec,RecSize); {auxiliary data record} BlockRead(F,AuxRec^,1,BlockIO); LastErased := AuxRec^.N; {update link list} {$I-} Seek(F,Rec); {$I+} IO := IOresult; AddRecord := IO; if IO <> 0 then Begin FreeMem(AuxRec,RecSize); Exit; End; Buf.N := 0; {mark as valid again} BlockWrite(F,B,1,BlockIO); {write the payload data to the erased rec} if BlockIO <> 1 then Begin AddRecord := -1; FreeMem(AuxRec,RecSize); Exit; End; Seek(F,0); {should not need the ioresult stuff here} BlockRead(F,AuxRec^,1,BlockIO); AuxRec^.N := LastErased; {now update rec#0 with new link} Seek(F,0); BlockWrite(F,AuxRec^,1,BlockIO); {put into root} FreeMem(AuxRec,RecSize); LastRecord := Rec; End; End; {marks a record as deleted and re-establishes the chain of deleted records} Function TDataFile.DelRecord(Rec:Longint):Integer; Begin DelRecord := -1; if RecSize = 0 then Exit; if (OpenMode and 2) = 0 then Exit; {opened in ReadOnly mode} {$I-} Seek(F,Rec); {$I+} IO := IOresult; DelRecord := IO; if IO <> 0 then Exit; GetMem(AuxRec,RecSize); BlockRead(F,AuxRec^,1,BlockIO); if (BlockIO <> 1) or (AuxRec^.N <> 0) then {do not double erase} Begin DelRecord := -1; FreeMem(AuxRec,RecSize); Exit; End; AuxRec^.N := LastErased; Seek(F,Rec); BlockWrite(F,AuxRec^,1,BlockIO); {$I-} Seek(F,0); {$I+} IO := IOresult; DelRecord := IO; if IO <> 0 then Begin FreeMem(AuxRec,RecSize); Exit; End; BlockRead(F,AuxRec^,1,BlockIO); if BlockIO <> 1 then Begin DelRecord := -1; FreeMem(AuxRec,RecSize); Exit; End; LastErased := Rec; AuxRec^.N := Rec; Seek(F,0); BlockWrite(F,AuxRec^,1,BlockIO); FreeMem(AuxRec,RecSize); LastRecord := Rec; End; {in most cases used to write a modified record back to disk} Function TDataFile.PutRecord(Var B;Rec:Longint):Integer; Var Buf : RecordBuffer absolute B; Begin PutRecord := -1; if RecSize = 0 then Exit; if (OpenMode and 2) = 0 then Exit; {opened in ReadOnly mode} if (Rec < 1) or (Rec >= FileSize(F)) then Exit; {no ADD with PUT!} Buf.N := 0; {make sure: "not erased"} {$I-} Seek(F,Rec); {$I+} IO := IOresult; PutRecord := IO; if IO <> 0 then Exit; BlockWrite(F,B,1,BlockIO); if BlockIO <> 1 then PutRecord := -1; LastRecord := Rec; End; {GetRecord can be used to read erased records too} Function TDataFile.GetRecord(Var B;Rec:Longint):Integer; Begin GetRecord := -1; if RecSize = 0 then Exit; if (Rec < 1) or (Rec >= FileSize(F)) then Exit; {$I-} Seek(F,Rec); {$I+} IO := IOresult; GetRecord := IO; if IO <> 0 then Exit; BlockRead(F,B,1,BlockIO); if BlockIO <> 1 then GetRecord := -1; LastRecord := Rec; End; {auxiliary procedure to restore/UnErase the latest erased record} Function TDataFile.UnErase(Var B; Var Rec : Longint):Integer; {Rec is a result from the UnErase procedure} Var Buf : RecordBuffer absolute B; Begin {calling program shall first ask the GetLastErased function if there is a restorable record. if -1 then no erased record available} UnErase := -1; Rec := LastErased; if Rec < 1 then Exit; {nothing to restore} if RecSize = 0 then Exit; {already closed} if (OpenMode and 2) = 0 then Exit; {opened in ReadOnly mode} if Rec >= FileSize(F) then Exit; {must be an error} {$I-} Seek(F,Rec); {$I+} IO := IOresult; UnErase := IO; if IO <> 0 then Exit; BlockRead(F,B,1,BlockIO); if BlockIO <> 1 then Begin UnErase := -1; Exit; End; LastErased := Buf.N; {get chain} Buf.N := 0; Seek(F,Rec); BlockWrite(F,B,1,BlockIO); {write "unerased" record to file} {now update the link in record #0:} Seek(F,0); GetMem(AuxRec,RecSize); BlockRead(F,AuxRec^,1,BlockIO); AuxRec^.N := LastErased; {update the chain} Seek(F,0); BlockWrite(F,AuxRec^,1,BlockIO); FreeMem(AuxRec,RecSize); LastRecord := Rec; End; Function TDataFile.Flush:Integer; {used to update DIR frequently} Var Hand : Word; Begin Flush := -1; if RecSize = 0 then Exit; if (OpenMode and 2) = 0 then Exit; {no flush necessary on r/o files} Hand := FileRec(F).Handle; ASM MOV BX,Hand MOV AX,$6800 {there are no more DOS versions < 3.0 running...} PUSH DS PUSH BP INT $21 POP BP POP DS JC @@1 MOV CX,0 @@1: MOV Hand,CX {Result, auxiliary} End; FlushCnt := 0; Flush := Hand; End; {note: MSDOS < 3 had no such system call "commit file", needed close and reopen or get a duplicate handle to close} {the following functions are a means to read the critical variables in the application units without the danger of corruption} Function TDataFile.GetRecSize : Word; Begin GetRecSize := RecSize; End; Function TDataFile.GetLastErased : Longint; Begin GetLastErased := LastErased; End; Function TDataFile.GetOpenMode : Integer; Begin GetOpenMode := OpenMode; End; End. {of unit} --------------------------------------------------------------- { Application example for demo only, not compileable! } Uses Objects,Views,Dialogs,MsgBox,App {...} ; Type TPersRec = Record Status : Longint; {THIS IS NECESSARY!!!} FstName: String[47]; {the following items are free} LstName: String[47]; Street : String[31]; PoCode : String[7]; City : String[31]; End; PKeyRec = ^TKeyRec; TKeyRec = Record Name : String[35]; Rec : Longint; {40 bytes total, shall be mult of 8} End; {The TSortedCollection object is never used immediately. It is a building block for your customized sorted collection} PPersKeyCollection = ^TPersKeyCollection; TPersKeyCollection = Object(TSortedCollection) Constructor Init; {the following 2 functions MUST be declared on any customized object:} function Compare(Key1, Key2: Pointer): Integer; virtual; procedure FreeItem(Item: Pointer); virtual; End; Var MyFile : PDataFile; Pers : TPersRec; Rc : Longint; I : Integer; Key : PKeyRec; KeyCollection : PPersKeyCollection; Constructor TPersKeyCollection.Init; Begin TSortedCollection.Init(64,32); {start with 64, increase by chunks of 32} {suggestion: Init with the filesize of the datafile + some spare} End; function Compare(Key1, Key2: Pointer): Integer; Begin if PKeyRec(Key1)^.Name > PKeyRec(Key2)^.Name then Compare := 1 else Compare := -1; {this allows multiple occurrencies of the same key} End; procedure FreeItem(Item: Pointer); Begin FreeMem(Item,sizeof(TKeyRec)); End; {. main ....} Begin MyFile := New(PDataFile); with MyFile^ do Begin Case Open('PERSON.DAT',sizeof(TPersRec),$02) of -2 : Begin Writeln('Record size does not fit: ',FN); Halt(3); End; -1 : Begin Writeln('Error opening/reading file: ',FN); Halt(3); End; 0 : ; 2 : if MessageBox('Data file'^M^J+FN+^M^J'not found. Create new?', Nil,mfError or mfYesNoCancel,hcMsgFileNotFound) = cmYes then Begin if Create('PERSON.DAT',sizeof(TPersRec),$02) <> 0 then Halt(3); End else Halt(4); else Begin Writeln('could not open the person-file ',FN); Writeln('Error #:',IO); Halt(3); End; KeyCollection := New(PPersKeyCollection,Init); if FileSize(F) > 1 then for I := 1 to FileSize(F)-1 do Begin if GetRecord(Pers,I)=0 then if Pers.Status=0 then Begin Key := New(PKeyRec); Key^.Name := UpcaseStr(Pers.LstName); {upcasestr not shown here} Key^.Rec := I; KeyCollection^.Insert(Key); End; End; ... if KeyCollection^.Count > 0 then .... End; {with} {note: of course you can save your tsortedcollection to disk as a stream. this avoids the necessity to read in the whole data file every morning. the collection stream then behalves similar to an index file} ... if MyFile^.GetRecord(Pers,PKeyRec(KeyCollection^.At(Item))^.Rec) = 0 then {note: Item is often derived from a MyListBox.Focused} ... {edit Pers record in a dialog window} if MyFile^.PutRecord(Pers,PKeyRec(KeyCollection^.At(Item))^.Rec) = 0 then ... {write Pers back to file. Caution: do not allow the user to modify the key strings, else it gets more complicated: delete old key in the collection and insert new key} {new record: first edit a new, empty record, then add it to the file} fillchar(Pers,sizeof(Pers),#0); if EditPers(Pers) = cmOK then if MyFile^.AddRecord(Pers,Rc) = 0 then Begin Key := New(PKeyRec); Key^.Name := UpcaseStr(Pers.FstName); {very primitive key generation} Key^.Rec := Rc; {result from AddRecord} KeyCollection^.Insert(Key); {will sort it in: TSortedCollection} {MyListBox^.SetRange(KeyCollection^.Count); MyListBox^.FocusItem(KeyCollection^.InsertedAt); MyListBox^.DrawView; } if MyFile^.FlushCnt > 4 then MyFile^.Flush; End; ... deleting: {Again: Item is usually some MyListBox.Focused} Key := KeyCollection^.At(Item); if MyFile^.DelRecord(Key^.Rec) = 0 then Begin KeyCollection^.AtDelete(Item); {MyListBox^.SetRange(KeyCollection^.Count); MyListBox^.DrawView; } End; finally: if MyFile^.Close <> 0 then; ... Dispose(MyFile); --------------------------------------------------------- You can use the remaining bytes of record #0 for whatever you like. --------------------------------------------------------- This version of DataFilUnit is compatible with the Turbo Database Access Toolkit which was available for Turbo Pascal 4 many years ago. The Database toolkit also had btree index files, but I do not use it, I prefer the TSortedCollection. The compatibility is somewhat restricted, but you can use datafiles from a toolbox (TP4 only, not TP3) program with this unit. --------------------------------------------------------- Franz Glaser http://members.eunet.at/meg-glaser http://www.geocities.com/SiliconValley/2926/tp.html meg-glaser@eunet.at