File Coverage

blib/lib/StoredHash.pm
Criterion Covered Total %
statement 150 486 30.8
branch 50 234 21.3
condition 6 42 14.2
subroutine 20 43 46.5
pod 11 30 36.6
total 237 835 28.3


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             StoredHash - Minimalistic, yet fairly complete DBI Persister with a definite NoSQL feel to it
4              
5             =head1 SYNOPSIS
6              
7             use StoredHash;
8             use DBI;
9             use Data::Dumper;
10            
11             my $dbh = DBI->connect("dbi:SQLite:dbname=/tmp/zoo.db");
12             # Lightweight demonstration of StoredHash in action (with SQLite)
13             $dbh->do("CREATE TABLE animals (speciesid INTEGER NOT NULL PRIMARY KEY, name CHAR(16), limbcnt INTEGER, family CHAR(16))");
14             my $shp = StoredHash->new('table' => 'animals', 'pkey' => ['speciesid'],
15             'autoid' => 1, 'dbh' => $dbh, 'debug' => 0);
16             # Hash object to be stored
17             my $monkey = {'name' => 'Common Monkey', 'limbcnt' => 5, 'family' => 'mammal',};
18            
19             # Happens to return numeric id (because of auto-increment pkey / autoid)
20             my $spid = $shp->insert($monkey);
21             print("Created by: id=$spid\n");
22             # Load entry
23             my $ent = $shp->load([$spid]);
24             print("Fetched (by $spid): ".Dumper($ent)."\n");
25             # Get: {'name' => 'Common Monkey', 'speciesid' => 469, 'limbcnt' => 5,'family' => 'mammal',}
26             # Fix error in entry (don't count tail to be limb)
27             $ent->{'limbcnt'} = 4;
28             # Update (with some redundant attributes that do not change)
29             print("Update $ent->{'speciesid'}\n");
30             $shp->update($ent, [$ent->{'speciesid'}]);
31             # Could reduce / optimize change to bare minimum:
32             my %change = ('limbcnt' => 4);
33             print("Reduce property value on $spid\n");
34             $shp->update(\%change, [$spid]);
35             # Later ... (species dies extinct ?)
36             #$shp->delete([$spid]);
37            
38             # Test if we need to insert / update (based on presence in DB)
39             my $id = 5987;
40             my $invals = {'name' => 'Crow', 'limbcnt' => 4, 'family' => 'birds'};
41             print("Test Presence of Animal '$id'\n");
42             if ($shp->exists([$id])) {$shp->update($invals, [$id]);}
43             else {$shp->insert($invals);}
44            
45             ##### Easy loading of sets / collections
46             # Load all the animals
47             my $animarr = $shp->loadset();
48             print("All Animals: ".Dumper($animarr)."\n");
49             # Load only mammals (by filter)
50             my $mammarr = $shp->loadset({'family' => 'mammal'});
51             print("Mammals: ".Dumper($mammarr)."\n");
52              
53             =head1 DESCRIPTION
54              
55             Allow DB Persistence operations (insert(), load(), update(), delete(),
56             exists()) on a plain old hash (unblessed or blessed) without writing
57             classes, persistence code or SQL.
58              
59             Optionally StoredHash allows your classes to inherit peristence capability from StoredHash allowing your objects
60             to call StoredHash persistence methoda via object directly.
61              
62              
63             =head1 GENERAL INFO ON StoredHash PERSISTENCE
64              
65             =over 4
66              
67             =item * Connection is stored in persister. Thus there is no need to pass it as parameter to persister methods.
68              
69             =item * Composite keys are supported by StoredHash. Because of this id values are passed in array. Id values must
70             be ordered the same as their attribute names suring construction (as passed in 'pkey' construction parameter).
71              
72             =item * Some persistence methods support 'attrs' parameter. This means "partial attributes" or "only these attributes" whatever the direction is
73             persistence operation is. Examples: load(): load only these attributes, update(): update only these attributes, etc.
74              
75             =item * StoredHash is not validating the hash keys / attribute (or 'attrs' parameter above) against these attributes
76             actually existing in DB schema. Caller of persistence methods is responsible validating the "fit" of hash to a schema.
77              
78             =back
79              
80             Above principles are consistent across persistence methods. These details will be not repeated in method documentation.
81              
82             =cut
83              
84             # Maintain a good NoSQL feel with your SQL database.
85             # ... a good nonrelation relationship
86              
87             # Author: olli.hollmen@gmail.com
88             # License: Perl License
89              
90             # StoredHash needs an OO instance of persister to function.
91              
92             # TODO: Because insert, update (the vals we want to pers.) are instance specific
93             # Possibly return an object or bare hash from preparation of ins/upd/del
94             # With
95             # - query
96             # - vals (to pass to exec)
97             # - attr (needed ?)
98             # - Assigned ID ?
99             # Make this object w. meth execute() ???? getid()
100             # http://www.nntp.perl.org/group/perl.dbi.dev/2010/03/msg5887.html
101             # ANSI X3.135 and ISO/IEC 9075
102             # ftp://sqlstandards.org/SC32/SQL_Registry/
103              
104             # TODO: Change/Add pkey => idattr @pkv => @idv
105             # Support Mappings (before storage as separate op ?)
106             package StoredHash;
107 5     5   93454 use Scalar::Util ('reftype'); #
  5         11  
  5         458  
108 5     5   1116 use Data::Dumper;
  5         12196  
  5         315  
109             our $hardval = 0; # 1= Call $dbh->do() 2=Return query
110 5     5   41 use strict;
  5         7  
  5         153  
111 5     5   18 use warnings;
  5         8  
  5         25478  
112             our $VERSION = '0.031';
113             # Module extraction config
114             #our $mecfg = {};
115             # Instance attributes (create accessors ?)
116             # Allow 'attr' to act as attr filter
117             my @opta = ('dbh', 'table','pkey','autoid','autoprobe','simu','errstr',
118             'seqname','debug',); #
119             # TODO: Support sequence for Oracle / Postgres
120             # seq_emp.NEXTVAL
121             my $bkmeta = {
122             #'mysql' => {'iq' => "SELECT LAST_INSERT_ID()",},
123             #'Sybase' => {'iq' => "SELECT \@\@identity",},
124             'Oracle' => {
125             #'iq' => "SELECT \@\@identity",
126             'sv' => '%s.NEXTVAL',}, # AS adid SET NOCOUNT OFF
127             # Postgres ???
128             };
129             # Tentative class-level / static structures:
130             # persister cache: $shpcache = {}; # Keyed by 'table'
131             # query cached: $qcache = {}; # Allow prepared and params ? plain K-V or HoH ?
132             #
133             =head1 METHODS
134              
135             =head2 $shp = StoredHash->new(%opts);
136              
137             Create new instance of StoredHash Persister.
138              
139             Keyword parameters in %opts:
140              
141             =over 4
142              
143             =item * 'pkey' - array (ref) to reflect the identifying attrtibute(s) of
144             entry (e.g. single attr for numeric sequential ids, multiple for composite key)
145              
146             =item * 'dbh' - DBI connection to database (optional). Not passing 'dbh' makes
147             methods insert/update/load/delete return the SQL query only (as a string)
148              
149             =back
150              
151             =cut
152             sub new {
153 3     3 1 1851 my ($class, %opt) = @_;
154 3         6 my $self = {};
155            
156             # Generate where by pkey OR use where
157             #if ($opt{'where'}) {}
158             # Moved for early bless
159 3         6 bless($self, $class);
160             # For Child loading / temp use
161 3 50       426 if ($opt{'loose'}) {goto PASSPKEY;}
  0         0  
162 3 50       8 if ($opt{'pkey'}) {
  0         0  
163 3         13 $self->{'pkey'} = $opt{'pkey'};
164             # TODO: Do NOT cache WHERE id ...
165 3         8 $self->{'where'} = whereid($self); # \%opt # join('AND', map({" $_ = ?";} pkeys(\%opt));
166             }
167             else {die("Need pkey info");}
168 3         20 PASSPKEY:
169             # Validate seq. (Need additional params to note call for seq?)
170             #if ($opt{'autoid'} eq 'seq') {
171             # #$c{'seqcall'};
172             #}
173             # Filter options to self
174             @$self{@opta} = @opt{@opta};
175            
176 3         10 return($self);
177             }
178              
179             =head2 $shp->errstr($v)
180              
181             Access error string that method may leave to object.
182             Notice that many methods throw exception (by die()) with
183             error message rather than leave it within object.
184              
185             =cut
186             sub errstr {
187 0     0 1 0 my ($p, $v) = @_;
188 0 0       0 if ($v) {$p->{'errstr'} = $v;}
  0         0  
189 0         0 $p->{'errstr'};
190             }
191              
192             # Internal method for executing query $q by filling placeholders with
193             # values passed in @$vals.
194             # Optional $rett (usually not passed) can force a special return type
195             # Some supported return force tags:
196             #=item * 'count' - number of entries counted with count(*) query
197             #=item * 'sth' - return statement handle ($sth), which will be used outside.
198             #=item * 'hash' - return a hash entry (first entry of resultset)
199             #=item * 'aoh' - return array of hashes reflecting result set.
200             # By default (no $rett) returns the ($ok)value from $sth->execute().
201             # Also by default statement statement handle gets properly closed
202             # (If requested return type was $sth, the caller should take care of
203             # calling $sth->finish()
204             sub qexecute {
205 0     0 0 0 my ($p, $q, $vals, $rett) = @_;
206 0         0 my $dbh = $p->{'dbh'};
207 0         0 my $sth; # Keep here to have avail in callbacks below
208 0 0 0     0 if (!$dbh || $p->{'simu'}) { #
209 0         0 local $Data::Dumper::Terse = 1;
210 0         0 local $Data::Dumper::Indent = 0;
211 0         0 print(STDERR "SQL($p->{'table'}): $q\nPlaceholder Vals:".Dumper($vals)."\n");
212 0         0 return(0);
213             }
214             # Special Return value generators
215             # These should also close the statement (if that is not returned)
216 0     0   0 my $rets = {
217 0         0 'count' => sub {my @a = $sth->fetchrow_array();$sth->finish();$a[0];},
  0         0  
218 0     0   0 'sth' => sub {return($sth);},
219 0     0   0 'hash' => sub {my $h = $sth->fetchrow_hashref();$sth->finish();$h;},
  0         0  
  0         0  
220 0     0   0 'aoh' => sub {my $arr = $sth->fetchall_arrayref({});$sth->finish();$arr;},
  0         0  
  0         0  
221 0         0 };
222             # $p->{'errstr'} =return(0);
223 0 0       0 if (!$dbh) {die("SHP: No Connection !");}
  0         0  
224 0 0 0     0 if ($p->{'debug'} && $vals) {print("Full Q: $q w. ".scalar(@$vals)." values\n");}
  0         0  
225             # Prepare cached ?
226 0         0 $sth = $dbh->prepare_cached($q); # print("CACHED\n");
227 0 0       0 if (!$sth) {die("Query ($q) Not prepared (".$dbh->errstr().")\n");}
  0         0  
228 0         0 my $ok = $sth->execute(@$vals);
229 0 0       0 if (!$ok) {die("Failed to execute\n - Query: $q\n - Vals: ".Dumper($vals)."\n - Message:\n".$sth->errstr()."");}
  0         0  
230             # Special return processing.
231             # TODO: Suppress Use of uninitialized value $rett in hash element
232 0 0       0 if (!$rett) {$rett = '';}
  0         0  
233 0 0       0 if (my $rcb = $rets->{$rett}) {
234             #print("Special return by $rett ($rcb)\n");
235 0         0 return($rcb->());
236             }
237             # Done with statement
238             DWS:
239 0         0 $sth->finish();
240 0         0 return($ok);
241             }
242              
243             ###################################################
244             # Make this the "best-possible" fallback quote when quote() method from driver (via connection)
245             # is not available. The surrounding quotes are included in the quoted string.
246             # This aims to be SQL compliant as much as possible
247             # Need looks like number (will fail sometimes) ?
248             sub quote {
249 0     0 0 0 my ($s) = @_;
250 0         0 $s =~ s/\'/\'\'/g;
251 0         0 $s =~ s/\n/\\n/g;
252 0         0 "'".$s."'";
253             }
254              
255             =head2 $shp->insert($e)
256              
257             Store entry %$e (hash) inserting it as a new entry to a database.
258              
259             Returns an array of ID values for the entry that got stored (array
260             of one element for numeric primary key, multiple for composite key).
261              
262             =cut
263             # (ref to)
264             #Connection has been passed previously in construction of persister.
265             #The table / schema to store to is either the one passed at
266             #construction or derived from perl "blessing" of entry ($e).
267             sub insert {
268 2     2 1 6 my ($p, $e, %c) = @_;
269 2         3 local $Data::Dumper::Terse = 1;local $Data::Dumper::Indent = 0;
  2         2  
270             # No enforced internal validation
271 2         2 eval {$p->validate();};
  2         4  
272 2 50       2 if ($@) {die("Persister validation error: $@");} # $p->{'errstr'} = $@;return(1);
  0         0  
273 2 50       5 if (reftype($e) ne 'HASH') {die("Entry needs to be HASH");} # return(2);
  0         0  
274             # Explicit attributes. Do not check for ref-valued attributes here.
275             # (with an idea that caller must know what it is passing).
276 2 50       5 if (my $ats = $c{'attrs'}) {
277 0 0       0 if (ref($ats) ne 'ARRAY') {die("Passed 'attrs' must be an array");}
  0         0  
278 0         0 %$e = map({($_, $e->{$_});} @$ats); # Reconfig $e content.
  0         0  
279             }
280             # Possibly also test for references (ds branching ?) eliminating them too
281             # In case some ARE found, make a copy, eliminate refs and mark copy as $e
282 2 50       4 if (grep({ref($e->{$_});} keys(%$e))) {
  6         10  
283             # Consider array serialization policy here or as a step before ?
284 0 0       0 my %ec = map({ref($e->{$_}) ? () : ($_, $e->{$_});} keys(%$e));
  0         0  
285 0         0 $e = \%ec;
286             }
287             # Extract attrs and values (for non-ref attrs)
288 2         6 my @ea = sort (keys(%$e));
289 2         5 my @ev = @$e{@ea}; # map()
290             # To Support sequence we MUST precalc placeholders here.
291             # In case of Sequence should place sequence call ...
292 2         2 my @pha = map({'?';} @ea);
  6         16  
293             # Sequence - Add sequenced ID allocation ???
294             # $p->{'seqname'}
295 2 50 33     6 if ($p->{'autoid'} && ($p->{'autoid'} eq 'seq')) {
296 0         0 my $bkt = 'Oracle';
297 0         0 my @pka = pkeys($p);
298 0 0       0 if (@pka > 1) {die("Error: Multiple (composite) pkeys for sequenced ID");}
  0         0  
299             # Add Sequence id attibute AND sequence call (unshift to front ?)
300 0         0 push(@ea, @pka); # $p->{'pkey'}->[0]
301             # Lookup Sequence Syntax (as printf format)for paticular DB Backend
302             # Fixed INSERT Below to NOT have placeholder for sequence (placeholders calc'd above)
303             # I case of sequence the counts of VALS vs @pha will be unbalanced (off by 1)
304 0         0 push(@pha, sprintf("$bkmeta->{$bkt}->{'sv'}", $p->{'seqname'}) ); #
305             #DEBUG:print("FMT: $bkmeta->{$bkt}->{'sv'} / $p->{'seqname'}\n");
306             }
307 2 50       4 if ($StoredHash::hardval) {
308              
309             #my $quote = $p->{'dbh'} ? ref($p->{'dbh'}).'::quote' : \"e;
310             #DEBUG:print("QUOTE = $quote\n\n\n\n");
311             #OLD:my $quote = $p->{'dbh'}->can('quote') ? \&$p->{'dbh'}->quote : \&sqlvalesc;
312 0 0   0   0 my $quoter = $p->{'dbh'}->can('quote') ? sub {$p->{'dbh'}->quote($_[0]);} : \"e;
  0         0  
313 0         0 @pha = map({
314             #$p->{'dbh'}->quote($e->{$_});
315 0         0 $quoter->($e->{$_}); # OLD: Embed $dbh as $_[0]
316             } @ea);
317             }
318 2         9 my $qp = "INSERT INTO $p->{'table'} (".join(',',@ea).") VALUES (".join(',', @pha).")";
319             # For now $StoredHash::hardval will always return query for the very efficient $dbh->do()
320             # to execute the query.
321 2 50       4 if (my $hv = $StoredHash::hardval) {
322 0 0       0 if ($hv == 2) {return($qp);} # Return INSERT ... as-is
  0 0       0  
323             elsif ($hv == 1) {
324 0         0 my $ok = $p->{'dbh'}->do($qp);
325 0 0       0 if (!$ok) {die("Failed do on hard query: $qp");}
  0         0  
326             # Proceed to autoid
327             }
328             }
329             #DEBUG:print(Dumper($p));
330 2 50       6 if ($p->{'debug'}) {print(STDERR "Ins.vals: ".Dumper(\@ev)."\n");}
  0         0  
331 2 50       4 if (!$p->{'dbh'}) {return($qp);} # No conn. - return SQL
  2         6  
332            
333 0         0 my $okid;
334 0 0       0 if (!$StoredHash::hardval) {$okid = $p->qexecute($qp, \@ev);}
  0         0  
335            
336             # Auto-id - either AUTO_INC style or Sequence (works for seq. too ?)
337 0 0       0 if ($p->{'autoid'}) {
338 0         0 my @pka = pkeys($p);
339 0 0       0 if (@pka != 1) {die(scalar(@pka)." Keys for Autoid");}
  0         0  
340 0         0 my $id = $p->fetchautoid();
341             #$e->{$pka[0]} = $id;
342 0         0 return(($id));
343             }
344             # Seq ?
345             #elsif () {}
346             # $p->pkeyvals($e); # wantarray ?
347             else {
348 0         0 my @pka = pkeys($p);
349 0         0 return(@$e{@pka}); # wantarray ? @$e{@pka} : [@$e{@pka}];
350             }
351             }
352              
353             =head2 $shp->update($e, $ids, %opts);
354              
355             Update an existing entry by ID(s) ($ids) in the database with values in hash %$e.
356              
357             Return true for success, false for failure (direct $ok values from underlying
358             $sth->execute() for 'autoid' => 1 ),
359              
360             =cut
361             # Provide protection for AUTO-ID (to not be changed) ?
362             #For flexibility the $idvals may be hash or array (reference) with
363             #hash containing (all) id keys and id values or alternatively array
364             #containing id values IN THE SAME ORDER as keys were passed during
365             #construction (with idattr/pkey parameter).
366              
367             sub update {
368 2     2 1 688 my ($p, $e, $idvals, %c) = @_;
369 2         3 local $Data::Dumper::Terse = 1;local $Data::Dumper::Indent = 0;
  2         2  
370 2         2 my @pka; # To be visible to closure
371             # Extract ID Values from hash OR array
372             # TODO: Loosen requirement for hash to describe pk-attributes ?
373 0     0   0 my $idvgens = {
374             'HASH' => sub {@$idvals{@pka};},
375 2     2   3 'ARRAY' => sub {return(@$idvals);},
376             #'' => sub {[$idvals];}
377 2         14 };
378             # No mandatory (internal) validation ?
379             #eval {$p->validate();};if ($@) {$p->{'errstr'} = $@;return(1);}
380 2         4 @pka = pkeys($p); # PKs from Persister
381 2 50       8 if (reftype($e) ne 'HASH') {die("Entry not passed as hash");} # {$p->{'errstr'} = "Entry needs to be hash";return(2);}
  0         0  
382             # Probe the type of $idvals
383 2         4 my $idrt = reftype($idvals);
384 2 50       4 if ($p->{'debug'}) {print("Got IDs:".Dumper($idvals)." as '$idrt'\n");}
  0         0  
385             #my @idv;
386 2         2 my @pkv; # PK Values
387             # Handle kw params for bulk updates ? Example: 'w' => {...} if (!$idrt {$widstr = wherefilter();}
388 2 50       5 if (my $idg = $idvgens->{$idrt}) {@pkv = $idg->();}
  2         3  
  0         0  
389             #VERYOLD:if ($idrt ne 'HASH') {$p->{'errstr'} = "ID needs to be hash";return(3);}
390             else {die("Need IDs as HASH or ARRAY (reference, got '$idrt')");}
391             #my ($cnt_a, $cnt_v) = (scalar(@pka), scalar(@pkv));
392 2 50       5 if (@pkv != @pka) {die("Number of ID keys and ID values (".scalar(@pka).'/'.scalar(@pkv).") not matching for update ($p->{'table'})");}
  0         0  
393             #OLDSIMPLE: my @ea = sort(keys(%$e));
394 2         2 my @ea;
395             # Leave to caller to check: Verify that we DO NOT HAVE pkeys in set (?)
396             #my $drive = 0;
397             # Comply to explicit attributes passed as 'attrs'
398 2 50       3 if (ref($c{'attrs'}) eq 'ARRAY') {
  2         8  
399             #print("DRIVE ATTRIBUTES: ");
400 0         0 @ea = @{$c{'attrs'}};
  0         0  
401             #$drive = 1;
402             }
403             # Use natural attributes from entry.
404             else {@ea = sort(keys(%$e));}
405             #print("DRIVE ATTRIBUTES = $drive: @ea\n");exit(1);
406             #my @pkv = @$idh{@pka}; # $idvals, Does not work for hash
407             # Check for undef/empty ID comps
408 2 50       6 if (my @badid = $p->invalidids(@pkv)) {
409 0         0 $p->{'errstr'} = "Bad ID Values found (@badid)";return(4);
  0         0  
410             }
411 2 50       4 my $widstr = whereid($p, $StoredHash::hardval ? (\@pkv) : () );
412             # Persistent object type
413 2         3 my $pot = $p->{'table'};
414 2 50       5 if (!$pot) {die("No table for update");}
  0         0  
415             #
416 2         6 my $qp = "UPDATE $pot SET ".join(',', map({" $_ = ?";} @ea)).
  6         11  
417             " WHERE $widstr";
418 2         2 my $dbh = $p->{'dbh'};
419            
420 2 50       4 if (my $hv = $StoredHash::hardval) {
421             #my $quote = $p->{'dbh'} ? ref($p->{'dbh'}).'::quote' : \"e;
422             #TODO:
423 0 0   0   0 my $quoter = $p->{'dbh'}->can('quote') ? sub {$p->{'dbh'}->quote($_[0]);} : \"e;
  0         0  
424 0         0 my $set = join(',', map({
425             #" $_ = ".$dbh->quote($e->{$_});
426 0         0 " $_ = ".$quoter->($e->{$_});
427             } @ea) );
428 0         0 $qp = "UPDATE $pot SET $set WHERE $widstr"; # hard values embedded by whereid()
429 0 0       0 if (!@pkv) {die("No ID:s for hardval=$StoredHash::hardval");}
  0         0  
430 0 0       0 if ($hv == 2) {return($qp);}
  0 0       0  
431             #elsif ($hv == 1) {return $dbh->do($qp);}
432             elsif ($hv == 1) {
433 0         0 my $ok = eval {$dbh->do($qp);};
  0         0  
434 0 0       0 if (!$ok) {die("Error DO(SQL: $qp): ".$@);}
  0         0  
435 0         0 return($ok);
436             }
437             }
438             # Combine Entry attr values and primary key values
439 2         5 my $allv = [@$e{@ea}, @pkv];
440 2 50       5 if ($p->{'debug'}) {print("Update allvals: ".Dumper($allv)."\n");}
  0         0  
441 2 50       4 if (!$p->{'dbh'}) {return($qp);}
  2         12  
442 0         0 my $ok;
443 0         0 eval {
444 0         0 $ok = $p->qexecute($qp, $allv);
445             };
446 0 0       0 if ($@) {die("Error Executing: ".$p->{'dbh'}->errstr()."\n");}
  0         0  
447             # Check all natural IDs (separate case for composite ?)
448             #if (!$p->{'autoid'}) {
449             #
450             #}
451 0         0 return($ok);
452             }
453              
454             =head2 $shp->delete($ids) OR $shp->delete($filter)
455              
456             Delete an entry from database by passing one of the following:
457              
458             =over 4
459              
460             =item * $ids - array with ID(s) for entry to be deleted (the usual use-case)
461              
462             =item * $filter - a hash with a where filter condition to delete by.
463              
464             =back
465              
466             Note that passing $filter haphazardly can cause massive destruction. Try to stick with passing $ids.
467              
468             =cut
469              
470             # (filter) ... containing (all) primary key(s) and their values)
471             #=item * array @$e - One or many primary key values for entry to be deleted
472             #The recommended use is case "array" as it is most versatile and most
473             #consistent with other API methods.
474              
475             sub delete {
476 2     2 1 643 my ($p, $e) = @_;
477             #if (!ref($p->{'pkey'})) {die("PKA Not Known");}
478             #eval {$p->validate();};if ($@) {$p->{'errstr'} = $@;return(1);}
479             #my @pka = @{$p->{'pkey'}};
480 2         4 my @pka = pkeys($p);
481 2 50       7 if (!$e) {die("Must have ID or filter for delete()\n");}
  0         0  
482            
483 2         4 my $rt = reftype($e); # Allows blessed
484 2         7 my $pkc = $p->pkeycnt();
485 2         2 my @pkv;my $wstr;
486             # $e Scalar, must have 1 pkey. Allow this forgiving behaviour for now.
487 2 50 33     7 if (!$rt && ($pkc == 1)) {$e = [$e];$rt = 'ARRAY';} # OLD: {@pkv = $e;}
  0         0  
  0         0  
488             # Hash - OLD: extract primary keys @pkv = @$e{@pka};
489             # NEW: treat as filter
490 2 50 0     3 if ($rt eq 'HASH') {
    0          
491 2 50       12 if (!%$e) {die("Will not delete by empty filter (HASH) !");}
  0         0  
492             # TODO: Share filter-case with load(), count()
493             #my @ks = sort(keys(%$e));
494 2         6 my @ks = grep({!ref($e->{$_})} keys(%$e));
  6         9  
495 2         6 @pkv = @$e{@ks}; # In this context @vs => @pkv - Not really vals for primary keys, but filter
496             #NOT:$wstr = wherefilter($e);
497 2         3 $wstr = join(' AND ', map({"$_ = ?";} @ks));
  6         11  
498             } #
499             # Array (of pk values) - check count matches
500 0         0 elsif (($rt eq 'ARRAY') && ($pkc == scalar(@$e))) {
501 0         0 @pkv = @$e;
502 0         0 $wstr = whereid($p);
503             }
504             else {die("No way to delete (without ARRAY for IDs or HASH for filter)\n");}
505             #NOTNEEDED:#my %pkh;@pkh{@pka} = @pkv;
506             #my $wstr = join(' AND ', map({"$_ = ?";} @pka));
507 2 50       5 if (!$wstr) {die("Not proceding to delete with empty filter !");}
  0         0  
508 2         5 my $qp = "DELETE FROM $p->{'table'} WHERE $wstr";
509 2 50       5 if (!$p->{'dbh'}) {return($qp);}
  2         6  
510 0         0 $p->qexecute($qp, \@pkv);
511             }
512             #my $dbh = $p->{'dbh'};
513             #my $sth = $dbh->prepare($qp);
514             #if (!$sth) {print("Not prepared\n");}
515             #$sth->execute(@pkv);
516              
517             =head2 $shp->exists($ids)
518              
519             Test if an entry exists in the DB table with ID values passed in @$ids (array).
520             Returns 1 (entry exists) or 0 (does not exist) under normal conditions.
521              
522             =cut
523             sub exists {
524 2     2 1 649 my ($p, $ids) = @_;
525 2 50       8 my $whereid = $p->{'where'} ? $p->{'where'} : whereid($p);
526 2         5 my $qp = "SELECT COUNT(*) FROM $p->{'table'} WHERE $whereid";
527 2 50       6 if (!$p->{'dbh'}) {return($qp);}
  2         4  
528 0         0 $p->qexecute($qp, $ids, 'count');
529             }
530              
531             =head2 $shp->load($ids)
532              
533             Load entry from DB table by its IDs passed in @$ids (array,
534             single id typical sequece autoid pkey, multiple for composite primary key).
535              
536             Entry will be loaded from single table passed at construction
537             (never as result of join from multiple tables).
538             Return entry as a hash (ref).
539              
540             =cut
541             sub load {
542 2     2 1 685 my ($p, $ids) = @_;
543 2 50       5 my $whereid = $p->{'where'} ? $p->{'where'} : whereid($p);
544             # Allow loading unique entry generic filter
545 2 50       8 if (reftype($ids) eq 'HASH') {
546             #$whereid = wherefilter($ids);
547 0         0 my @ks = grep({!ref($ids->{$_})} keys(%$ids));
  0         0  
548 0         0 my @vs = @$ids{@ks};
549 0         0 $whereid = join(' AND ', map({"$_ = ?";} @ks));
  0         0  
550 0         0 $ids = \@vs;
551             # Need hard (unique values) for Certain DBs
552             # TODO: Move to neater abstraction for $StoredHash::hardval
553 0 0 0     0 if (my $hv = $StoredHash::hardval && $p->{'dbh'}) {
554 0         0 my $i = -1;
555 0         0 my $dbh = $p->{'dbh'};
556 0         0 $whereid = join(' AND ', map({$i++;"$_ = ".$dbh->quote($vs[$i]);} @ks));
  0         0  
  0         0  
557 0         0 $ids = undef;
558             }
559             }
560 2         6 my $qp = "SELECT * FROM $p->{'table'} WHERE $whereid";
561 2 50       4 if (!$p->{'dbh'}) {return($qp);}
  2         4  
562 0         0 $p->qexecute($qp, $ids, 'hash');
563             #if (my $c = $p->{'class'}) {return(bless($h, $c));}
564             }
565              
566             =head2 $shp->loadset($filter, $sort, %opts);
567              
568             Load a set of Entries from persistent storage.
569             Optionally provide simple "where filter hash" ($filter), whose key-value criteria
570             is ANDed together to form the filter. Allow attibutes (in $sort, arrayref) to define sorting for entry set.
571             Allow %opts to contain 'attrs' (arrayref) to explicitly to define ettributes to load for each entry.
572             Return set / collection of entries as array of hashes (AoH).
573              
574             =cut
575             sub loadset {
576 0     0 1 0 my ($p, $h, $sort, %c) = @_; # filter, sortby
577 0         0 my $w = '';
578 0         0 my $s = '';
579             # if (@_ = 2 && ref($_[1]) eq 'HASH') {}
580 0 0       0 if ($h) {
581 0         0 my $vals = []; # Parameteric values
582 0         0 my $wf = wherefilter($h); # 'vals' => $vals
583 0 0       0 if (!$wf) {die("Empty Filter !");}
  0         0  
584 0         0 $w = " WHERE $wf";
585             }
586             # TODO: How to trigger DESC sorting (Something in %c OR first or last elem of $sort) ?
587 0 0 0     0 if (ref($sort) && @$sort) {
588 0         0 my $stype = ''; # Default in SQL:
589             #if ($sort->[0] eq '') {}
590 0         0 $s = ' ORDER BY '.join(',', @$sort);
591             }
592 0 0       0 if ($p->{'debug'}) {print("Loading set by '$w'\n");}
  0         0  
593 0         0 my $fldstr = '*';
594 0 0       0 if (ref($c{'attrs'}) eq 'ARRAY') {
595 0         0 $fldstr = join(',', @{$c{'attrs'}});
  0         0  
596             }
597 0         0 my $qp = "SELECT $fldstr FROM $p->{'table'} $w $s";
598             # Clean up query by (?):
599 0         0 $qp =~ s/\s+$//;
600 0         0 $p->qexecute($qp, undef, 'aoh');
601             }
602             =head2 $shp->cols(%opts)
603              
604             Sample Column names from (current) DB table.
605             Return (ref to) array with field names in it.
606              
607             %opts may contain KW parameter 'full' to get full DBI column_info() structure (See DBI for details).
608              
609             =cut
610             sub cols {
611 0     0 0 0 my ($p, %c) = @_;
612            
613             # Alternative for full table schema info
614             # TODO: 'fullinfo' => 1 or 'meta'
615 0 0       0 if ($c{'full'}) {
616 0         0 my $dbh = $p->{'dbh'};
617 0         0 my $sth = $dbh->column_info(undef, undef, $p->{'table'}, '%');
618 0         0 my $fullinfo = $sth->fetchall_arrayref({});
619 0         0 $sth->finish();
620 0         0 return($fullinfo);
621             }
622             # Likely Most portable way of quering cols
623 0         0 my $qp = "SELECT * FROM $p->{'table'} WHERE 1 = 0";
624 0         0 my $sth = $p->qexecute($qp, undef, 'sth');
625 0         0 my $cols = $sth->{'NAME'};
626 0 0       0 if (@_ == 1) {$sth->finish();return($cols);}
  0         0  
  0         0  
627             #elsif (@_ == 2) {$rett = $_[1];};
628             #if ($rett ne 'meta') {return(undef);}
629 0         0 return(undef);
630             }
631              
632             # TODO: Load "tree" of entries rooted at an entry / entries (?)
633             # Returns a set (array) of entries or single (root entry if
634             # option $c{'fsingle'} - force single - is set.
635             sub loadtree {
636 0     0 0 0 my ($p, %c) = @_;
637 0         0 my $chts = $c{'ctypes'};
638 0         0 my $w = $c{'w'};
639 0         0 my $fsingle = $c{'fsingle'}; # singleroot, uniroot
640 0         0 my $arr = loadset($p, $w);
641 0         0 for my $e (@$arr) {my $err = loadchildern($p, $e, %c);}
  0         0  
642             # Choose return type
643 0 0       0 if ($fsingle) {return($arr->[0]);}
  0         0  
644 0         0 return($arr);
645             }
646              
647             # TODO: Load Instances of child object types for entry.
648             # Child types are defined in 'ctypes' array(ref) in options.
649             # Array 'ctypes' may be one of the following
650             #=item * Plain child type names (array of scalars), the rest is guessed
651             #=item * Array of child type definition hashes with hashes defining following:
652             # =item * table - The table / objectspace of child type
653             # =item * parkey - Parent id field in child ("foreign key" field in rel DBs)
654             # =item * memname - Mamber name to place the child collection into in parent entry
655             #=item * Array of arrays with inner arrays containing 'table','parkey','memname' in
656             # that order(!), (see above for meanings)
657             # Return 0 for no errors
658              
659             # TODO: Maintain persister cache with rudimentary relational info.
660             sub loadchildren {
661 0     0 0 0 my ($p, $e, %c) = @_;
662 0         0 my $chts = $c{'ctypes'};
663 0 0       0 if (!$chts) {die("No Child types indicated");}
  0         0  
664 0 0       0 if (ref($chts) ne 'ARRAY') {die("Child types not ARRAY");}
  0         0  
665 0         0 my @ids = pkeyvals($p, $e);
666 0 0       0 if (@ids > 1) {die("Loading not supported for composite keys");}
  0         0  
667 0         0 my $dbh = $p->{'dbh'};
668 0         0 my $debug = $p->{'debug'};
669 0         0 for (@$chts) {
670             #my $ct = $_;
671 0         0 my $cfilter;
672             # Use or create a complete hash ?
673 0         0 my $cinfo = makecinfo($p, $_);
674 0 0       0 if ($debug) {print(Dumper($cinfo));}
  0         0  
675             # Load type by created filter
676 0         0 my ($ct, $park, $memn) = @$cinfo{'table','parkey','memname',};
677 0 0       0 if (!$park) {}
678             # Create where by parkey info
679             #$cfilter = {$park => $ids[0]}; # What is par key - assume same as parent
680 0 0       0 if (@$park != @ids) {die("Par and child key counts mismatch");}
  0         0  
681 0         0 @$cfilter{@$park} = @ids;
682             #my $cfilter =
683             # Take a shortcut by not providing pkey
684 0         0 my $shc = StoredHash->new('table' => $ct, 'pkey' => [],
685             'dbh' => $dbh, 'loose' => 1, 'debug' => $debug);
686 0         0 my $carr = $shc->loadset($cfilter);
687 0 0 0     0 if (!$carr || !@$carr) {next;}
  0         0  
688             #if ($debug) {print("Got Children".Dumper($arr));}
689 0         0 $e->{$memn} = $carr;
690             # Blessing
691 0 0       0 if (my $bto = $cinfo->{'blessto'}) {map({bless($_, $bto);} @$carr);}
  0         0  
  0         0  
692             # Circular Ref from child to parent ?
693             #if (my $pla = $cinfo->{'parlinkattr'}) {map({$_->{$pla} = $e;} @$carr);}
694             }
695             # Autobless Children ?
696 0         0 return(0);
697             }
698              
699              
700             # Internal method for using or making up Child relationship information
701             # for loading related entities.
702             sub makecinfo {
703 0     0 0 0 my ($p, $cv) = @_;
704             # Support array with: 'table','parkey','memname'
705 0 0       0 if (ref($cv) eq 'ARRAY') {
    0          
    0          
706 0         0 my $cinfo;
707 0 0       0 if (@$cv != 3) {die("Need table, parkey, memname in array");}
  0         0  
708 0         0 @$cinfo{'table','parkey','memname'} = @$cv;
709 0         0 return($cinfo);
710             }
711             # Assume all is there (could validate and provide missing)
712 0         0 elsif (ref($cv) eq 'HASH') {
713 0         0 my @a = ('table','parkey','memname');
714             # Try guess parkey ?
715 0 0       0 if (!$cv->{'parkey'}) {$cv->{'parkey'} = [pkeys($p)];}
  0         0  
716 0 0       0 for (@a) {if (!$cv->{$_}) {die("Missing '$_' in cinfo");}}
  0         0  
  0         0  
717 0         0 return($cv);
718             }
719             elsif (ref($cv) ne '') {die("child type Not scalar (or hash)");}
720             ################## Make up
721 0         0 my $ctab = $cv;
722 0         0 my $memname = $ctab; # Default memname to child type name (Plus 's') ?
723             # Guess by parent
724 0         0 my $parkey = [pkeys($p)];
725 0         0 my $cinfo = {'table' => $ctab, 'parkey' => $parkey, 'memname' => $ctab,};
726 0         0 return($cinfo);
727             }
728              
729             ###################################################################
730              
731             # Internal Persister validator for the absolutely mandatory properties of
732             # persister object itself.
733             # Doesn't not validate entry
734             sub validate {
735 2     2 0 2 my ($p) = @_;
736 2 50       5 if (ref($p->{'pkey'}) ne 'ARRAY') {die("PK Attributes Not Known\n");}
  0         0  
737             # Allow table to come from blessing (so NOT required)
738             #if (!$p->{'table'}) {die("No Table\n");}
739 2 50       6 if ($p->{'simu'}) {return;}
  0         0  
740             # Do NOT Require conenction
741             #if (!ref($p->{'dbh'})) {die("NO dbh to act on\n");} # ne 'DBI'
742            
743             }
744              
745             #=head2 @pka = $shp->pkeys()
746             #
747             # Internal method for returning array of id keys (Real array, not ref).
748             #
749             #=cut
750             sub pkeys {
751 11     11 0 12 my ($p) = @_;
752 11         25 my $prt = reftype($p);
753 11 50       22 if ($prt ne 'HASH') {
754 0         0 $|=1;
755 0         0 print(STDERR Dumper([caller(1)]));
756 0         0 die("StoredHash Not a HASH (is '$p'/'$prt')");
757             }
758             # Excessive validation ?
759 11 50       23 if (ref($p->{'pkey'}) ne 'ARRAY') {die("Primary keys not in an array");}
  0         0  
760             #return($p->{'pkey'});
761 11         9 return(@{$p->{'pkey'}});
  11         24  
762             }
763              
764             =head2 $shp->count($filter)
765              
766             Get Count of all or a filtered set of entries (by optional $filter) in table.
767             Return (scalar) count of entries.
768              
769             =cut
770             sub count {
771 4     4 1 1985 my ($p, $fh) = @_; # $fh - Filter Hash
772 4         9 my $qc = "SELECT COUNT(*) FROM $p->{'table'}";
773             # TODO: See filter case for load(), delete()
774             # Use it and replace 2nd param of qexecute w. params
775 4 100 66     19 if (ref($fh) eq 'HASH' && keys(%$fh)) {
776 2         5 my $w = wherefilter($fh); # my ($w, $vals) = wherefilter_para($fh);
777 2         4 $qc .= " WHERE $w";
778             }
779 4 50       8 if ($p->{'debug'}) {print("Count Query:$qc\n");}
  0         0  
780 4 50       7 if (!$p->{'dbh'}) {return($qc);}
  4         9  
781 0         0 $p->qexecute($qc, undef, 'count'); # $vals
782             }
783              
784             =head1 INTERNAL METHODS
785              
786             These methods you should not need working on the high level. However for the curious they are outlined here.
787              
788             =head2 @pkv = $shp->pkeyvals($e)
789              
790             Return Primary key values (as "real" array, not ref to one) from hash %$e.
791             undef values are produced for non-existing keys.
792             Mostly used for internal operations (and possibly debugging).
793              
794             =cut
795             sub pkeyvals {
796 0     0 1 0 my ($p, $e) = @_;
797 0         0 my @pkeys = pkeys($p);
798 0         0 @$e{@pkeys};
799             }
800              
801             # TODO: Implement pulling last id from sequence
802             sub fetchautoid {
803 0     0 0 0 my ($p) = @_;
804 0         0 my $dbh;
805             #$dbh->{'Driver'}; # Need to test ?
806             #DEV:print("AUTOID FETCH TO BE IMPLEMENTED\n");return(69);
807 0         0 my $pot = $p->{'table'};
808 0 0       0 if (!$pot) {die("No table for fetching auto-ID");}
  0         0  
809 0 0       0 if (!($dbh = $p->{'dbh'})) {die("No Connection for fetching ID");}
  0         0  
810 0         0 $dbh->last_insert_id(undef, undef, $pot, undef);
811             }
812              
813             sub pkeycnt {
814 2     2 0 2 my ($p) = @_;
815             #if (ref($p->{'pkey'}) ne 'ARRAY') {die("Primary keys not in an array");}
816             #scalar(@{$p->{'pkey'}});
817 2         4 my @pkeys = pkeys($p);
818 2         3 scalar(@pkeys);
819             }
820              
821             # Internal method for checking for empty or undefined ID values.
822             # In all reasonable databases and apps these are not valid values.
823             sub invalidids {
824 2     2 0 3 my ($p, @idv) = @_;
825 2 50       2 my @badid = grep({!defined($_) || $_ eq '';} @idv);
  3         15  
826 2         6 return(@badid);
827             }
828              
829             =head2 $shp->whereid($pkvals);
830              
831             Generate SQL WHERE Clause for update() (or delete() or load() or exists()) based on primary keys of current (table) type.
832             Return WHERE clause with id-attribute(s) and placeholder(s) (idkey = ?, ...), without the WHERE keyword.
833             Mostly called for internal operations. You should not need this.
834              
835             =cut
836             sub whereid {
837 5     5 1 6 my ($p, $pkval) = @_;
838             # # Allow IDs to be hash OR array ?? Not because hash would req. to store order
839 5         9 my @pka = pkeys($p);
840 5 50       426 if (@pka < 1) {die("No Pkeys to create where ID clause");}
  0         0  
841             # my $wstr =
842 5 0 33     10 if ($pkval && (ref($pkval) eq 'ARRAY') && (@$pkval == @pka) ) {
      33        
843             # TODO: Mock DBI
844 0         0 my $dbh = $p->{'dbh'};
845             # Can't use string ("DBI::db::quote") as a subroutine ref while "strict refs" in use
846 0     0   0 my $quote = $p->{'dbh'} ?
847             #ref($p->{'dbh'}).'::quote'
848             sub {$p->{'dbh'}->quote($_[0]);}
849 0 0       0 : \"e;
850 0         0 my $i = -1;
851             #no strict 'refs';
852 0         0 my $wif = join(' AND ', map({
853 0         0 $i++;"$_ = ".$quote->($pkval->[$i]);
  0         0  
854             } @pka));
855 0         0 return $wif; # OLD (nostrict) $wid
856             }
857 5         8 return join(' AND ', map({"$_ = ?";} @pka));
  7         24  
858             }
859              
860             # Internal fallback method to escape (string) value. Prefer using $dbh->quote() if $dbh is
861             # handle available and the associated DBD Driver supports it.
862             # The first parameter to this is an unused dummy parameter to match call of $dbh->quote($str).
863             # This does not place surrounding quotes on the value returned.
864             # Return value properly escaped.
865             # TODO: Cover all scenarios
866             sub sqlvalesc {
867 2     2 0 2 my ($foo, $v) = @_;
868             #$v =~ s/'/\\'/g; # $str =~ s/'/''/g;
869 2         3 $v =~ s/\'/\'\'/g;
870 2         2 $v =~ s/\n/\\n/g;
871 2         7 $v;
872             }
873              
874             # TODO: Create list for WHERE IN Clause based on some assumptions
875             sub invalues {
876 0     0 0 0 my ($vals) = @_;
877             # Assume array ref validated outside
878 0 0       0 if (ref($vals) ne 'ARRAY') {die("Not an array for invals");}
  0         0  
879             # Escape within Quotes ?
880 0         0 join(',', map({
881 0 0       0 if (/^\d+$/) {$_;}
  0         0  
882             else {
883 0         0 my $v = sqlvalesc(undef, $_);
884 0         0 "'$v'";
885             }
886             } @$vals));
887             }
888              
889             sub rangefilter {
890 0     0 0 0 my ($attr, $v) = @_;
891 0 0       0 if (ref($v) ne 'ARRAY') {die("Need value range as ARRAY of 2 elems");}
  0         0  
892             # Or just even and sort, grab 2 at the time ?
893 0 0       0 if (@$v != 2) {die("Range cannot be formed - need exactly 2 elements");}
  0         0  
894 0 0 0     0 if (!defined($v->[0]) || !defined($v->[1]) ) {die("Missing either of the values ($v->[0], $v->[0])");}
  0         0  
895             # Auto-arrange ??? Test for both being numbers
896 0 0       0 my @nums = map({Scalar::Util::looks_like_number($_) ? (1) : ();} @$v);
  0         0  
897 0 0       0 if (@nums == 2) {
898 0 0       0 if ($v->[1] < $v->[0]) {$v = [$v->[1],$v->[0]];}
  0         0  
899             }
900             # Detect need to escape (time vs. number)
901 0         0 return "($attr >= $v->[0]) AND ($attr <= $v->[1])";
902             #return " $attr BETWEEN $v->[0] AND $v->[1]";
903             }
904              
905             #=head2 StoredHash::wherefilter($e,%c);
906             #
907             # Generate simple WHERE filter by hash %$e. The keys are assumed to be attributes
908             # of DB and values are embedded as values into SQL (as opposed to using placeholers).
909             # To be perfect in escaping per attribute type info would be needed.
910             # For now we do best effort heuristics (attr val \d+ is assumed
911             # to be a numeric field in SQL, however 000002345 could actually
912             # be content of a char/text/varchar field).
913             # Return WHERE filter clause without WHERE keyword.
914             sub wherefilter {
915 2     2 0 3 my ($e, %c) = @_;
916 2         3 my $w = '';
917 2         2 my $fop = ' AND ';
918             #my $rnga = $c{'rnga'}; # Range attributes
919 2 50       4 if (ref($e) ne 'HASH') {die("No hash for filter generation");}
  0         0  
920             # Ensure deterministic order
921 2         6 my @keys = sort keys(%$e);
922 2         2 my @qc; # Query Components
923             # Assume hard values, treat everything as string (?)
924             # TODO: forcestr ?
925 2         3 @qc = map({
926 2         3 my $v = $e->{$_};
927             #my $rv = ref($v);
928             #if ($rnga->{$_} && ($rv eq 'ARRAY') && (@$v == 2)) {rangefilter($_, $v);}
929             # For now, assume IN - clause
930 2 50       11 if (ref($v) eq 'ARRAY') {" $_ IN (".invalues($v).") ";}
  0 50       0  
  0 50       0  
931             # SQL Wildcard
932 0         0 elsif ($v =~ /%/) {"$_ LIKE '$v'";}
933             # Detect numeric (likely numeric, not perfect)
934             # TODO: Explicit param to
935 2         6 elsif ($v =~ /^\d+$/) {"$_ = $v";}
936             # Assume string
937             else {"$_ = '".sqlvalesc(undef, $v)."'";}
938            
939             } @keys);
940             # Create PARAMETRIC query
941 2 50       6 if (ref $c{'vals'} eq 'ARRAY') {
942 0         0 my @vals = ();
943 0         0 map({
944 0         0 my $v = $e->{$_};
945 0 0       0 if (ref($v) eq 'ARRAY') {push(@vals, @$v);" $_ IN (".join(',', map({"?";} @$v)).") ";}
  0 0       0  
  0         0  
  0         0  
  0         0  
946 0         0 elsif ($v =~ /%/) {push(@vals, $v);"$_ LIKE ?";}
  0         0  
947 0         0 else {push(@vals, $v);"$_ = ?";}
948             } @keys);
949 0         0 push(@{$c{'vals'}}, @vals);
  0         0  
950             }
951 2         5 return(join($fop, @qc)); # join by AND
952             }
953              
954             #=head2 my ($where, $para) = wherefilter_para($e);
955             # Where filter for parametric query (for load(), delete() count())
956             # Return WHERE clause (without 'WHERE') and parametric values.
957             # Throw exception on empty %$e or ... (empty filter)
958             # Caller should not simply check the count of keys in hash as ref
959             # valued key-pairs are skipped here.
960             sub wherefilter_para {
961 0     0 0 0 my ($e) = @_;
962 0 0 0     0 if (!$e || !%$e) {die("Will not generate filter by no HASH / empty HASH !");}
  0         0  
963 0         0 my @ks = grep({!ref($e->{$_})} keys(%$e));
  0         0  
964 0         0 my @vs = @$e{@ks}; # In this context @vs => @pkv - Not really vals for primary keys, but filter
965 0         0 my $wstr = join(' AND ', map({"$_ = ?";} @ks));
  0         0  
966             #if (!$wstr || $wstr =~ /^\s*$/) {die("Will not generate empty filter clause");}
967 0         0 return($wstr, \@vs);
968             }
969              
970             # Internal: Serialize all values (singles,multi) from a hash to an array
971             # based on sorted key order. Multi-valued keys (with value being array reference)
972             # add multiple items.
973             sub allentvals {
974 2     2 0 11 my ($h) = @_;
975 0         0 map({
976 2 50       11 if (ref($h->{$_}) eq 'HASH') {();}
  6 50       13  
  0         0  
977 0         0 elsif (ref($h->{$_}) eq 'ARRAY') {@{$h->{$_}};}
  6         11  
978             else {($h->{$_});}
979             } sort(keys(%$h)));
980             }
981              
982              
983             # TODO: Move to util ?
984             #=head2 $p->dbtabinfo(%opts) OR StoredHash::dbtabinfo($dbh, %opts);
985             # Covenience method for $dbh->table_info()
986             # Options:
987             #=item * tabonly - Filter out all DB Objects where TABLE_TYPE is not 'TABLE'
988             #
989             # Return AoH where each of inner hashes are info for single table. Property names are in
990             # standard DBI table_info() format (see perldoc DBI).
991             sub dbtabinfo {
992 0     0 0   my (%c) = @_;
993 0           my ($p, $pdbh);
994 0           my $rt = reftype($_[0]);
995 0 0         if ($rt eq 'StoredHash') {$p = shift();%c = @_;}
  0            
  0            
  0            
996             # elsif ($rt eq '')
997 0           else {$pdbh = shift();%c = @_;}
998 0   0       my $dbh = $pdbh || $p->{'dbh'} || $c{'dbh'};
999 0 0         if (!$dbh) {die("No Connection for table info");}
  0            
1000 0           my $sth = $dbh->table_info();
1001 0           my $tabinfo = $sth->fetchall_arrayref({}); # AoH
1002 0           $sth->finish();
1003             # Replace with $c{'all'} - Get all database objects (like views, indices ...)
1004 0 0         if ($c{'tabonly'}) {
1005 0           @$tabinfo = grep({$_->{'TABLE_TYPE'} eq 'TABLE';} @$tabinfo);
  0            
1006             }
1007            
1008 0           return($tabinfo);
1009             }
1010             # Experimental wrapper to query attributes
1011             # TODO: dbattrinfo($dbh, $tn);
1012             sub dbattrinfo {
1013 0     0 0   my ($dbh, $tn) = @_;
1014             #my ($p, $pdbh);
1015             #my $rt = reftype($_[0]);
1016             #if ($rt eq 'StoredHash') {$p = shift();%c = @_;}
1017             #elsif ($rt eq '') {$pdbh = shift();%c = @_;}
1018             #my $dbh = $pdbh || $p->{'dbh'} || $c{'dbh'};
1019 0 0         if (!$dbh) {die("No Connection for attribute info");}
  0            
1020 0 0         if (!$tn) {die("No table name for attribute info");}
  0            
1021 0           my $sth = $dbh->column_info(undef, undef, $tn, '%');
1022 0           my $arr = $sth->fetchall_arrayref({}); # AoH
1023 0           $sth->finish();
1024 0           return($arr);
1025             }
1026             1;