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.
|