File Coverage

blib/lib/XBase/SDBM.pm
Criterion Covered Total %
statement 97 199 48.7
branch 26 76 34.2
condition 6 12 50.0
subroutine 8 15 53.3
pod 0 13 0.0
total 137 315 43.4


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             XBase::SDBM - SDBM index support for dbf
5              
6             =head1 DESCRIPTION
7              
8             When developing the XBase.pm/DBD::XBase module, I was trying to
9             support as many existing variants of file formats as possible. The
10             module thus accepts wide range of dbf files and their versions from
11             various producers. But with index files, the task is much, much
12             harder. First, there is little or no documentation of index files
13             formats, so the development is based on reverse engineering.
14              
15             None if the index formats support is finalized. That made it hard to
16             integrate them into one consistent API. That is why I decided to write
17             my own index support, and as I wanted to avoid inventing yet another
18             way of storing records in pages and similar things, I used SDBM. It
19             comes with Perl, so you already have it, and it's proven and it
20             works.
21              
22             Now, SDBM is a module that aims at other task than to do supporting
23             indexes for a dbf. But equality tests are fast with it and I have
24             creted a structure in each index file to enable "walk" though the
25             index file.
26              
27             =head1 VERSION
28              
29             1.02
30              
31             =head1 AVAILABLE FROM
32              
33             http://www.adelton.com/perl/DBD-XBase/
34              
35             =head1 AUTHOR
36              
37             (c) 2001--2011 Jan Pazdziora.
38              
39             All rights reserved. This package is free software; you can
40             redistribute it and/or modify it under the same terms as Perl itself.
41              
42             =cut
43              
44             package XBase::SDBM;
45 1     1   1606 use SDBM_File;
  1         4489  
  1         50  
46 1     1   9 use Fcntl;
  1         3  
  1         3681  
47              
48             sub fetch {
49 85     85 0 151 my $self = shift;
50 85         384 my $current = $self->{'current'}; # current pointer
51 85 100       186 return unless defined $current;
52 82         390 my $hash = $self->{'sdbmhash'};
53 82         1082 my $value = $hash->{$current};
54              
55 82 50       227 if (not defined $value) {
56 0         0 delete $self->{'current'};
57 0         0 return;
58             }
59 82         2164 my ($key, $num) = ($current =~ /^(.*):(\d+)$/s);
60 82         115 $num++;
61 82 50       972 if (defined $hash->{"$key:$num"}) { # next record for the same key
62 0         0 $self->{'current'} = "$key:$num";
63             } else {
64 82         860 my $newkey = $hash->{"$key:next"}; # next key
65 82 100       196 if (defined $newkey) {
66 79         191 $self->{'current'} = "$newkey:1";
67             } else {
68 3         13 delete $self->{'current'};
69             }
70             }
71 82         468 return ($key, $value);
72             }
73             sub fetch_current {
74 0     0 0 0 my $self = shift;
75 0         0 my $current = $self->{'current'};
76 0 0       0 return unless defined $current;
77 0         0 my $value = $self->{'sdbmhash'}{$current};
78 0 0       0 return unless defined $value;
79 0         0 my ($key) = ($current =~ /^(.*):\d+$/s);
80 0         0 return ($key, $value);
81             }
82             sub tags {
83 0     0 0 0 my $self = shift;
84 0 0       0 return map { if (s/:file$//) { ( $_ ) } else { () } }
  0         0  
  0         0  
  0         0  
85 0         0 keys %{$self->{'definition'}};
86             }
87              
88             sub prepare_select {
89 1     1 0 2 my $self = shift;
90 1         8 $self->{'current'} = $self->{'sdbmhash'}{':first'};
91 1 50       6 $self->{'current'} .= ':1' if defined $self->{'current'};
92 1         5 1;
93             }
94             sub prepare_select_eq {
95 2     2 0 6 my ($self, $eq, $recno) = @_;
96 2         5 delete $self->{'current'};
97 2         14 my $hash = $self->{'sdbmhash'};
98 2         4 my $start = $eq;
99 2         29 my $value = $hash->{"$start:1"};
100              
101 2 100       10 if (not defined $value) {
102             # not exact match
103 1         12 $start = $hash->{':first'};
104 1 50       6 if (not defined $start) {
105             # no records, jsut return
106 0         0 return 1;
107             }
108             # move throught the chain
109 1   66     9 while (defined $start and $start lt $eq) {
110 22         271 $start = $hash->{"$start:next"};
111             }
112 1 50       4 if (not defined $start) {
113 0         0 return 1;
114             }
115 1 50       5 if ($start gt $eq) {
116 1         16 $self->{'current'} = "$start:1";
117 1         5 return 1;
118             }
119             # we shouldn't have never got here, but nevermind
120 0         0 $value = $hash->{"$start:1"};
121             }
122              
123             # here we've found exact match of the key
124 1 50       5 if (not defined $recno) {
125             # if not requested exact match of the recno, return
126 1         4 $self->{'current'} = "$start:1";
127 1         4 return 1;
128             }
129              
130 0         0 my $num = 1;
131 0   0     0 while (defined $value and $value != $recno) {
132 0         0 $num++;
133 0         0 $value = $hash->{"$start:$num"};
134             }
135              
136 0 0       0 if (defined $value) {
137 0         0 $self->{'current'} = "$start:$num";
138             } else {
139 0         0 $start = $hash->{"$start:next"};
140 0 0       0 $self->{'current'} = "$start:1" if defined $start;
141             }
142 0         0 1;
143             }
144              
145              
146             # method new (open) will open the named SDBM index for given dbf
147             sub new {
148 1     1 0 5 my ($class, $filename, %opts) = @_;
149 1         2 my $dbf = $opts{'dbf'};
150 1         2 my $tag = $opts{'tag'};
151            
152             # return immediatelly if the index file was already opened
153 1 50 33     21 return $dbf->{'sdbm_definition'}{'tags'}{$tag}
154             if defined $dbf->{'sdbm_definition'}
155             and defined $dbf->{'sdbm_definition'}{'tags'}{$tag};
156              
157 0         0 my $dbffile = $dbf->{'filename'};
158 0         0 my $file = $dbffile;
159 0         0 $file =~ s/\.dbf$/.sdbmd/i;
160              
161             # some of the SDBM indexes were already touched
162             # the definitionhash is a SDBM that lists the content of the
163             # actual SDBM index files
164 0         0 my $definitionhash = {};
165 0 0       0 if (defined $dbf->{'sdbm_definition'}) {
166 0         0 $definitionhash = $dbf->{'sdbm_definition'}{'definitionhash'};
167             }
168             else {
169             # if it wasn't opened yet, open the definition file
170 0 0       0 if (not tie(%$definitionhash, 'SDBM_File',
171             $file, O_RDWR, 0666)) {
172 0         0 die "SDBM index definition file `$file' not found for `$dbffile': $!.";
173             }
174 0         0 $dbf->{'sdbm_definition'} = { 'filename' => $file,
175             'definitionhash' => $definitionhash };
176             }
177              
178             # check the definition file for tag requested
179 0         0 my $sdbmfile = $definitionhash->{"$tag:file"};
180 0 0       0 if (not defined $sdbmfile) {
181             # no such SDBM index exists, the definition SDBM says
182 0         0 die "SDBM index `$tag' not known for `$dbffile'.";
183             }
184              
185             # open the SDBM index file
186 0         0 my $sdbmhash = {};
187 0 0       0 unless (tie(%$sdbmhash, 'SDBM_File', $sdbmfile, O_RDWR, 0666)) {
188 0         0 die "SDBM index file `$sdbmfile' not found for `$dbffile': $!.";
189             }
190              
191 0         0 my $self = bless { 'dbf' => $dbf,
192             'tag' => $tag, 'sdbmhash' => $sdbmhash,
193             'definition' => $definitionhash }, $class;
194 0         0 $dbf->{'sdbm_definition'}{'tags'}{$tag} = $self;
195 0         0 return $self;
196             }
197             *open = \&new;
198              
199             # method create will create SDBM index with given name and expression
200             # for the dbf table
201             sub create {
202 1     1 0 271 my ($class, $dbf, $tag, $expression) = @_;
203 1         3 my $dbffile = $dbf->{'filename'};
204 1         2 my $file;
205              
206             my $definitionhash;
207 1 50       3 if (defined $dbf->{'sdbm_definition'}) {
208             # the definition SDBM was already opened
209 0         0 $definitionhash = $dbf->{'sdbm_definition'}{'definitionhash'};
210             } else {
211 1         2 $file = $dbffile;
212 1         7 $file =~ s/\.dbf$/.sdbmd/i;
213              
214 1         2 $definitionhash = {};
215             # open or create the definition SDBM file
216 1 50       155 if (not tie(%$definitionhash, 'SDBM_File',
217             $file, O_RDWR|O_CREAT, 0666)) {
218 0         0 die "SDBM index definition file `$file' not found/created for `$dbffile': $!.";
219             }
220 1         12 $dbf->{'sdbm_definition'} = { 'filename' => $file,
221             'definitionhash' => $definitionhash };
222             }
223              
224 1 50       28 if (defined $definitionhash->{"$tag:file"}) {
225 0         0 die "SDBM index `$tag' already exists for `$dbfffile'.";
226             }
227              
228 1         54 my $maxindexnumber = ++$definitionhash->{'tagnumber'};
229              
230 1         3 my $sdbmfile = $dbffile;
231 1         8 $sdbmfile =~ s/\.dbf$/.sdbm$maxindexnumber/i;
232              
233 1         2 my $sdbmhash = {};
234 1 50       105 if (not tie(%$sdbmhash, 'SDBM_File', $sdbmfile, O_CREAT|O_EXCL|O_RDWR, 0666)) {
235 0         0 die "SDBM index file `$sdbmfile' couldn't be created for `$dbffile': $!."
236             }
237              
238 1         15 my $self = bless { 'dbf' => $dbf, 'tag' => $tag,
239             'sdbmhash' => $sdbmhash,
240             'definition' => $definitionhash}, $class;
241 1         4 $dbf->{'sdbm_definition'}{'tags'}{$tag} = $self;
242 1         16 $definitionhash->{"$tag:file"} = $sdbmfile;
243              
244 1 50       8 if (defined $dbf->field_type(uc $expression)) {
245 1         2 $expression = uc $expression;
246             }
247 1 50       5 if (not defined $dbf->field_type($expression)) {
248 0         0 $self->drop;
249 0         0 die "SDBM index `$expression' couldn't be created for `$dbffile': no such column name.";
250             }
251 1         16 $definitionhash->{"$tag:expression"} = $expression;
252              
253 1         4 my $i = 0;
254 1         5 while ($i <= $dbf->last_record) {
255 42         114 my ($deleted, $value) = $dbf->get_record($i);
256 42 50       87 if (not $deleted) {
257 42         93 $self->insert($value, $i + 1);
258             }
259 42         132 $i++;
260             }
261              
262 1         7 return $self;
263             }
264              
265             # method drop will drop the SDBM index
266             sub drop {
267 0     0 0 0 my ($self) = @_;
268 0         0 my $tag = $self->{'tag'};
269 0         0 my $definitionhash = $self->{'definition'};
270 0         0 my $sdbmfile = $definitionhash->{"$tag:file"};
271 0         0 delete $definitionhash->{"$tag:file"};
272 0         0 delete $definitionhash->{"$tag:definition"};
273 0         0 delete $self->{'dbf'}{'sdbm_definition'}{'tags'}{$tag};
274 0         0 unlink "$sdbmfile.pag", "$sdbmfile.dir";
275             }
276              
277             sub insert {
278 42     42 0 63 my ($self, $key, $value) = @_;
279             ### print "Adding $key $value\n";
280 42         58 my $hash = $self->{'sdbmhash'};
281 42         288 my $key_maxid = $hash->{"$key:0"};
282 42         75 $key_maxid++;
283              
284 42         850 $hash->{"$key:$key_maxid"} = $value;
285 42         652 $hash->{"$key:0"} = $key_maxid;
286 42 50       105 return 1 if $key_maxid > 1; # no need to change the chain
287            
288 42         48 my $prev = undef;
289 42         46 my $prev_next = ':first';
290 42         37 my $next;
291 42   100     442 while (defined($next = $hash->{$prev_next}) and $key gt $next) {
292 591         943 $prev = $next;
293 591         632 $prev_next = "$prev:next";
294 591         6284 $next = undef;
295             }
296              
297 42 100       96 if (not defined $next) {
298 10         129 $hash->{':last'} = $key; # we reached the last record
299             } else {
300 32         537 $hash->{"$key:next"} = $next;
301 32         481 $hash->{"$next:prev"} = $key;
302             }
303 42 100       89 if (not defined $prev) {
304 2         26 $hash->{':first'} = $key;
305             } else {
306 40         562 $hash->{"$prev:next"} = $key;
307 40         596 $hash->{"$key:prev"} = $prev;
308             }
309 42         110 return 1;
310             }
311              
312             sub delete {
313 0     0 0   my ($self, $key, $value) = @_;
314             ### print "Deleting $key $value\n";
315 0           my $hash = $self->{'sdbmhash'};
316 0           my $key_maxid = $hash->{"$key:0"};
317              
318 0           my $number = 1;
319 0           while ($number <= $key_maxid) {
320 0 0         if ($hash->{"$key:$number"} == $value) {
321 0           last;
322             }
323 0           $number++;
324             }
325 0 0         if ($number > $key_maxid) {
326             # such a record was not found
327 0           return 0;
328             }
329              
330 0 0         if ($key_maxid > 1) {
331 0 0         $hash->{"$key:$number"} = $hash->{"$key:$key_maxid"}
332             if $number != $key_maxid;
333 0           delete $hash->{"$key:$key_maxid"};
334 0           $hash->{"$key:0"} = $key_maxid - 1;
335             } else {
336 0           my $next = $hash->{"$key:next"};
337 0           my $prev = $hash->{"$key:prev"};
338 0 0         if (defined $next) {
339 0 0         if (not defined $prev) {
340 0           $hash->{':first'} = $next;
341 0           delete $hash->{"$next:prev"};
342             } else {
343 0           $hash->{"$prev:next"} = $next;
344 0           $hash->{"$next:prev"} = $prev;
345 0           delete $hash->{"$key:prev"};
346             }
347 0           delete $hash->{"$key:next"};
348             } else {
349 0 0         if (not defined $prev) {
350 0           delete $hash->{':first'};
351 0           delete $hash->{':last'};
352             } else {
353 0           $hash->{':last'} = $prev;
354 0           delete $hash->{"$prev:next"};
355 0           delete $hash->{"$key:prev"};
356             }
357             }
358 0           delete $hash->{"$key:0"};
359 0           delete $hash->{"$key:1"};
360             }
361 0           return 1;
362             }
363             sub delete_current {
364 0     0 0   my $self = shift;
365 0           my ($key, $value) = $self->fetch_current;
366 0 0         if (defined $value) {
367 0           $self->delete($key, $value);
368             }
369             }
370             sub insert_before_current {
371 0     0 0   die "SDBM index doesn't support backward rolling yet.\n";
372             }
373              
374             sub dump {
375 0     0 0   my $self = shift;
376 0           my $hash = $self->{'sdbmhash'};
377              
378 0           for (sort keys %$hash) {
379 0           print "$_ $hash->{$_}\n";
380             }
381             }
382              
383             1;
384