Repository navigation
Expand file tree
/
Copy pathuFile.pas
More file actions
executable file
·1835 lines (1677 loc) · 69.6 KB
/
Copy pathuFile.pas
File metadata and controls
executable file
·1835 lines (1677 loc) · 69.6 KB
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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
unit uFile;
{ ThinkSQL Relational Database Management System
Copyright © 2000-2012 Greg Gaughan
See LICENCE.txt for details
}
{Generic DB-file management routines
Just has a file page directory and no page structure
(although Trec is specified for Add/Read abstract routines)
and virtual methods for reading and adding records and for scanning the file
Note:
the dbserver.buffer is accessed via the passed transaction - so it had better be stable
- else we could pin using one buffer and try to unpin using another (can't see how this could happen)
Note:
pages chained in a heap file must match the order of the pages in the file directory
the dirSlot references in the file directory are continous and are split into pages
in the directory routines.
To speed up some of the routines, the dirSlot page is often passed (dirPageId) as well:
this avoids the need to scan long directory chains for large files, but does
assume that the directory slots and pages are stable.
(May have been better using RID? Could still calculate prevSlot:=slot-1?)
}
//{$DEFINE DEBUGDETAIL}
{$DEFINE DEBUG_CHECKDIR}
{$DEFINE SAFETY} //extra sense checks, e.g. page=0
interface
uses uPage, uGlobal, uStmt, uGlobalDef, IdTCPConnection{debug only};
const
TFileDirSize=sizeof(pageId)+sizeof(word) +2; //Note: +2 because record seems to round up
FileDirPerBlock=BlockSize div TFileDirSize;
InvalidDirSlot=-1;
type
TfileDir=record //note: see TFileDirSize above - keep in sync!!
pid:PageId;
Space:word; //(limits blockSize to 65535)
end; {TfileDir}
recType=(rtHeader, {Special slot header}
rtEmpty, {Slot no longer used - can be purged if at end of page slot array}
rtDeletedRecord, {Deleted header - retain - deletion could be rolled-back => same as normal rtRecord}
rtRecord, {Record header}
rtDelta, {Record delta}
rtReservedSlot, {Note: this is for internal page shuffling only - it should never be seen on disk!}
rtBlob {Blob data - reference by rtRecord or rtDelta}
); //todo check/force size is byte? = default (or use $MINENUMSIZE 1)
//todo keep in sync with recTypeText strings below
{(These limit the max blockSize to 65535)} //note: ensure algorithms don't try to go past the limit
RecId=word; //in-page pointer
RecSize=word; //in-page record length
PtrRecData=^TrecData;
TrecData=array [0..MaxRecSize-1] of char; //record data (used for pointer typing and AddRecord tests)
//note similarity between TSlot and Trec! -same except Trec has a swizzled pointer to memory
//maybe could use Tslot.start(longint) as memory pointer? saving what?
Trec=class //Record block pointer within a (pinned) page (overlay)
private
public
rType:recType; //record type
dataPtr:PtrRecData; //data start
len:integer; //length
Wt:StampId; //Transaction W timestamp
prevRID:Trid; //pointer to previous record in db
end; {Trec}
TDBFile=class //database heapfile //todo abstract?
protected
fstartPage:PageId; //first page of page directory
fname:string; //the db filename
//todo move these to the transaction level & have scan routines pass the latest value to us
// that way multiple users can share the same file (esp. db.sysTable etc.) but can
// scan independently.
// So, make sure we make this thread-safe: esp. access to properties/open/new routines etc.
//This then allows them to share heapfile & so Trelation
// - again make sure these are thread safe:
// - ->we need to move relation.fTuple to the transaction also!
// but then iterator trees would collapse!
// we really need a new ftuple per tran using the relation
// but then there's little point sharing the relation!
// Think/design...
// only need to share ftuple (and file.currentRid) for db.sysTableRel etc.
// and these are only used for quick lookup access (don't serialise!)
// and occasionaly insertions/updates (could be serialised)
// So, each tran really needs a currentRid + currentTuple for each sysRel
// to allow concurrent scanning/reading (no sharing 1 tuple for all sysRel, since may be fast-joining etc)
// -probably will be around: col,table,auth,schema,dom,constr,dom-con,col-con,fk,idx= 10
// system relations = extra ~10k per transaction = not bad
// alternative is to keep as they are, via db = bottleneck
// but bottle neck only when
// opening/creating relations (then in tuple def)
// building constraint trees for insert/update (will be done once at start)
// so maybe hardly ever any contention????? so extra complexity not worth it?
// -maybe in future could add another thread/set of sysRels to handle extra requests in parallel
// - i.e. 1 or 2 sets of sysRels should handle it?
// We should replace hCatalogMutex with hsysTableCriticalSection, hsysColumnCriticalSection etc.
// and this means all system catalog functions must be made atomic -via db.routines? - ok?
// - may need sysCatalog.lock; sysCatalog.findNextColumn; ...; finally sysCatalog.unlock
// plus, these routines make it easier to have an extra catalog server thread later...
// plus they hide the details!
// but maybe for speed each tran should have its own sysTableRel,sysColumnRel?
// (etc. -use array of rels, e.g. tr.sysCatalog[sysTable].startscan)
// then rel.open is very fast with no bottlenecks
// - other stuff like reading constraints/permissions
// can be left to db routines (serialised, but maybe less so later...)
// use tr.sysCatalog[] array for all sys relations
// - for those we can serialise for now, point the rel at db.sysRel instead of creating & opening it!
// - i.e. looks independent but is shared resource - who serialises access to it?
// - maybe use Trelation.serialiseAccess
// and all scanstart's rel.lock which will either do nothing or grab critical section
// and all scanstop's rel.unlock -prone to error? -not very neat? waste CS per relation
// maybe db.FindSysDomain() can be used for now
// or all use shared db.FindNextCol(tr,rel)
// & if passed non-db rel, use else serialise access...
// e.g. tr: db.FindNextCol(tr,tr.sysCatalog[sysColumn])
// tr: db.FindNextDomain(tr,db.sysDomain) -> serialised cos rel.owner=db
// tr: db.FindFirstCol(tr,tr.sysCatalog[sysColumn],'schema1','table1')
// but then may as well get findFirstCol to use tr.sysCatalog[sysColumn]
// and findFirstDomain to use db.sysDomain - save params
// -still isolated for user & can be changed later by adding to
// tr.sysCatalog array & tr.start (to open sysRels) & server routines
// plus catalog access code is in db routines where it belongs (not in tuple/rel routines)
// plus allows us to serialise/centralise issuing of new id's etc.
//
fCurrentRID:Trid; //scan pointer (fCurrentRID.pid will be pinned during scan)
fCurrentPage:TPage; //scan page
fDirPageFindStartPage:PageId; //first page of page directory that we already found a space on
fDirPageFindDirPageOffset:integer; //page offset from fStartPage for fDirPageFindStartPage
fDirPageFindSpace:word; //last space sought
public
property startPage:PageId read fStartPage;
property name:string read fname; //only currently used for index name debug messages, so remove
property currentRID:Trid read fCurrentRID; //need to reference from tuple for deletions
constructor Create; virtual;
destructor Destroy; override;
function createFile(st:Tstmt;const fname:string):integer; virtual;
function deleteFile(st:Tstmt):integer; virtual;
function openFile(st:Tstmt;const filename:string;startPage:PageId):integer; virtual;
function freeSpace(st:Tstmt;page:TPage):integer; virtual;
//TODO change to ReadRecord(rid:Trid,r:Trec):integer to match AddRecord etc.!
// also move to HeapFile? i.e. make virtual;abstract - don't assume slot structure at this level? maybe?
function ReadRecord(st:Tstmt;page:TPage;sid:SlotId;r:Trec):integer; virtual; abstract;
function AddRecord(st:Tstmt;r:Trec;var rid:Trid):integer; virtual; abstract;
{File directory is used for:
tracking pages allocated to this file (and ones that should be deallocated)
tracking free space within pages for new insertions
providing a contiguous array of page references [0..DirCount-1] for hash file mapping
- maybe expose a higher level than these?
}
function DirCount(st:Tstmt;var dirSlotCount:integer;var lastDirPage,prevLastSlotDirPage:PageId;retryForAccuracy:boolean):integer;
function DirPage(st:Tstmt;DirPageId:PageId;dirSlot:DirSlotId;var pid:PageId;var space:word):integer;
function DirPageSet(st:Tstmt;DirPageId:PageId;dirSlot:DirSlotId;pid:PageId;space:word;AllowOverwrite:boolean):integer;
function DirPageFind(st:Tstmt;space:word;var pid:PageId;var DirPageId:PageId;var dirSlot:DirSlotId):integer;
function DirPageFindFromPID(st:Tstmt;pid:PageId;var DirPageId:PageId;var dirSlot:DirSlotId):integer;
function DirPageAdd(st:Tstmt;pid:PageId;space:word;var prevDirSlotPageId,DirPageId:PageId;var dirSlot:DirSlotId):integer;
function DirPageRemove(st:Tstmt;pid:PageId;dirSlot:DirSlotId):integer;
function DirPageAdjustSpace(st:Tstmt;DirPageId:PageId;dirSlot:DirSlotId;spaceAdj:integer;var space:word):integer;
function GetScanStart(st:Tstmt;var rid:Trid):integer; virtual;
function GetScanNext(st:Tstmt;var rid:Trid;var noMore:boolean):integer; virtual;
function GetScanStop(st:Tstmt):integer; virtual;
function debugDump(st:Tstmt;connection:TIdTCPConnection;summary:boolean):integer; virtual;
end; {TDBFile}
const
recTypeText:array [rtHeader..rtBlob] of string = ('header',
'empty',
'deletedRecord',
'record',
'delta',
'reserved',
'blob'
);
var
debugFileCreate:integer=0;
debugFileDestroy:integer=0;
implementation
uses
{$IFDEF Debug_Log}
uLog,
{$ENDIF}
sysUtils, uServer, uTransaction, uOS {for sleep}
,uEvsHelpers
;
const
where='uFile';
who='';
{Retry attempts}
RETRY_DIRPAGEADD=50;
RETRY_DIRCOUNT=50; //Note: this could be doubled by retry in DirPageAdd
dirCountEmptyBackoffMin=5; //min. milliseconds (plus random) //todo make proportional to CPU speed/active threads
dirCountEmptyBackoffExtra=50; //max. milliseconds (random) //todo make proportional to CPU speed/active threads
constructor TDBFile.Create;
begin
inc(debugFileCreate);
{$IFDEF DEBUG_LOG}
if debugFileCreate=1 then
log.add(who,where,format(' File memory size=%d',[instanceSize]),vDebugLow);
{$ENDIF}
fStartPage:=InvalidPageId;
fCurrentRID.pid:=InvalidPageId;
fCurrentRID.sid:=InvalidSlotId;
end;
destructor TDBFile.Destroy;
const routine=':destroy';
begin
if fCurrentRID.pid<>InvalidPageId then
begin
{$IFDEF DEBUG_LOG}
log.add(who,where+routine,format('Scan of %s starting at %d is in progress - page %d will be left pinned',[fname,fStartPage,fCurrentRID.pid]),vDebugError);
{$ELSE}
;
{$ENDIF}
end;
inc(debugFileDestroy);
inherited;
end;
function TDBFile.createFile(st:Tstmt;const fname:string):integer;
{Creates a file in the database
IN : st the statement (connected to a db)
: fname the new filename
RETURN : +ve=ok, else fail
}
const routine=':createFile';
var
page:Tpage;
fileDir:TFileDir;
i:DirSlotId;
needDirEntry:boolean;
begin
//todo assert db<>nil
result:=Fail;
needDirEntry:=(fname=sysTable_file); //only need db-dir entry for sysTable
if Ttransaction(st.owner).db.addFile(st,fname,fstartPage,needDirEntry)<>ok then
result:=Fail
else
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('File %s created starting at page %d',[fname,fStartPage]),vDebug);
{$ENDIF}
{Reset DirPageFind cache}
fDirPageFindStartPage:=InvalidPageId;
fDirPageFindDirPageOffset:=0;
fDirPageFindSpace:=MAX_WORD;
{Create the directory page}
//note: would be neater to do this when needed i.e. use DirPageAdd() chicken&egg
with Ttransaction(st.owner).db.owner as TDBserver do
begin
if buffer.pinPage(st,fStartPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Failed pinning header page',vDebugError);
{$ENDIF}
result:=Fail;
exit;
end;
try
if page.latch(st)=ok then //note: no real need since page is local?
begin
try
page.block.pageType:=ptFileDir; //overkill, ptFiledir is the default
//speed: page.block.prevPage:=page.block.thisPage; //make linked list a circle for fast adding later
{Write zeroised page pointers}
fileDir.pid:=InvalidPageId;
fileDir.space:=0;
for i:=0 to FileDirPerBlock-1 do
begin
page.SetBlock(st,i*sizeof(fileDir),sizeof(fileDir),@fileDir);
end;
page.dirty:=True;
finally
page.unlatch(st);
end; {try}
//30/08/00 moved:page.dirty:=True;
end
else
begin
result:=fail;
exit;
end;
finally
buffer.unpinPage(st,fStartPage);
end; {try}
end; {with}
result:=ok;
end;
end; {createFile}
function TDBFile.deleteFile(st:TStmt):integer;
{Deletes the file from the database
IN : st the statement (connected to a db)
RETURN : +ve=ok, else fail
Assumes:
file has been opened
Obviously you should not try to use the file after this routine!
}
const routine=':deleteFile';
var
page,startPage:Tpage;
pid:PageId;
fileDir:TFileDir;
i:DirSlotId;
needDirEntry:boolean;
//newpid:PageId;
prevPid:PageId;
//newPage:TPage;
prevPage:TPage;
lastDirPage:PageId;
dirSlot:DirSlotId;
begin
//todo assert db<>nil
result:=Fail;
with Ttransaction(st.owner).db.owner as TDBserver do
begin
{Move forwards through chain & check the directory pages are clear}
pid:=fStartPage;
if buffer.pinPage(st,pid,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Failed pinning header page',vDebugError);
{$ENDIF}
exit;
end;
lastDirPage:=pid;
try
{Check each page in the file directory chain is clear: we can guarantee at least 1 file directory page}
while pid<>InvalidPageId do
begin
{Check that this page is ok to delete}
if page.block.pageType<>ptFileDir then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('File directory page (%d) is not the expected type (%d)',[pid,ord(page.block.pageType)]),vAssertion);
{$ENDIF}
exit; //abort
end;
(*
if page.block.nextPage<>InvalidPageId then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Initial file directory page has a forward pointer (%d)',[page.block.nextPage]),vAssertion);
{$ENDIF}
exit; //abort
end;
*)
for i:=0 to FileDirPerBlock-1 do
begin
page.AsBlock(st,i*sizeof(fileDir),sizeof(fileDir),@fileDir);
if (FileDir.pid<>InvalidPageId) or (fileDir.space<>0) then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('File file directory page (%d) has an allocted slot (%d) [%d:%d]',[pid,i,FileDir.pid,fileDir.space]),vAssertion);
{$ENDIF}
exit; //abort
end;
end;
//next in chain
pid:=page.block.nextPage;
if pid<>InvalidPageId then
begin
buffer.unpinPage(st,page.block.thisPage);
if buffer.pinPage(st,pid,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Failed pinning page',vDebugError);
{$ENDIF}
exit;
end;
lastDirPage:=pid;
end;
//speed: else assert fStartPage.block.prevPage=page.block.thisPage? (i.e. the end is what we expected)
end;
finally
buffer.unpinPage(st,page.block.thisPage);
end; {try}
{Remove the dir pages (except the first one), starting with the last one moving backwards through the chain}
(*speed:
{First pin the start page so we can keep its end-page pointer up to date}
if buffer.pinPage(st,fStartPage,startPage)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Failed pinning header page',vDebugError);
{$ENDIF}
exit;
end;
startPage.latch(st); //keep latched, which also locks others out
try
*)
if buffer.pinPage(st,lastDirPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Failed pinning last directory page',vDebugError);
{$ENDIF}
exit;
end;
try
while lastDirPage<>fStartPage do
begin
{De-initialise the file directory page}
{Unlink from previous dir page}
if lastDirPage=fStartPage then
begin
prevPid:=InvalidPageId; //this is the very first directory page
(*speed:
startPage.block.prevPage:=fStartPage; //point startPage to itself (keep list circle intact)
startPage.dirty:=True;
*)
end
else
begin
prevPid:=page.block.prevPage;
if buffer.pinPage(st,prevpid,prevpage)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed reading dir page''s previous page, %d',[prevpid]),vError);
{$ENDIF}
exit; //abort
end;
prevPage.latch(st);
try
(*speed:
startPage.block.prevPage:=prevpid; //point startPage to this new end page (keep list circle intact)
startPage.dirty:=True;
*)
prevPage.block.nextPage:=InvalidPageId; //sever link forwards
prevPage.dirty:=True;
finally
prevPage.Unlatch(st);
buffer.unpinPage(st,prevpid);
end; {try}
end;
//move to previous in chain
lastDirPage:=prevPid;
if lastDirPage<>InvalidPageId then
begin
buffer.unpinPage(st,page.block.thisPage);
{Remove this directory page}
//Note: these pages do not appear in the file directory itself, so no need to remove //double-check!
if Ttransaction(st.owner).db.deallocatePage(st,page.block.thisPage)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Failed deallocating file directory page',vError);
{$ENDIF}
exit; //abort
end;
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Contracted file page directory by page %d',[page.block.thisPage]),vDebug);
{$ENDIF}
if buffer.pinPage(st,lastDirPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Failed pinning page',vDebugError);
{$ENDIF}
exit;
end;
end;
end;
finally
buffer.unpinPage(st,page.block.thisPage);
end; {try}
(*speed:
finally
startPage.Unlatch(st);
buffer.unpinPage(st,startPage.block.thisPage);
end; {try}
*)
{note: removing the first directory page is done by db.removeFile}
end; {with}
needDirEntry:=(fname=sysTable_file); //only need db-dir entry for sysTable
if Ttransaction(st.owner).db.removeFile(st,fname,fstartPage,needDirEntry)<>ok then
result:=Fail
else
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('File %s deleted starting at page %d',[fname,fStartPage]),vDebug);
{$ENDIF}
result:=ok;
end;
end; {deleteFile}
function TDBFile.openFile(st:TStmt;const filename:string;startPage:PageId):integer;
{Opens a file in the specified database
i.e. goes to the file's page directory header page
IN : db the database
: filename the existing filename
: startPage the start page for this file (found by caller from catalog)
RETURN : +ve=ok, else fail
Side effects:
sets fStartPage for this file
sets fname for this file
Assumes:
filename and startpage are valid
}
const routine=':openFile';
var
page:TPage;
begin
result:=Fail;
//todo assert db<>nil
// assert file exists?
fname:=filename;
{Get the directory page}
with Ttransaction(st.owner).db.owner as TDBserver do
begin
if buffer.pinPage(st,StartPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed reading start page %d of %s',[fStartPage,filename]),vDebugError);
{$ENDIF}
exit; //abort
end;
try
fstartPage:=startPage;
{$IFDEF DEBUGDETAIL}
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('File %s opened starting at page %d',[filename,fStartPage]),vDebug);
{$ENDIF}
{$ENDIF}
//todo sanity checks?
{Reset DirPageFind cache}
fDirPageFindStartPage:=InvalidPageId;
fDirPageFindDirPageOffset:=0;
fDirPageFindSpace:=MAX_WORD;
result:=ok;
finally
buffer.unpinPage(st,StartPage); //todo leave pinned until ScanNext
end; {try}
end; {with}
end; {openFile}
function TDBFile.freeSpace(st:TStmt;page:TPage):integer;
{Returns amount of free record space in the specified page
IN : page the page to examine
RETURN : the amount of free space
}
const routine=':freeSpace';
begin
result:=BlockSize;
end; {FreeSpace}
function TDBFile.DirCount(st:TStmt;var dirSlotCount:integer;var lastDirPage,prevLastSlotDirPage:PageId;retryForAccuracy:boolean):integer;
{Returns count of dir slots allocated to this file
IN : retryForAccuracy True = retries if fails due to other insertions
else until
OUT : dirSlotCount the number of dir slots (pages) used
: lastDirPage the page id of the last dir page
: prevLastSlotDirPage the page id of the dir page of the last slot-1 (InvalidPageId=no previous slot)
(used for chaining later on)
//Note: this is always = lastDirPage!!! todo so remove!
RETURNS : +ve=ok, else fail
Note: used for adding new pages: dirSlotCount = next free dir slot
Note: if retryForAccuracy then it retries if necessary to count the slots in the file's directory
and latches the final page while it counts the entries in an attempt to stabilise multiple inserts
(but could fail if RETRY_DIRCOUNT is too small)
}
const routine=':DirCount';
var
page:TPage;
fileDir:TfileDir;
dirPageOffset:integer;
i:DirSlotId;
retry:integer;
begin
result:=Fail;
if fStartPage=InvalidPageId then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Uninitialised start page',vDebugError);
{$ENDIF}
exit;
end;
with Ttransaction(st.owner).db.owner as TDBserver do
begin
retry:=-1; //i.e. don't retry at all, unless certain circumstance
while (result<>ok) and ((retry>0) or (retry=-1)) do //outer loop to retry in case we find our last dir page has a next page when we've read it
begin
if retry>0 then
begin
dec(retry); //i.e. once retry fired, no more to avoid infinite loop
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Retrying (%d more tries to go)',[retry]),vDebugMedium);
{$ENDIF}
end;
dirPageOffset:=0;
{Move to last page}
lastDirPage:=fStartPage;
if buffer.pinPage(st,lastDirPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed reading next first dir page %d',[lastDirPage]),vDebugError);
{$ENDIF}
if retry=-1 then retry:=1; //todo any point?
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Retry set to %d',[retry]),vDebugLow);
{$ENDIF}
//exit;
continue; //abort/retry
end;
try
{speed: jump straight to last page rather than reading whole chain but:
currently we need to count the dir pages to get the total slot count
todo: get dirPageSet to take slot relative to page & then we can just count slots on the last page here
lastDirPage:=page.block.prevPage; //use startPage's prevpage to jump straight to end (circular list)
}
while page.block.nextPage<>InvalidPageId do
begin
lastDirPage:=page.block.nextPage;
buffer.unpinPage(st,page.block.thisPage{was lastDirPage & was before lastDirPage was reset});
if buffer.pinPage(st,lastDirPage,page)<>ok then //note: use outer finally to unpin
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed reading next dir page %d',[lastDirPage]),vDebugError);
{$ENDIF}
if retry=-1 then retry:=1; //todo any point?
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Retry set to %d',[retry]),vDebugLow);
{$ENDIF}
//exit; //abort - note will cause fail on finally, unpin
continue; //abort/retry
end;
inc(dirPageOffset);
end; {while}
{Initial count based on number of full pages read
(race note: the last page may just have been added by another thread but the new slot not yet set.
If so, we don't want to return because the caller will think we still need a new page
based on the slot count but we know we don't. So if the count on the final page = 0, we retry
- effectively waiting for the new slot to be filled //todo need backoff delay? not much...
//what if it never comes? retry will timeout...)
}
dirSlotCount:=dirPageOffset*FileDirPerBlock;
{Now count the entries in this last page}
i:=0;
{We latch here in an attempt to make more stable (reduce retries) for multi-threaded adding}
//todo check this doesn't cause it to be slower...
if retryForAccuracy then
page.latch(st);
try
//todo check if page.block.nextPage<>InvalidPageId=>retry now to avoid delay? speed
page.AsBlock(st,i*sizeof(fileDir),sizeof(fileDir),@fileDir);
while filedir.pid<>InvalidPageId do
begin
inc(dirSlotCount);
inc(i);
if i=FileDirPerBlock then
begin //reached last entry in dir page - read next?
if page.block.nextPage<>InvalidPageId then
begin
{Note: this could be because another thread is racing away adding entries...so we retry if caller needs accuracy}
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Last dir page (%d) has a next pointer, but the initial bulk count reported none',[lastDirPage]),vDebugWarning); //i.e. race bug, e.g. during DirPageAdd! = disaster? but we retry...
{$ENDIF}
if retry=-1 then
if retryForAccuracy then
retry:=RETRY_DIRCOUNT
else
retry:=1; //todo any point? see code when i=0 below: surely this is accurate enough //speed
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Retry set to %d',[retry]),vDebugLow);
{$ENDIF}
sleepOS(dirCountEmptyBackoffMin+random(dirCountEmptyBackoffExtra));
//exit; //abort
break; //abort/retry
end;
//this last one is full, end loop
fileDir.pid:=InvalidPageId;
end;
{Look at next slot}
if fileDir.pid<>InvalidPageId then
page.AsBlock(st,i*sizeof(fileDir),sizeof(fileDir),@fileDir);
end; {while}
finally
if retryForAccuracy then
page.unlatch(st);
end; {try}
if filedir.pid<>InvalidPageId then continue; //must have broken out in error so jump to retry
if (dirPageOffset>0) and (i=0) then
begin //last page is empty (& not just an empty file) - if we return now we could be causing caller to add another dir page unecessarily
if retryForAccuracy then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Last dir page (%d) is empty, so we retry to wait for initial entry to avoid double adding',[lastDirPage]),vDebugError);
{$ENDIF}
//speed? I suppose we could return with i=1 so the caller tries to use this new page immediately
// seems like a better option but we can't be sure that slot will be free for us!
// - we could just retry this last page loop count (but once we can jump to the last dir page, no speed benefit)
if retry=-1 then
retry:=RETRY_DIRCOUNT;
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Retry set to %d',[retry]),vDebugLow);
{$ENDIF}
//exit; //abort - note will cause fail on finally, unpin
continue; //abort/retry
end;
//else don't retry, we have the right count probably -1
end;
prevLastSlotDirPage:=lastDirPage; //all other slots' previous slots are on this page
//since we return the last slot page & the count
// - so it's up the the caller who adds a new dir page to
// set a different prev dir page
result:=ok;
finally
buffer.unpinPage(st,lastDirPage);
end; {try}
end; {retry}
if (result<>ok) and (retry=0) then
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed after retries',[nil]),vAssertion);
{$ELSE}
;
{$ENDIF}
end; {with}
end; {DirCount}
function TDBFile.DirPage(st:TStmt;DirPageId:PageId;dirSlot:DirSlotId;var pid:PageId;var space:word):integer;
{Returns details of specified dir slot allocated to this file
IN : dirPageId the dir slot's page reference (InvalidPageId=not known, use slot reference only)
(only used to shortcut the dir chain if we already know the dir page)
: dirSlot the dir slot's reference
OUT : pid the page id
(NOTE: if InvalidPageId then not allocated=fail & caller to handle
- except dirSlot=0 = empty file
)
: space the amount of free space on the page
RETURNS : +ve=ok, else fail
}
const routine=':DirPage';
var
page:TPage;
fileDir:TfileDir;
dirPageOffset:integer; //page jump count
dirPage:PageId;
nextDirPage:PageId;
begin
result:=Fail;
if fStartPage=InvalidPageId then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Uninitialised start page',vDebugError);
{$ENDIF}
exit;
end;
//SAFETY: check dirSlot>=0!
{Find the appropriate dir page}
dirPageOffset:=dirSlot div FileDirPerBlock; //page offset
nextDirPage:=fStartPage;
with Ttransaction(st.owner).db.owner as TDBserver do
begin
if DirPageId=InvalidPageId then
begin
{Find the appropriate dir page}
while dirPageOffset>0 do
begin
dirPage:=nextDirPage;
if buffer.pinPage(st,dirPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed reading dir page %d',[dirPage]),vDebugError);
{$ENDIF}
exit; //abort
end;
try
if page.block.nextPage=InvalidPageId then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Missing next dir page from %d to read dir slot %d',[dirPage,dirSlot]),vAssertion);
{$ENDIF}
exit; //abort
end;
nextDirPage:=page.block.nextPage;
dec(dirPageOffset);
finally
buffer.unpinPage(st,dirPage);
end; {try}
end; {while}
end
else
begin //dir page reference was passed by caller, so we can jump straight to it
nextDirPage:=DirPageId;
end;
{Ok, now get the dir slot}
if buffer.pinPage(st,nextdirPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed reading dir page %d',[nextdirPage]),vDebugError);
{$ENDIF}
exit; //abort
end;
try
page.AsBlock(st,(dirSlot mod FileDirPerBlock)*sizeof(fileDir),sizeof(fileDir),@fileDir);
pid:=fileDir.pid; //if = InvalidPageId then read past end - caller to deal with this
space:=fileDir.Space;
{$IFDEF SAFETY}
if (dirSlot<>0) and (pid=InvalidPageId) then //i.e. if initial dirSlot (0) could just be an empty file with no datapages yet
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Invalid page (%d) (space=%d) read from dir page %d (slot=%d)',[pid,space,nextdirPage,dirSlot]),vAssertion);
{$ENDIF}
exit; //abort
end;
{$ENDIF}
result:=ok;
finally
buffer.unpinPage(st,nextdirPage);
end; {try}
end; {with}
end; {DirPage}
function TDBFile.DirPageSet(st:TStmt;DirPageId:PageId;dirSlot:DirSlotId;pid:PageId;space:word;AllowOverwrite:boolean):integer;
{Sets details of specified dir slot allocated to this file
IN : dirPageId the dir slot's page reference (InvalidPageId=not known, use slot reference only)
(only used to shortcut the dir chain if we already know the dir page)
: dirSlot the dir slot's reference
: pid the page id
: space the amount of free space on the page
: AllowOverwrite True=ignore if pageId differs from existing one
False=abort if pageId differs
RETURNS : +ve=ok,
-2 = slot was occupied by another pageId, e.g. add race lost
else fail
Note: it is up to the caller to retry (or whatever) if this fails (especially when AllowOverwrite=False = normal case),
e.g. dirPageAdd tries to set the 1st free dir slot but it could be
beaten to it by another thread so it should retry
Assumes: if dirPageId is passed, we assume the page is stable with respect to the dirSlot, i.e. within the dir chain
(so future directory page insertion/deletion algorithms could cause problems)
}
const routine=':DirPageSet';
var
page:TPage;
fileDir:TfileDir;
{$IFDEF DEBUG_CHECKDIR}
existingFileDir:TfileDir;
{$ENDIF}
dirPageOffset:integer; //page jump count
dirPage:PageId;
nextDirPage:PageId;
begin
result:=Fail;
if fStartPage=InvalidPageId then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Uninitialised start page',vDebugError);
{$ENDIF}
exit;
end;
with Ttransaction(st.owner).db.owner as TDBserver do
begin
if DirPageId=InvalidPageId then
begin
{Find the appropriate dir page}
dirPageOffset:=dirSlot div FileDirPerBlock; //page offset
dirPage:=fStartPage;
nextDirPage:=fStartPage;
while dirPageOffset>0 do
begin
dirPage:=nextDirPage;
if buffer.pinPage(st,dirPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed reading dir page %d',[dirPage]),vDebugError);
{$ENDIF}
exit; //abort
end;
try
if page.block.nextPage=InvalidPageId then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Missing next dir page from %d to read dir slot %d',[dirPage,dirSlot]),vDebugError);
{$ENDIF}
exit; //abort
end;
nextDirPage:=page.block.nextPage;
dec(dirPageOffset);
finally
buffer.unpinPage(st,dirPage);
end; {try}
end; {while}
end
else
begin //dir page reference was passed by caller, so we can jump straight to it
nextDirPage:=DirPageId;
end;
{Ok, now set the dir slot}
if buffer.pinPage(st,nextdirPage,page)<>ok then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Failed reading dir page %d',[nextdirPage]),vDebugError);
{$ENDIF}
exit; //abort
end;
try
if pid=InvalidPageId then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,'Cannot add an invalid page id to the page directory',vAssertion);
{$ENDIF}
exit;
end;
fileDir.pid:=pid;
fileDir.Space:=space;
if page.latch(st)=ok then
begin
try
//Keep live to avoid race! {$IFDEF DEBUG_CHECKDIR}
if not AllowOverwrite then
begin
{Check that the page we're setting is still ours, i.e. when adding new ones check no-one else has grabbed our slot}
page.AsBlock(st,(dirSlot mod FileDirPerBlock)*sizeof(existingFileDir),sizeof(existingFileDir),@existingFileDir);
if (existingFileDir.pid<>InvalidPageId) and (existingFileDir.pid<>fileDir.pid) then
begin
{$IFDEF DEBUG_LOG}
log.add(st.who,where+routine,format('Warning: trying to set an existing page id (%d) in the page directory to a new one (%d) - rejecting',[existingFileDir.pid,fileDir.pid]),vDebugError); //i.e. race bug, e.g. during DirPageAdd! = disaster? but we retry...
{$ENDIF}
result:=-2;
exit; //abort! - up to caller to retry
end;
end;
//else assume caller knows what they're doing!
//{$ENDIF}
page.SetBlock(st,(dirSlot mod FileDirPerBlock)*sizeof(fileDir),sizeof(fileDir),@fileDir);
page.dirty:=True;
finally
page.unlatch(st);
end; {try}
//30/08/00 moved:page.dirty:=True;