summaryrefslogtreecommitdiff
path: root/fpcsrc/packages/fcl-db/tests/dbftoolsunit.pas
blob: 601068fa72506ee7718e8caa477499f56b9d7df7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
unit DBFToolsUnit;

{ Sets up dbf datasets for testing
Tests expect Get*Dataset to return a dataset with structure and test data, but closed.
}

{$IFDEF FPC}
  {$mode objfpc}{$H+}
{$ENDIF}

// If defined, save the dbf files when done and print out location to stdout:
{.$DEFINE KEEPDBFFILES}

interface

uses
  Classes, SysUtils, toolsunit,
  DB, Dbf, dbf_common;

type
  { TDBFDBConnector }

  TDBFDBConnector = class(TDBConnector)
  protected
    procedure CreateNDatasets; override;
    procedure CreateFieldDataset; override;
    procedure DropNDatasets; override;
    procedure DropFieldDataset; override;
    // InternalGetNDataset reroutes to ReallyInternalGetNDataset
    function InternalGetNDataset(n: integer): TDataset; override;
    function InternalGetFieldDataset: TDataSet; override;
    // GetNDataset allowing trace dataset if required;
    // if trace is on, use a TDbfTraceDataset instead of TDBFAutoClean
    function ReallyInternalGetNDataset(n: integer; Trace: boolean): TDataset;
  public
    function GetTraceDataset(AChange: boolean): TDataset; override;
  end;

  { TDBFAutoClean }
  // DBF descendant that saves to a memory stream instead of file
  TDBFAutoClean = class(TDBF)
  private
    FBackingStream: TMemoryStream;
    FIndexBackingStream: TMemoryStream;
    FMemoBackingStream: TMemoryStream;
    FCreatedBy: string;
  public
    // Keeps track of which function created the dataset, useful for troubleshooting
    property CreatedBy: string read FCreatedBy write FCreatedBy;
    constructor Create;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function UserRequestedTableLevel: integer;
  end;

  { TDbfTraceDataset }
  TDbfTraceDataset = class(TdbfAutoClean)
  protected
    procedure SetCurrentRecord(Index: longint); override;
    procedure RefreshInternalCalcFields(Buffer: PChar); override;
    procedure InternalInitFieldDefs; override;
    procedure CalculateFields(Buffer: PChar); override;
    procedure ClearCalcFields(Buffer: PChar); override;
  end;


implementation

uses
  FmtBCD;

function GetNewTempDBFName: string;
// Scans temp directory for dbf names and adds
var
  Res: TSearchRec;
  Path, Name: string;
  FileAttr: LongInt;
  Attr,NextFileNo: Integer;
begin
  NextFileNo:=0;
  Attr := faAnyFile;
  if FindFirst(IncludeTrailingPathDelimiter(GetTempDir)+'*.dbf', Attr, Res) = 0 then
  begin
    Path := GetTempDir;
    repeat
       Name := ConcatPaths([Path, Res.Name]);
       FileAttr := FileGetAttr(Name);
       if FileAttr and faDirectory = 0 then
       begin
         // Capture alphabetically latest name
         try
           //... only if it is numeric
           if strtoint(ChangeFileExt(Res.Name,''))>NextFileNo then
             NextFileNo:=strtoint(ChangeFileExt(Res.Name,''));
         except
           // apparently not numeric
         end;
       end
    until FindNext(Res) <> 0;
  end;
  FindClose(Res);
  // now we now the latest file, add 1, and paste the temp directory in front of it
  NextFileNo:=NextFileNo+1;
  Result:=IncludeTrailingPathDelimiter(GetTempDir)+IntToStr(NextFileNo)+'.DBF';
end;

{ TDBFAutoClean }

function TDBFAutoClean.UserRequestedTableLevel: integer;
  // User can specify table level as a connector param, e.g.:
  // connectorparams=4
  // If none given, default to DBase IV
var
  TableLevelProvided: integer;
begin
  TableLevelProvided := StrToIntDef(dbconnectorparams, 4);
  if not (TableLevelProvided in [3, 4, 5, 7, 
    TDBF_TABLELEVEL_FOXPRO, TDBF_TABLELEVEL_VISUALFOXPRO]) then
  begin
    Result := -1; // hope this crashes the tests so user is alerted.
    //Invalid tablelevel specified in connectorparams= field. Aborting
    exit;
  end;
  Result := TableLevelProvided;
end;

constructor TDBFAutoClean.Create;
begin
  // Create storage for data:
  FBackingStream:=TMemoryStream.Create;
  FIndexBackingStream:=TMemoryStream.Create;
  FMemoBackingStream:=TMemoryStream.Create;
  // Create a unique name (within the 10 character DBIII limit):
  TableName := FormatDateTime('hhnnssz',Now())+'_'+inttostr(random(99));
  TableLevel := UserRequestedTableLevel;
  Storage:=stoMemory;
  UserStream:=FBackingStream;
  UserIndexStream:=FIndexBackingStream;
  UserMemoStream:=FMemoBackingStream;
  CreateTable; //this will also write out the dbf header to disk/stream
end;

constructor TDBFAutoClean.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Self.Create;
end;

destructor TDBFAutoClean.Destroy;
{$IFDEF KEEPDBFFILES}
var
  FileName: string;
{$ENDIF}
begin
  {$IFDEF KEEPDBFFILES}
  Close;
  FileName := GetNewTempDBFName;
  FBackingStream.SaveToFile(FileName);
  FIndexBackingStream.SaveToFile(ChangeFileExt(FileName, '.mdx'));
  if Self.TableLevel in [TDBF_TABLELEVEL_FOXPRO, TDBF_TABLELEVEL_VISUALFOXPRO] then
    FMemoBackingStream.SaveToFile(ChangeFileExt(FileName, '.fpt'))
  else
    FMemoBackingStream.SaveToFile(ChangeFileExt(FileName, '.dbt'));
  writeln('TDBFAutoClean: file created by ',CreatedBy,' left file: ',FileName);
  {$ENDIF}
  inherited Destroy;
  FBackingStream.Free;
  FIndexBackingStream.Free;
end;


procedure TDBFDBConnector.CreateNDatasets;
begin
  // All datasets are created in InternalGet*Dataset
end;

procedure TDBFDBConnector.CreateFieldDataset;
begin
  // All datasets are created in InternalGet*Dataset
end;

procedure TDBFDBConnector.DropNDatasets;
begin
  // Nothing to be done here; the dataset is cleaned up in TDBFAutoClean.Destroy
end;

procedure TDBFDBConnector.DropFieldDataset;
begin
  // Nothing to be done here; the dataset is cleaned up in TDBFAutoClean.Destroy
end;

function TDBFDBConnector.InternalGetNDataset(n: integer): TDataset;
begin
  result:=ReallyInternalGetNDataset(n,false);
end;

function TDBFDBConnector.InternalGetFieldDataset: TDataSet;
var
  i: integer;
begin
  Result := (TDbfAutoClean.Create(nil) as TDataSet);
  with (Result as TDBFAutoClean) do
  begin
    CreatedBy:='InternalGetFieldDataset';
    FieldDefs.Add('ID', ftInteger);
    FieldDefs.Add('FSTRING', ftString, 10);
    FieldDefs.Add('FSMALLINT', ftSmallint);
    FieldDefs.Add('FINTEGER', ftInteger);
    FieldDefs.Add('FWORD', ftWord);
    FieldDefs.Add('FBOOLEAN', ftBoolean);
    FieldDefs.Add('FFLOAT', ftFloat);
    // Field types only available in (Visual) FoxPro
    if (Result as TDBF).TableLevel >= TDBF_TABLELEVEL_FOXPRO then
      FieldDefs.Add('FCURRENCY', ftCurrency);
    if (Result as TDBF).TableLevel >= TDBF_TABLELEVEL_FOXPRO then
      FieldDefs.Add('FBCD', ftBCD);
    FieldDefs.Add('FDATE', ftDate);
    FieldDefs.Add('FDATETIME', ftDateTime);
    FieldDefs.Add('FLARGEINT', ftLargeint);
    FieldDefs.Add('FMEMO', ftMemo);
    CreateTable;
    Open;
    for i := 0 to testValuesCount - 1 do
    begin
      Append;
      FieldByName('ID').AsInteger := i;
      FieldByName('FSTRING').AsString := testStringValues[i];
      FieldByName('FSMALLINT').AsInteger := testSmallIntValues[i];
      FieldByName('FINTEGER').AsInteger := testIntValues[i];
      FieldByName('FWORD').AsInteger := testWordValues[i];
      FieldByName('FBOOLEAN').AsBoolean := testBooleanValues[i];
      FieldByName('FFLOAT').AsFloat := testFloatValues[i];
      if (Result as TDBF).TableLevel >= TDBF_TABLELEVEL_FOXPRO then
        FieldByName('FCURRENCY').AsCurrency := testCurrencyValues[i];
      // work around missing TBCDField.AsBCD:
      if (Result as TDBF).TableLevel >= TDBF_TABLELEVEL_FOXPRO then
        FieldByName('FBCD').AsBCD := StrToBCD(testFmtBCDValues[i],Self.FormatSettings);
      FieldByName('FDATE').AsDateTime := StrToDate(testDateValues[i], 'yyyy/mm/dd', '-');
      FieldByName('FDATETIME').AsDateTime := StrToDateTime(testValues[ftDateTime,i], Self.FormatSettings);
      FieldByName('FLARGEINT').AsLargeInt := testLargeIntValues[i];
      FieldByName('FMEMO').AsString := testStringValues[i];
      Post;
    end;
    Close;
  end;
end;

function TDBFDBConnector.ReallyInternalGetNDataset(n: integer; Trace: boolean): TDataset;
var
  countID: integer;
begin
  if Trace then
    Result := (TDbfTraceDataset.Create(nil) as TDataSet)
  else
    Result := (TDBFAutoClean.Create(nil) as TDataSet);
  with (Result as TDBFAutoclean) do
  begin
    CreatedBy:='InternalGetNDataset('+inttostr(n)+')';
    FieldDefs.Add('ID', ftInteger);
    FieldDefs.Add('NAME', ftString, 50);
    CreateTable;
    Open;
    if n > 0 then
      for countId := 1 to n do
      begin
        Append;
        FieldByName('ID').AsInteger := countID;
        FieldByName('NAME').AsString := 'TestName' + IntToStr(countID);
        // Explicitly call .post, since there could be a bug which disturbs
        // the automatic call to post. (example: when TDataset.DataEvent doesn't
        // work properly)
        Post;
      end;
    if state = dsinsert then
      Post;
    Close;
  end;
end;

function TDBFDBConnector.GetTraceDataset(AChange: boolean): TDataset;
begin
  // Mimic TDBConnector.GetNDataset
  if AChange then FChangedDatasets[NForTraceDataset] := True;
  Result := ReallyInternalGetNDataset(NForTraceDataset,true);
  FUsedDatasets.Add(Result);
end;

{ TDbfTraceDataset }

procedure TDbfTraceDataset.SetCurrentRecord(Index: longint);
begin
  DataEvents := DataEvents + 'SetCurrentRecord' + ';';
  inherited SetCurrentRecord(Index);
end;

procedure TDbfTraceDataset.RefreshInternalCalcFields(Buffer: PChar);
begin
  DataEvents := DataEvents + 'RefreshInternalCalcFields' + ';';
  inherited RefreshInternalCalcFields(Buffer);
end;

procedure TDbfTraceDataset.InternalInitFieldDefs;
var
  i: integer;
  IntCalcFieldName: string;
begin
  // To fake an internal calculated field, set its fielddef InternalCalcField
  // property to true, before the dataset is opened.
  // This procedure takes care of setting the automatically created fielddef's
  // InternalCalcField property to true. (works for only one field)
  IntCalcFieldName := '';
  for i := 0 to FieldDefs.Count - 1 do
    if fielddefs[i].InternalCalcField then
      IntCalcFieldName := FieldDefs[i].Name;
  inherited InternalInitFieldDefs;
  if IntCalcFieldName <> '' then
    with FieldDefs.find(IntCalcFieldName) do
    begin
      InternalCalcField := True;
    end;
end;

procedure TDbfTraceDataset.CalculateFields(Buffer: PChar);
begin
  DataEvents := DataEvents + 'CalculateFields' + ';';
  inherited CalculateFields(Buffer);
end;

procedure TDbfTraceDataset.ClearCalcFields(Buffer: PChar);
begin
  DataEvents := DataEvents + 'ClearCalcFields' + ';';
  inherited ClearCalcFields(Buffer);
end;

initialization
  RegisterClass(TDBFDBConnector);
end.