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   879 use SDBM_File;
  1         1441  
  1         33  
46 1     1   4 use Fcntl;
  1         1  
  1         1741  
47              
48             sub fetch {
49 85     85 0 52 my $self = shift;
50 85         135 my $current = $self->{'current'}; # current pointer
51 85 100       117 return unless defined $current;
52 82         58 my $hash = $self->{'sdbmhash'};
53 82         445 my $value = $hash->{$current};
54              
55 82 50       150 if (not defined $value) {
56 0         0 delete $self->{'current'};
57 0         0 return;
58             }
59 82         245 my ($key, $num) = ($current =~ /^(.*):(\d+)$/s);
60 82         72 $num++;
61 82 50       392 if (defined $hash->{"$key:$num"}) { # next record for the same key
62 0         0 $self->{'current'} = "$key:$num";
63             } else {
64 82         304 my $newkey = $hash->{"$key:next"}; # next key
65 82 100       113 if (defined $newkey) {
66 79         96 $self->{'current'} = "$newkey:1";
67             } else {
68 3         6 delete $self->{'current'};
69             }
70             }
71 82         238 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  
85 0         0 keys %{$self->{'definition'}};
  0         0  
86             }
87              
88             sub prepare_select {
89 1     1 0 2 my $self = shift;
90 1         6 $self->{'current'} = $self->{'sdbmhash'}{':first'};
91 1 50       4 $self->{'current'} .= ':1' if defined $self->{'current'};
92 1         3 1;
93             }
94             sub prepare_select_eq {
95 2     2 0 3 my ($self, $eq, $recno) = @_;
96 2         2 delete $self->{'current'};
97 2         7 my $hash = $self->{'sdbmhash'};
98 2         3 my $start = $eq;
99 2         16 my $value = $hash->{"$start:1"};
100              
101 2 100       6 if (not defined $value) {
102             # not exact match
103 1         6 $start = $hash->{':first'};
104 1 50       4 if (not defined $start) {
105             # no records, jsut return
106 0         0 return 1;
107             }
108             # move throught the chain
109 1   66     5 while (defined $start and $start lt $eq) {
110 22         121 $start = $hash->{"$start:next"};
111             }
112 1 50       3 if (not defined $start) {
113 0         0 return 1;
114             }
115 1 50       2 if ($start gt $eq) {
116 1         2 $self->{'current'} = "$start:1";
117 1         2 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       3 if (not defined $recno) {
125             # if not requested exact match of the recno, return
126 1         4 $self->{'current'} = "$start:1";
127 1         2 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         1 my $tag = $opts{'tag'};
151            
152             # return immediatelly if the index file was already opened
153             return $dbf->{'sdbm_definition'}{'tags'}{$tag}
154             if defined $dbf->{'sdbm_definition'}
155 1 50 33     15 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 30 my ($class, $dbf, $tag, $expression) = @_;
203 1         2 my $dbffile = $dbf->{'filename'};
204 1         1 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         5 $file =~ s/\.dbf$/.sdbmd/i;
213              
214 1         1 $definitionhash = {};
215             # open or create the definition SDBM file
216 1 50       135 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         5 $dbf->{'sdbm_definition'} = { 'filename' => $file,
221             'definitionhash' => $definitionhash };
222             }
223              
224 1 50       19 if (defined $definitionhash->{"$tag:file"}) {
225 0         0 die "SDBM index `$tag' already exists for `$dbfffile'.";
226             }
227              
228 1         45 my $maxindexnumber = ++$definitionhash->{'tagnumber'};
229              
230 1         2 my $sdbmfile = $dbffile;
231 1         7 $sdbmfile =~ s/\.dbf$/.sdbm$maxindexnumber/i;
232              
233 1         2 my $sdbmhash = {};
234 1 50       74 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         6 my $self = bless { 'dbf' => $dbf, 'tag' => $tag,
239             'sdbmhash' => $sdbmhash,
240             'definition' => $definitionhash}, $class;
241 1         3 $dbf->{'sdbm_definition'}{'tags'}{$tag} = $self;
242 1         11 $definitionhash->{"$tag:file"} = $sdbmfile;
243              
244 1 50       6 if (defined $dbf->field_type(uc $expression)) {
245 1         1 $expression = uc $expression;
246             }
247 1 50       3 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         8 $definitionhash->{"$tag:expression"} = $expression;
252              
253 1         2 my $i = 0;
254 1         4 while ($i <= $dbf->last_record) {
255 42         68 my ($deleted, $value) = $dbf->get_record($i);
256 42 50       63 if (not $deleted) {
257 42         82 $self->insert($value, $i + 1);
258             }
259 42         88 $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 37 my ($self, $key, $value) = @_;
279             ### print "Adding $key $value\n";
280 42         41 my $hash = $self->{'sdbmhash'};
281 42         214 my $key_maxid = $hash->{"$key:0"};
282 42         48 $key_maxid++;
283              
284 42         426 $hash->{"$key:$key_maxid"} = $value;
285 42         305 $hash->{"$key:0"} = $key_maxid;
286 42 50       85 return 1 if $key_maxid > 1; # no need to change the chain
287            
288 42         29 my $prev = undef;
289 42         30 my $prev_next = ':first';
290 42         29 my $next;
291 42   100     237 while (defined($next = $hash->{$prev_next}) and $key gt $next) {
292 591         532 $prev = $next;
293 591         384 $prev_next = "$prev:next";
294 591         2834 $next = undef;
295             }
296              
297 42 100       71 if (not defined $next) {
298 10         59 $hash->{':last'} = $key; # we reached the last record
299             } else {
300 32         225 $hash->{"$key:next"} = $next;
301 32         210 $hash->{"$next:prev"} = $key;
302             }
303 42 100       67 if (not defined $prev) {
304 2         12 $hash->{':first'} = $key;
305             } else {
306 40         232 $hash->{"$prev:next"} = $key;
307 40         238 $hash->{"$key:prev"} = $prev;
308             }
309 42         68 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