Commit | Line | Data |
ffed8b01 |
1 | package DBM::Deep; |
2 | |
3 | ## |
4 | # DBM::Deep |
5 | # |
6 | # Description: |
d0b74c17 |
7 | # Multi-level database module for storing hash trees, arrays and simple |
8 | # key/value pairs into FTP-able, cross-platform binary database files. |
ffed8b01 |
9 | # |
d0b74c17 |
10 | # Type `perldoc DBM::Deep` for complete documentation. |
ffed8b01 |
11 | # |
12 | # Usage Examples: |
d0b74c17 |
13 | # my %db; |
14 | # tie %db, 'DBM::Deep', 'my_database.db'; # standard tie() method |
ffed8b01 |
15 | # |
d0b74c17 |
16 | # my $db = new DBM::Deep( 'my_database.db' ); # preferred OO method |
17 | # |
18 | # $db->{my_scalar} = 'hello world'; |
19 | # $db->{my_hash} = { larry => 'genius', hashes => 'fast' }; |
20 | # $db->{my_array} = [ 1, 2, 3, time() ]; |
21 | # $db->{my_complex} = [ 'hello', { perl => 'rules' }, 42, 99 ]; |
22 | # push @{$db->{my_array}}, 'another value'; |
23 | # my @key_list = keys %{$db->{my_hash}}; |
24 | # print "This module " . $db->{my_complex}->[1]->{perl} . "!\n"; |
ffed8b01 |
25 | # |
26 | # Copyright: |
d0b74c17 |
27 | # (c) 2002-2006 Joseph Huckaby. All Rights Reserved. |
28 | # This program is free software; you can redistribute it and/or |
29 | # modify it under the same terms as Perl itself. |
ffed8b01 |
30 | ## |
31 | |
460b1067 |
32 | use 5.6.0; |
33 | |
ffed8b01 |
34 | use strict; |
460b1067 |
35 | use warnings; |
8b957036 |
36 | |
d8db2929 |
37 | our $VERSION = q(0.99_03); |
86867f3a |
38 | |
9a63e1f2 |
39 | use Fcntl qw( :flock ); |
12b96196 |
40 | |
41 | use Clone::Any '_clone_data'; |
ffed8b01 |
42 | use Digest::MD5 (); |
a8fdabda |
43 | use FileHandle::Fmode (); |
ffed8b01 |
44 | use Scalar::Util (); |
ffed8b01 |
45 | |
9a63e1f2 |
46 | use DBM::Deep::Engine; |
460b1067 |
47 | use DBM::Deep::File; |
95967a5e |
48 | |
ffed8b01 |
49 | ## |
50 | # Setup constants for users to pass to new() |
51 | ## |
9a63e1f2 |
52 | sub TYPE_HASH () { DBM::Deep::Engine->SIG_HASH } |
53 | sub TYPE_ARRAY () { DBM::Deep::Engine->SIG_ARRAY } |
ffed8b01 |
54 | |
9a63e1f2 |
55 | # This is used in all the children of this class in their TIE<type> methods. |
0ca7ea98 |
56 | sub _get_args { |
57 | my $proto = shift; |
58 | |
59 | my $args; |
60 | if (scalar(@_) > 1) { |
61 | if ( @_ % 2 ) { |
62 | $proto->_throw_error( "Odd number of parameters to " . (caller(1))[2] ); |
63 | } |
64 | $args = {@_}; |
65 | } |
d0b74c17 |
66 | elsif ( ref $_[0] ) { |
4d35d856 |
67 | unless ( eval { local $SIG{'__DIE__'}; %{$_[0]} || 1 } ) { |
0ca7ea98 |
68 | $proto->_throw_error( "Not a hashref in args to " . (caller(1))[2] ); |
69 | } |
70 | $args = $_[0]; |
71 | } |
d0b74c17 |
72 | else { |
0ca7ea98 |
73 | $args = { file => shift }; |
74 | } |
75 | |
76 | return $args; |
77 | } |
78 | |
ffed8b01 |
79 | sub new { |
d0b74c17 |
80 | ## |
81 | # Class constructor method for Perl OO interface. |
82 | # Calls tie() and returns blessed reference to tied hash or array, |
83 | # providing a hybrid OO/tie interface. |
84 | ## |
85 | my $class = shift; |
86 | my $args = $class->_get_args( @_ ); |
87 | |
88 | ## |
89 | # Check if we want a tied hash or array. |
90 | ## |
91 | my $self; |
92 | if (defined($args->{type}) && $args->{type} eq TYPE_ARRAY) { |
6fe26b29 |
93 | $class = 'DBM::Deep::Array'; |
94 | require DBM::Deep::Array; |
d0b74c17 |
95 | tie @$self, $class, %$args; |
96 | } |
97 | else { |
6fe26b29 |
98 | $class = 'DBM::Deep::Hash'; |
99 | require DBM::Deep::Hash; |
d0b74c17 |
100 | tie %$self, $class, %$args; |
101 | } |
ffed8b01 |
102 | |
d0b74c17 |
103 | return bless $self, $class; |
ffed8b01 |
104 | } |
105 | |
96041a25 |
106 | # This initializer is called from the various TIE* methods. new() calls tie(), |
107 | # which allows for a single point of entry. |
0795f290 |
108 | sub _init { |
0795f290 |
109 | my $class = shift; |
994ccd8e |
110 | my ($args) = @_; |
0795f290 |
111 | |
83371fe3 |
112 | $args->{storage} = DBM::Deep::File->new( $args ) |
113 | unless exists $args->{storage}; |
460b1067 |
114 | |
115 | # locking implicitly enables autoflush |
116 | if ($args->{locking}) { $args->{autoflush} = 1; } |
117 | |
0795f290 |
118 | # These are the defaults to be optionally overridden below |
119 | my $self = bless { |
95967a5e |
120 | type => TYPE_HASH, |
e06824f8 |
121 | base_offset => undef, |
9a63e1f2 |
122 | staleness => undef, |
359a01ac |
123 | |
83371fe3 |
124 | storage => undef, |
9a63e1f2 |
125 | engine => undef, |
0795f290 |
126 | }, $class; |
9a63e1f2 |
127 | |
128 | $args->{engine} = DBM::Deep::Engine->new( { %{$args}, obj => $self } ) |
129 | unless exists $args->{engine}; |
8db25060 |
130 | |
fde3db1a |
131 | # Grab the parameters we want to use |
0795f290 |
132 | foreach my $param ( keys %$self ) { |
133 | next unless exists $args->{$param}; |
3e9498a1 |
134 | $self->{$param} = $args->{$param}; |
ffed8b01 |
135 | } |
d0b74c17 |
136 | |
9a63e1f2 |
137 | eval { |
138 | local $SIG{'__DIE__'}; |
0795f290 |
139 | |
9a63e1f2 |
140 | $self->lock; |
141 | $self->_engine->setup_fh( $self ); |
142 | $self->_storage->set_inode; |
143 | $self->unlock; |
144 | }; if ( $@ ) { |
145 | my $e = $@; |
146 | eval { local $SIG{'__DIE__'}; $self->unlock; }; |
147 | die $e; |
148 | } |
359a01ac |
149 | |
0795f290 |
150 | return $self; |
ffed8b01 |
151 | } |
152 | |
ffed8b01 |
153 | sub TIEHASH { |
6fe26b29 |
154 | shift; |
155 | require DBM::Deep::Hash; |
156 | return DBM::Deep::Hash->TIEHASH( @_ ); |
ffed8b01 |
157 | } |
158 | |
159 | sub TIEARRAY { |
6fe26b29 |
160 | shift; |
161 | require DBM::Deep::Array; |
162 | return DBM::Deep::Array->TIEARRAY( @_ ); |
ffed8b01 |
163 | } |
164 | |
ffed8b01 |
165 | sub lock { |
994ccd8e |
166 | my $self = shift->_get_self; |
83371fe3 |
167 | return $self->_storage->lock( $self, @_ ); |
ffed8b01 |
168 | } |
169 | |
170 | sub unlock { |
994ccd8e |
171 | my $self = shift->_get_self; |
83371fe3 |
172 | return $self->_storage->unlock( $self, @_ ); |
ffed8b01 |
173 | } |
174 | |
906c8e01 |
175 | sub _copy_value { |
176 | my $self = shift->_get_self; |
177 | my ($spot, $value) = @_; |
178 | |
179 | if ( !ref $value ) { |
180 | ${$spot} = $value; |
181 | } |
182 | elsif ( eval { local $SIG{__DIE__}; $value->isa( 'DBM::Deep' ) } ) { |
f9c33187 |
183 | ${$spot} = $value->_repr; |
906c8e01 |
184 | $value->_copy_node( ${$spot} ); |
185 | } |
186 | else { |
187 | my $r = Scalar::Util::reftype( $value ); |
188 | my $c = Scalar::Util::blessed( $value ); |
189 | if ( $r eq 'ARRAY' ) { |
190 | ${$spot} = [ @{$value} ]; |
191 | } |
192 | else { |
193 | ${$spot} = { %{$value} }; |
194 | } |
95bbd935 |
195 | ${$spot} = bless ${$spot}, $c |
906c8e01 |
196 | if defined $c; |
197 | } |
198 | |
199 | return 1; |
200 | } |
201 | |
9a63e1f2 |
202 | #sub _copy_node { |
203 | # die "Must be implemented in a child class\n"; |
204 | #} |
205 | # |
206 | #sub _repr { |
207 | # die "Must be implemented in a child class\n"; |
208 | #} |
ffed8b01 |
209 | |
210 | sub export { |
d0b74c17 |
211 | ## |
212 | # Recursively export into standard Perl hashes and arrays. |
213 | ## |
994ccd8e |
214 | my $self = shift->_get_self; |
d0b74c17 |
215 | |
f9c33187 |
216 | my $temp = $self->_repr; |
d0b74c17 |
217 | |
218 | $self->lock(); |
219 | $self->_copy_node( $temp ); |
220 | $self->unlock(); |
221 | |
9a63e1f2 |
222 | my $classname = $self->_engine->get_classname( $self ); |
223 | if ( defined $classname ) { |
224 | bless $temp, $classname; |
68f943b3 |
225 | } |
226 | |
d0b74c17 |
227 | return $temp; |
ffed8b01 |
228 | } |
229 | |
230 | sub import { |
d0b74c17 |
231 | ## |
232 | # Recursively import Perl hash/array structure |
233 | ## |
d0b74c17 |
234 | if (!ref($_[0])) { return; } # Perl calls import() on use -- ignore |
235 | |
994ccd8e |
236 | my $self = shift->_get_self; |
237 | my ($struct) = @_; |
d0b74c17 |
238 | |
c9cec40e |
239 | # struct is not a reference, so just import based on our type |
d0b74c17 |
240 | if (!ref($struct)) { |
f9c33187 |
241 | $struct = $self->_repr( @_ ); |
d0b74c17 |
242 | } |
243 | |
12b96196 |
244 | #XXX This isn't the best solution. Better would be to use Data::Walker, |
245 | #XXX but that's a lot more thinking than I want to do right now. |
7a960a12 |
246 | eval { |
9a63e1f2 |
247 | local $SIG{'__DIE__'}; |
12b96196 |
248 | $self->begin_work; |
249 | $self->_import( _clone_data( $struct ) ); |
250 | $self->commit; |
9a63e1f2 |
251 | }; if ( my $e = $@ ) { |
7a960a12 |
252 | $self->rollback; |
9a63e1f2 |
253 | die $e; |
7a960a12 |
254 | } |
255 | |
256 | return 1; |
ffed8b01 |
257 | } |
258 | |
13ff93d5 |
259 | #XXX Need to keep track of who has a fh to this file in order to |
260 | #XXX close them all prior to optimize on Win32/cygwin |
ffed8b01 |
261 | sub optimize { |
d0b74c17 |
262 | ## |
263 | # Rebuild entire database into new file, then move |
264 | # it back on top of original. |
265 | ## |
994ccd8e |
266 | my $self = shift->_get_self; |
cc4bef86 |
267 | |
268 | #XXX Need to create a new test for this |
83371fe3 |
269 | # if ($self->_storage->{links} > 1) { |
1400a48e |
270 | # $self->_throw_error("Cannot optimize: reference count is greater than 1"); |
d0b74c17 |
271 | # } |
272 | |
7a960a12 |
273 | #XXX Do we have to lock the tempfile? |
274 | |
d0b74c17 |
275 | my $db_temp = DBM::Deep->new( |
83371fe3 |
276 | file => $self->_storage->{file} . '.tmp', |
9a63e1f2 |
277 | type => $self->_type, |
278 | |
279 | # Bring over all the parameters that we need to bring over |
280 | num_txns => $self->_engine->num_txns, |
281 | byte_size => $self->_engine->byte_size, |
282 | max_buckets => $self->_engine->max_buckets, |
d0b74c17 |
283 | ); |
d0b74c17 |
284 | |
285 | $self->lock(); |
286 | $self->_copy_node( $db_temp ); |
287 | undef $db_temp; |
288 | |
289 | ## |
290 | # Attempt to copy user, group and permissions over to new file |
291 | ## |
292 | my @stats = stat($self->_fh); |
293 | my $perms = $stats[2] & 07777; |
294 | my $uid = $stats[4]; |
295 | my $gid = $stats[5]; |
83371fe3 |
296 | chown( $uid, $gid, $self->_storage->{file} . '.tmp' ); |
297 | chmod( $perms, $self->_storage->{file} . '.tmp' ); |
d0b74c17 |
298 | |
ffed8b01 |
299 | # q.v. perlport for more information on this variable |
90f93b43 |
300 | if ( $^O eq 'MSWin32' || $^O eq 'cygwin' ) { |
d0b74c17 |
301 | ## |
302 | # Potential race condition when optmizing on Win32 with locking. |
303 | # The Windows filesystem requires that the filehandle be closed |
304 | # before it is overwritten with rename(). This could be redone |
305 | # with a soft copy. |
306 | ## |
307 | $self->unlock(); |
83371fe3 |
308 | $self->_storage->close; |
d0b74c17 |
309 | } |
310 | |
83371fe3 |
311 | if (!rename $self->_storage->{file} . '.tmp', $self->_storage->{file}) { |
312 | unlink $self->_storage->{file} . '.tmp'; |
d0b74c17 |
313 | $self->unlock(); |
1400a48e |
314 | $self->_throw_error("Optimize failed: Cannot copy temp file over original: $!"); |
d0b74c17 |
315 | } |
316 | |
317 | $self->unlock(); |
83371fe3 |
318 | $self->_storage->close; |
9a63e1f2 |
319 | |
83371fe3 |
320 | $self->_storage->open; |
9a63e1f2 |
321 | $self->lock(); |
72e315ac |
322 | $self->_engine->setup_fh( $self ); |
9a63e1f2 |
323 | $self->unlock(); |
70b55428 |
324 | |
d0b74c17 |
325 | return 1; |
ffed8b01 |
326 | } |
327 | |
328 | sub clone { |
d0b74c17 |
329 | ## |
330 | # Make copy of object and return |
331 | ## |
994ccd8e |
332 | my $self = shift->_get_self; |
d0b74c17 |
333 | |
334 | return DBM::Deep->new( |
c3aafc14 |
335 | type => $self->_type, |
d0b74c17 |
336 | base_offset => $self->_base_offset, |
9a63e1f2 |
337 | staleness => $self->_staleness, |
83371fe3 |
338 | storage => $self->_storage, |
9a63e1f2 |
339 | engine => $self->_engine, |
d0b74c17 |
340 | ); |
ffed8b01 |
341 | } |
342 | |
9a63e1f2 |
343 | #XXX Migrate this to the engine, where it really belongs and go through some |
344 | # API - stop poking in the innards of someone else.. |
ffed8b01 |
345 | { |
346 | my %is_legal_filter = map { |
347 | $_ => ~~1, |
348 | } qw( |
349 | store_key store_value |
350 | fetch_key fetch_value |
351 | ); |
352 | |
353 | sub set_filter { |
354 | ## |
355 | # Setup filter function for storing or fetching the key or value |
356 | ## |
994ccd8e |
357 | my $self = shift->_get_self; |
358 | my $type = lc shift; |
359 | my $func = shift; |
d0b74c17 |
360 | |
ffed8b01 |
361 | if ( $is_legal_filter{$type} ) { |
83371fe3 |
362 | $self->_storage->{"filter_$type"} = $func; |
ffed8b01 |
363 | return 1; |
364 | } |
365 | |
366 | return; |
367 | } |
368 | } |
369 | |
fee0243f |
370 | sub begin_work { |
371 | my $self = shift->_get_self; |
9a63e1f2 |
372 | return $self->_engine->begin_work( $self, @_ ); |
fee0243f |
373 | } |
374 | |
375 | sub rollback { |
376 | my $self = shift->_get_self; |
9a63e1f2 |
377 | return $self->_engine->rollback( $self, @_ ); |
fee0243f |
378 | } |
379 | |
359a01ac |
380 | sub commit { |
381 | my $self = shift->_get_self; |
9a63e1f2 |
382 | return $self->_engine->commit( $self, @_ ); |
359a01ac |
383 | } |
fee0243f |
384 | |
ffed8b01 |
385 | ## |
386 | # Accessor methods |
387 | ## |
388 | |
72e315ac |
389 | sub _engine { |
390 | my $self = $_[0]->_get_self; |
391 | return $self->{engine}; |
392 | } |
393 | |
83371fe3 |
394 | sub _storage { |
2ac02042 |
395 | my $self = $_[0]->_get_self; |
83371fe3 |
396 | return $self->{storage}; |
ffed8b01 |
397 | } |
398 | |
4d35d856 |
399 | sub _type { |
2ac02042 |
400 | my $self = $_[0]->_get_self; |
d0b74c17 |
401 | return $self->{type}; |
ffed8b01 |
402 | } |
403 | |
4d35d856 |
404 | sub _base_offset { |
2ac02042 |
405 | my $self = $_[0]->_get_self; |
d0b74c17 |
406 | return $self->{base_offset}; |
ffed8b01 |
407 | } |
408 | |
9a63e1f2 |
409 | sub _staleness { |
410 | my $self = $_[0]->_get_self; |
411 | return $self->{staleness}; |
412 | } |
413 | |
994ccd8e |
414 | sub _fh { |
994ccd8e |
415 | my $self = $_[0]->_get_self; |
83371fe3 |
416 | return $self->_storage->{fh}; |
994ccd8e |
417 | } |
418 | |
ffed8b01 |
419 | ## |
420 | # Utility methods |
421 | ## |
422 | |
261d1296 |
423 | sub _throw_error { |
95967a5e |
424 | die "DBM::Deep: $_[1]\n"; |
ffed8b01 |
425 | } |
426 | |
ffed8b01 |
427 | sub STORE { |
d0b74c17 |
428 | ## |
429 | # Store single hash key/value or array element in database. |
430 | ## |
431 | my $self = shift->_get_self; |
9a63e1f2 |
432 | my ($key, $value) = @_; |
81d3d316 |
433 | |
a8fdabda |
434 | if ( !FileHandle::Fmode::is_W( $self->_fh ) ) { |
acd4faf2 |
435 | $self->_throw_error( 'Cannot write to a readonly filehandle' ); |
436 | } |
d0b74c17 |
437 | |
438 | ## |
439 | # Request exclusive lock for writing |
440 | ## |
441 | $self->lock( LOCK_EX ); |
442 | |
0cb639bd |
443 | # User may be storing a complex value, in which case we do not want it run |
444 | # through the filtering system. |
83371fe3 |
445 | if ( !ref($value) && $self->_storage->{filter_store_value} ) { |
446 | $value = $self->_storage->{filter_store_value}->( $value ); |
d0b74c17 |
447 | } |
448 | |
9a63e1f2 |
449 | $self->_engine->write_value( $self, $key, $value); |
d0b74c17 |
450 | |
451 | $self->unlock(); |
452 | |
86867f3a |
453 | return 1; |
ffed8b01 |
454 | } |
455 | |
456 | sub FETCH { |
d0b74c17 |
457 | ## |
458 | # Fetch single value or element given plain key or array index |
459 | ## |
cb79ec85 |
460 | my $self = shift->_get_self; |
9a63e1f2 |
461 | my ($key) = @_; |
ffed8b01 |
462 | |
d0b74c17 |
463 | ## |
464 | # Request shared lock for reading |
465 | ## |
466 | $self->lock( LOCK_SH ); |
467 | |
9a63e1f2 |
468 | my $result = $self->_engine->read_value( $self, $key); |
d0b74c17 |
469 | |
470 | $self->unlock(); |
471 | |
a86430bd |
472 | # Filters only apply to scalar values, so the ref check is making |
473 | # sure the fetched bucket is a scalar, not a child hash or array. |
83371fe3 |
474 | return ($result && !ref($result) && $self->_storage->{filter_fetch_value}) |
475 | ? $self->_storage->{filter_fetch_value}->($result) |
cb79ec85 |
476 | : $result; |
ffed8b01 |
477 | } |
478 | |
479 | sub DELETE { |
d0b74c17 |
480 | ## |
481 | # Delete single key/value pair or element given plain key or array index |
482 | ## |
a97c8f67 |
483 | my $self = shift->_get_self; |
9a63e1f2 |
484 | my ($key) = @_; |
d0b74c17 |
485 | |
a8fdabda |
486 | if ( !FileHandle::Fmode::is_W( $self->_fh ) ) { |
a86430bd |
487 | $self->_throw_error( 'Cannot write to a readonly filehandle' ); |
488 | } |
d0b74c17 |
489 | |
490 | ## |
491 | # Request exclusive lock for writing |
492 | ## |
493 | $self->lock( LOCK_EX ); |
494 | |
d0b74c17 |
495 | ## |
496 | # Delete bucket |
497 | ## |
9a63e1f2 |
498 | my $value = $self->_engine->delete_key( $self, $key); |
a86430bd |
499 | |
83371fe3 |
500 | if (defined $value && !ref($value) && $self->_storage->{filter_fetch_value}) { |
501 | $value = $self->_storage->{filter_fetch_value}->($value); |
3b6a5056 |
502 | } |
503 | |
d0b74c17 |
504 | $self->unlock(); |
505 | |
506 | return $value; |
ffed8b01 |
507 | } |
508 | |
509 | sub EXISTS { |
d0b74c17 |
510 | ## |
511 | # Check if a single key or element exists given plain key or array index |
512 | ## |
a97c8f67 |
513 | my $self = shift->_get_self; |
514 | my ($key) = @_; |
d0b74c17 |
515 | |
d0b74c17 |
516 | ## |
517 | # Request shared lock for reading |
518 | ## |
519 | $self->lock( LOCK_SH ); |
520 | |
9a63e1f2 |
521 | my $result = $self->_engine->key_exists( $self, $key ); |
d0b74c17 |
522 | |
523 | $self->unlock(); |
524 | |
525 | return $result; |
ffed8b01 |
526 | } |
527 | |
528 | sub CLEAR { |
d0b74c17 |
529 | ## |
530 | # Clear all keys from hash, or all elements from array. |
531 | ## |
a97c8f67 |
532 | my $self = shift->_get_self; |
ffed8b01 |
533 | |
a8fdabda |
534 | if ( !FileHandle::Fmode::is_W( $self->_fh ) ) { |
a86430bd |
535 | $self->_throw_error( 'Cannot write to a readonly filehandle' ); |
536 | } |
537 | |
d0b74c17 |
538 | ## |
539 | # Request exclusive lock for writing |
540 | ## |
541 | $self->lock( LOCK_EX ); |
542 | |
9a63e1f2 |
543 | #XXX Rewrite this dreck to do it in the engine as a tight loop vs. |
544 | # iterating over keys - such a WASTE - is this required for transactional |
545 | # clearning?! Surely that can be detected in the engine ... |
f9a320bb |
546 | if ( $self->_type eq TYPE_HASH ) { |
547 | my $key = $self->first_key; |
548 | while ( $key ) { |
83c43bb5 |
549 | # Retrieve the key before deleting because we depend on next_key |
f9a320bb |
550 | my $next_key = $self->next_key( $key ); |
9a63e1f2 |
551 | $self->_engine->delete_key( $self, $key, $key ); |
f9a320bb |
552 | $key = $next_key; |
553 | } |
554 | } |
555 | else { |
556 | my $size = $self->FETCHSIZE; |
c3aafc14 |
557 | for my $key ( 0 .. $size - 1 ) { |
9a63e1f2 |
558 | $self->_engine->delete_key( $self, $key, $key ); |
f9a320bb |
559 | } |
560 | $self->STORESIZE( 0 ); |
561 | } |
d0b74c17 |
562 | |
563 | $self->unlock(); |
564 | |
565 | return 1; |
ffed8b01 |
566 | } |
567 | |
ffed8b01 |
568 | ## |
569 | # Public method aliases |
570 | ## |
7f441181 |
571 | sub put { (shift)->STORE( @_ ) } |
572 | sub store { (shift)->STORE( @_ ) } |
573 | sub get { (shift)->FETCH( @_ ) } |
574 | sub fetch { (shift)->FETCH( @_ ) } |
baa27ab6 |
575 | sub delete { (shift)->DELETE( @_ ) } |
576 | sub exists { (shift)->EXISTS( @_ ) } |
577 | sub clear { (shift)->CLEAR( @_ ) } |
ffed8b01 |
578 | |
579 | 1; |
ffed8b01 |
580 | __END__ |