File Coverage

blib/lib/StoredHash.pm
Criterion Covered Total %
statement 95 301 31.5
branch 29 136 21.3
condition 2 15 13.3
subroutine 15 33 45.4
pod 12 25 48.0
total 153 510 30.0


line stmt bran cond sub pod time code
1             # Minimalistic, yet fairly complete DBI Persister
2             # Allow DB Persistence operations (insert(), load(), update(), delete(),
3             # exists()) on a plain old hash (unblessed or blessed) without writing
4             # classes, persistence code or SQL.
5              
6             # Author: olli.hollmen@gmail.com
7             # License: Perl License
8              
9             # StoredHash needs an OO instance of persister to function.
10              
11             # Because insert, update (the vals we want to pers.) are instance specific
12             # Possibly return an object or bare hash from preparation of ins/upd/del
13             # With
14             # - query
15             # - vals (to pass to exec)
16             # - attr (needed ?)
17             # - Assigned ID ?
18             # Make this object w. meth execute() ???? getid()
19              
20             # TODO: Change/Add pkey => idattr @pkv => @idv
21             # Support Mappings (before storage as separate op ?)
22             package StoredHash;
23 3     3   115434 use Scalar::Util ('reftype'); #
  3         7  
  3         356  
24 3     3   1239 use Data::Dumper;
  3         11939  
  3         13252  
25              
26             #use strict;
27             #use warnings;
28             our $VERSION = '0.029';
29             # Module extraction config
30             our $mecfg = {};
31             # Instance attr (create access ...)
32             # Allow 'attr' to act as attr filter
33             my @opta = ('dbh', 'table','pkey','autoid','autoprobe','simu','errstr',
34             'seqname','debug',); #
35             # TODO: Support sequence for Oracle / Postgres
36             # seq_emp.NEXTVAL
37             my $bkmeta = {
38             #'mysql' => {'iq' => "SELECT LAST_INSERT_ID()",},
39             #'Sybase' => {'iq' => "SELECT \@\@identity",},
40             'Oracle' => {
41             #'iq' => "SELECT \@\@identity",
42             'sv' => '%s.NEXTVAL',}, # AS adid SET NOCOUNT OFF
43             # Postgres ???
44             };
45              
46             # Create New instance of StoredHash Persister.
47             # Options in %opt must have
48             # - pkey/idattr - array (ref) to reflect the identifying attrtibute(s) of
49             # entry (single attr for numeric ids, multiple for composite key)
50             # Optional attributes
51             # - dbh - DBI connection to database. Not passing 'dbh' makes
52             # methods insert/update/load/delete return the SQL query only (as a string)
53             sub new {
54 3     3 1 1150 my ($class, %opt) = @_;
55 3         8 my $self = {};
56            
57             # Generate where by pkey OR use where
58             #if ($opt{'where'}) {}
59             # Moved for early bless
60 3         8 bless($self, $class);
61             # For Child loading / temp use
62 3 50       13 if ($opt{'loose'}) {goto PASSPKEY;}
  0         0  
63 3 50       12 if ($opt{'pkey'}) {
  0         0  
64 3         14 $self->{'pkey'} = $opt{'pkey'};
65             # TODO: Do NOT cache WHERE id ...
66 3         13 $self->{'where'} = whereid($self); # \%opt # join('AND', map({" $_ = ?";} pkeys(\%opt));
67             }
68             else {die("Need pkey info");}
69 3         36 PASSPKEY:
70             # Validate seq. (Need additional params to note call for seq?)
71             #if ($opt{'autoid'} eq 'seq') {
72             # #$c{'seqcall'};
73             #}
74             # Filter options to self
75             @$self{@opta} = @opt{@opta};
76            
77 3         11 return($self);
78             }
79             # Access error string that method may leave to object.
80             # Notice that many methods throw exception (by die()) with
81             # error message rather than leave it within object.
82             sub errstr {
83 0     0 1 0 my ($p, $v) = @_;
84 0 0       0 if ($v) {$p->{'errstr'} = $v;}
  0         0  
85 0         0 $p->{'errstr'};
86             }
87              
88             # Internal method for executing query $q by filling placeholders with
89             # values passed in @$vals.
90             # Optional $rett (usually not passed) can force a special return type
91             # Some supported return force tags:
92             # - 'count' - number of entries counted with count(*) query
93             # - 'sth' - return statement handle ($sth), which will be used outside.
94             # - 'hash' - return a hash entry (first entry of resultset)
95             # - 'aoh' - return array of hashes reflecting result set.
96             # By default (no $rett) returns the ($ok)value from $sth->execute().
97             # Also by default statement statement handle gets properly closed
98             # (If requested return type was $sth, the caller should take care of
99             # calling $sth->finish()
100             sub qexecute {
101 0     0 0 0 my ($p, $q, $vals, $rett) = @_;
102 0         0 my $dbh = $p->{'dbh'};
103 0         0 my $sth; # Keep here to have avail in callbacks below
104 0 0 0     0 if (!$dbh || $p->{'simu'}) { #
105 0         0 local $Data::Dumper::Terse = 1;
106 0         0 local $Data::Dumper::Indent = 0;
107 0         0 print("SQL($p->{'table'}): $q\nPlaceholder Vals:".Dumper($vals)."\n");
108 0         0 return(0);
109             }
110             # Special Return value generators
111             # These should also close the statement (if that is not returned)
112 0     0   0 my $rets = {
113 0         0 'count' => sub {my @a = $sth->fetchrow_array();$sth->finish();$a[0];},
  0         0  
114 0     0   0 'sth' => sub {return($sth);},
115 0     0   0 'hash' => sub {my $h = $sth->fetchrow_hashref();$sth->finish();$h;},
  0         0  
  0         0  
116 0     0   0 'aoh' => sub {my $arr = $sth->fetchall_arrayref({});$sth->finish();$arr;},
  0         0  
  0         0  
117 0         0 };
118 0 0       0 if (!$dbh) {$p->{'errstr'} = "No Connection !";return(0);}
  0         0  
  0         0  
119 0 0       0 if ($p->{'debug'}) {print("Full Q: $q\n");}
  0         0  
120             # Prepare cached ?
121 0         0 $sth = $dbh->prepare($q);
122 0 0       0 if (!$sth) {die("Query ($q) Not prepared (".$dbh->errstr().")\n");}
  0         0  
123 0         0 my $ok = $sth->execute(@$vals);
124 0 0       0 if (!$ok) {die("Failed to execute ".$sth->errstr()."");}
  0         0  
125             # Special return processing
126 0 0       0 if (my $rcb = $rets->{$rett}) {
127             #print("Special return by $rett ($rcb)\n");
128 0         0 return($rcb->());
129             }
130             # Done with statement
131             DWS:
132 0         0 $sth->finish();
133 0         0 return($ok);
134             }
135              
136             ###################################################
137              
138             # Store entry %$e (hash) inserting it as a new entry to a database.
139             # Connection has been passed previously in construction of persister.
140             # The table / schema to store to is either the one passed at
141             # construction or derived from perl "blessing" of entry ($e).
142             # Returns (ref to) an array of ID values for the entry that got stored (array
143             # of one element for numeric primary key, multiple for composite key).
144             sub insert {
145 2     2 1 347 my ($p, $e) = @_;
146             # No enforced internal validation
147 2 50       4 eval {$p->validate();};if ($@) {$p->{'errstr'} = $@;return(1);}
  2         9  
  2         8  
  0         0  
  0         0  
148 2 50       11 if (reftype($e) ne 'HASH') {$p->errstr("Entry need to be HASH");return(2);}
  0         0  
  0         0  
149             # Possibly also test for references (ds branching ?) eliminating them too
150 2         9 my @ea = sort (keys(%$e));
151 2         9 my @ev = @$e{@ea}; # map()
152            
153             # Sequence - Add sequenced ID allocation ???
154             # $p->{'seqname'}
155 2 50 33     9 if ($p->{'autoid'} && ($p->{'autoid'} eq 'seq')) {
156 0         0 my $bkt = 'Oracle';
157 0         0 my @pka = pkeys($p);
158 0 0       0 if (@pka > 1) {die("Multiple pkeys for sequenced ID");}
  0         0  
159             # Add Sequence id attibute AND sequence call (unshift to front ?)
160             #
161 0         0 push(@ea, @pka); # $p->{'pkey'}->[0]
162 0         0 push(@ev, sprintf("$bkmeta->{$bkt}->{'sv'}", $p->{'seqname'}) ); #
163             #DEBUG:print("FMT: $bkmeta->{$bkt}->{'sv'} / $p->{'seqname'}\n");
164             }
165 6         24 my $qp = "INSERT INTO $p->{'table'} (".join(',',@ea).") ".
166 2         12 "VALUES (".join(',', map({'?';} @ea)).")";
167 2 50       7 if (!$p->{'dbh'}) {return($qp);}
  2         17  
168 0         0 my $okid = $p->qexecute($qp, \@ev);
169            
170             # Auto-id - either UTO_INC style or Sequence (works for seq. too ?
171 0 0       0 if ($p->{'autoid'}) {
172 0         0 my @pka = pkeys($p);
173 0 0       0 if (@pka != 1) {die(scalar(@pka)." Keys for Autoid");}
  0         0  
174 0         0 my $id = $p->fetchautoid();
175             #$e->{$pka[0]} = $id;
176 0         0 return(($id));
177             }
178             # Seq ?
179             #elsif () {}
180             else {
181 0         0 my @pka = pkeys($p);
182 0         0 return(@$e{@pka});
183             }
184             }
185              
186             # Update an existing entry in the database with values in %$e (hash).
187             # Provide protection for AUTO-ID (to not be changed) ?
188             # For flexibility the $idvals may be hash or array (reference) with
189             # hash containing (all) id keys and id values or alternatively array
190             # containing id values IN THE SAME ORDER as keys were passed during
191             # construction (with idattr/pkey parameter).
192             sub update {
193 2     2 1 11 my ($p, $e, $idvals) = @_;
194 2         4 my @pka; # To be visible to closure
195             # Extract ID Values from hash OR array
196 0     0   0 my $idvgens = {
197             'HASH' => sub {@$idvals{@pka};},
198 2     2   7 'ARRAY' => sub {return(@$idvals);},
199             #'' => sub {[$idvals];}
200 2         19 };
201             # No mandatory (internal) validation ?
202             #eval {$p->validate();};if ($@) {$p->{'errstr'} = $@;return(1);}
203 2         5 @pka = pkeys($p);
204 2 50       11 if (reftype($e) ne 'HASH') {$p->{'errstr'} = "Entry need to be hash";return(2);}
  0         0  
  0         0  
205             # Probe the type of $idvals
206 2         6 my $idrt = reftype($idvals);
207 2 50       7 if ($p->{'debug'}) {print("Got IDs:".Dumper($idvals)." as $idrt\n");}
  0         0  
208             #my @idv;
209 2         3 my @pkv;
210 2 50       23 if (my $idg = $idvgens->{$idrt}) {@pkv = $idg->();}
  2         6  
  0         0  
211             #if ($idrt ne 'HASH') {$p->{'errstr'} = "ID needs to be hash";return(3);}
212             else {die("Need IDs as HASH or ARRAY (reference, got '$idrt')");}
213             #my ($cnt_a, $cnt_v) = (scalar(@pka), scalar(@pkv));
214 2 50       8 if (@pkv != @pka) {die("Number of ID keys and values not matching for update");}
  0         0  
215 2         9 my @ea = sort(keys(%$e));
216             #my @pkv = @$idh{@pka}; # $idvals, Does not work for hash
217            
218 2 50       9 if (my @badid = $p->invalidids(@pkv)) {$p->{'errstr'} = "Bad ID Values found (@badid)";return(4);}
  0         0  
  0         0  
219 2         16 my $widstr = whereid($p);
220             # Persistent object type
221 2         10 my $pot = $p->{'table'};
222 2 50       6 if (!$pot) {die("No table for update");}
  0         0  
223 2         7 my $qp = "UPDATE $pot SET ".join(',', map({" $_ = ?";} @ea)).
  6         25  
224             " WHERE $widstr";
225 2 50       7 if (!$p->{'dbh'}) {return($qp);}
  2         18  
226             # Combine Entry attr values and primary key values
227 0         0 my $allv = [@$e{@ea}, @pkv];
228 0         0 $p->qexecute($qp, $allv);
229             }
230              
231             # Delete an entry from database by passing $e as one of the following
232             # - hash %$e - a hash containing (all) primary key(s) and their values.
233             # - scalar $e - Entry ID for entry to be deleted
234             # - array @$e - One or many primary key values for entry to be deleted
235             # The recommended use is caae "array" as it is most versatile and most
236             # consistent with other API methods.
237             sub delete {
238 2     2 1 10 my ($p, $e) = @_;
239             #if (!ref($p->{'pkey'})) {die("PKA Not Known");}
240             #eval {$p->validate();};if ($@) {$p->{'errstr'} = $@;return(1);}
241             #my @pka = @{$p->{'pkey'}};
242 2         4 my @pka = pkeys($p);
243 2 50       7 if (!$e) {die("Must have Identifier for delete()\n");}
  0         0  
244            
245 2         6 my $rt = reftype($e);
246 2         7 my $pkc = $p->pkeycnt();
247 2         3 my @pkv;
248             # $e Scalar, must have 1 pkey
249 2 50 33     5368 if (!$rt && ($pkc == 1)) {@pkv = $e;}
  0 50 0     0  
  2 0       9  
250             # Hash - extract primary keys
251 0         0 elsif ($rt eq 'HASH') {@pkv = @$e{@pka};}
252             # Array (of pk values) - check count matches
253 0         0 elsif (($rt eq 'ARRAY') && ($pkc == scalar(@$e))) {@pkv = @$e;}
254             else {die("No way to delete (without HASH or ARRAY for IDs)\n");}
255             #NOTNEEDED:#my %pkh;@pkh{@pka} = @pkv;
256             #my $wstr = join(' AND ', map({"$_ = ?";} @pka));
257 2         6 my $wstr = whereid($p);
258 2         8 my $qp = "DELETE FROM $p->{'table'} WHERE $wstr";
259 2 50       9 if (!$p->{'dbh'}) {return($qp);}
  2         11  
260 0         0 $p->qexecute($qp, \@pkv);
261             }
262             #my $dbh = $p->{'dbh'};
263             #my $sth = $dbh->prepare($qp);
264             #if (!$sth) {print("Not prepared\n");}
265             #$sth->execute(@pkv);
266              
267             # Test if an entry exists in the DB table with ID values passed in @$ids (array).
268             # Returns 1 (entry exists) or 0 (does not exist) under normal conditions.
269             sub exists {
270 2     2 1 12 my ($p, $ids) = @_;
271 2 50       8 my $whereid = $p->{'where'} ? $p->{'where'} : whereid($p);
272 2         6 my $qp = "SELECT COUNT(*) FROM $p->{'table'} WHERE $whereid";
273 2 50       9 if (!$p->{'dbh'}) {return($qp);}
  2         7  
274 0         0 $p->qexecute($qp, $ids, 'count');
275             }
276              
277             # Load entry from DB table by its IDs passed in @$ids (array,
278             # single id typical sequece autoid pkey, multiple for composite primary key).
279             # Entry will be loaded from single table passed at construction
280             # (never as result of join from multiple tables).
281             # Return entry as a hash (ref).
282             sub load {
283 2     2 1 11 my ($p, $ids) = @_;
284 2 50       6 my $whereid = $p->{'where'} ? $p->{'where'} : whereid($p);
285 2         7 my $qp = "SELECT * FROM $p->{'table'} WHERE $whereid";
286 2 50       7 if (!$p->{'dbh'}) {return($qp);}
  2         6  
287 0         0 $p->qexecute($qp, $ids, 'hash');
288             }
289              
290             # Load a set of Entries from persistent storage.
291             # Optionally provide simple "where filter hash" ($h), whose key-value criteria
292             # is ANDed together to form the filter.
293             # Return set / collection of entries as array of hashes.
294             sub loadset {
295 0     0 1 0 my ($p, $h, $sort) = @_; # filter, sortby
296 0         0 my $w = '';
297             # if (@_ = 2 && ref($_[1]) eq 'HASH') {}
298 0 0       0 if ($h) {
299 0         0 my $wf = wherefilter($h);
300 0 0       0 if (!$wf) {die("Empty Filter !");}
  0         0  
301 0         0 $w = " WHERE $wf";
302             }
303 0 0       0 if ($p->{'debug'}) {print("Loading set by '$w'\n");}
  0         0  
304 0         0 my $qp = "SELECT * FROM $p->{'table'} $w";
305 0         0 $p->qexecute($qp, undef, 'aoh');
306             }
307              
308             # Sample Column names from (current) DB table.
309             # Return (ref to) array with field names in it.
310             sub cols {
311 0     0 1 0 my ($p) = @_;
312 0         0 my $qp = "SELECT * FROM $p->{'table'} WHERE 1 = 0";
313 0         0 my $sth = $p->qexecute($qp, undef, 'sth');
314 0         0 my $cols = $sth->{'NAME'};
315 0 0       0 if (@_ == 1) {$sth->finish();return($cols);}
  0         0  
  0         0  
316             #elsif (@_ == 2) {$rett = $_[1];};
317             #if ($rett ne 'meta') {return(undef);}
318 0         0 return(undef);
319             }
320              
321             # TODO: Load "tree" of entries rooted at an entry / entries (?)
322             # Returns a set (array) of entries or single (root entry if
323             # option $c{'fsingle'} - force single - is set.
324             sub loadtree {
325 0     0 0 0 my ($p, %c) = @_;
326 0         0 my $chts = $c{'ctypes'};
327 0         0 my $w = $c{'w'};
328 0         0 my $fsingle = $c{'fsingle'}; # singleroot, uniroot
329 0         0 my $arr = loadset($p, $w);
330 0         0 for my $e (@$arr) {my $err = loadchildern($p, $e, %c);}
  0         0  
331             # Choose return type
332 0 0       0 if ($fsingle) {return($arr->[0]);}
  0         0  
333 0         0 return($arr);
334             }
335              
336             # TODO: Load Instances of child object types for entry.
337             # Child types are defined in 'ctypes' array(ref) in options.
338             # Array 'ctypes' may be one of the following
339             # - Plain child type names (array of scalars), the rest is guessed
340             # - Array of child type definition hashes with hashes defining following:
341             # - table - The table / objectspace of child type
342             # - parkey - Parent id field in child ("foreign key" field in rel DBs)
343             # - memname - Mamber name to place the child collection into in parent entry
344             # - Array of arrays with inner arrays containing 'table','parkey','memname' in
345             # that order(!), (see above for meanings)
346             # Return 0 for no errors
347             sub loadchildren {
348 0     0 0 0 my ($p, $e, %c) = @_;
349 0         0 my $chts = $c{'ctypes'};
350 0 0       0 if (!$chts) {die("No Child types indicated");}
  0         0  
351 0 0       0 if (ref($chts) ne 'ARRAY') {die("Child types not ARRAY");}
  0         0  
352 0         0 my @ids = pkeyvals($p, $e);
353 0 0       0 if (@ids > 1) {die("Loading not supported for composite keys");}
  0         0  
354 0         0 my $dbh = $p->{'dbh'};
355 0         0 my $debug = $p->{'debug'};
356 0         0 for (@$chts) {
357             #my $ct = $_;
358 0         0 my $cfilter;
359             # Use or create a complete hash ?
360 0         0 my $cinfo = makecinfo($p, $_);
361 0 0       0 if ($debug) {print(Dumper($cinfo));}
  0         0  
362             # Load type by created filter
363 0         0 my ($ct, $park, $memn) = @$cinfo{'table','parkey','memname',};
364 0 0       0 if (!$park) {}
365             # Create where by parkey info
366             #$cfilter = {$park => $ids[0]}; # What is par key - assume same as parent
367 0 0       0 if (@$park != @ids) {die("Par and child key counts mismatch");}
  0         0  
368 0         0 @$cfilter{@$park} = @ids;
369             #my $cfilter =
370             # Take a shortcut by not providing pkey
371 0         0 my $shc = StoredHash->new('table' => $ct, 'pkey' => [],
372             'dbh' => $dbh, 'loose' => 1, 'debug' => $debug);
373 0         0 my $carr = $shc->loadset($cfilter);
374 0 0 0     0 if (!$carr || !@$carr) {next;}
  0         0  
375             #if ($debug) {print("Got Children".Dumper($arr));}
376 0         0 $e->{$memn} = $carr;
377             # Blessing
378 0 0       0 if (my $bto = $cinfo->{'blessto'}) {map({bless($_, $bto);} @$carr);}
  0         0  
  0         0  
379             # Circular Ref from child to parent ?
380             #if (my $pla = $cinfo->{'parlinkattr'}) {map({$_->{$pla} = $e;} @$carr);}
381             }
382             # Autobless Children ?
383 0         0 return(0);
384             }
385             # Internal method for using or making up Child relationship information
386             # for loading related entities.
387             sub makecinfo {
388 0     0 0 0 my ($p, $cv) = @_;
389             # Support array with: 'table','parkey','memname'
390 0 0       0 if (ref($cv) eq 'ARRAY') {
    0          
    0          
391 0         0 my $cinfo;
392 0 0       0 if (@$cv != 3) {die("Need table, parkey, memname in array");}
  0         0  
393 0         0 @$cinfo{'table','parkey','memname'} = @$cv;
394 0         0 return($cinfo);
395             }
396             # Assume all is there (could validate and provide missing)
397 0         0 elsif (ref($cv) eq 'HASH') {
398 0         0 my @a = ('table','parkey','memname');
399             # Try guess parkey ?
400 0 0       0 if (!$cv->{'parkey'}) {$cv->{'parkey'} = [pkeys($p)];}
  0         0  
401 0 0       0 for (@a) {if (!$cv->{$_}) {die("Missing '$_' in cinfo");}}
  0         0  
  0         0  
402 0         0 return($cv);
403             }
404             elsif (ref($cv) ne '') {die("child type Not scalar (or hash)");}
405             ################## Make up
406 0         0 my $ctab = $cv;
407 0         0 my $memname = $ctab; # Default memname to child type name (Plus 's') ?
408             # Guess by parent
409 0         0 my $parkey = [pkeys($p)];
410 0         0 my $cinfo = {'table' => $ctab, 'parkey' => $parkey, 'memname' => $ctab,};
411 0         0 return($cinfo);
412             }
413             ###################################################################
414              
415             # Internal Persister validator for the absolutely mandatory properties of
416             # persister object itself.
417             # Doesn't not validate entry
418             sub validate {
419 2     2 0 3 my ($p) = @_;
420 2 50       11 if (ref($p->{'pkey'}) ne 'ARRAY') {die("PK Attributes Not Known\n");}
  0         0  
421             # Allow table to come from blessing (so NOT required)
422             #if (!$p->{'table'}) {die("No Table\n");}
423 2 50       18 if ($p->{'simu'}) {return;}
  0         0  
424             # Do NOT Require conenction
425             #if (!ref($p->{'dbh'})) {die("NO dbh to act on\n");} # ne 'DBI'
426            
427             }
428              
429             # Internal method for returning array of id keys (Real array, not ref).
430             sub pkeys {
431 13     13 0 20 my ($p) = @_;
432 13         39 my $prt = reftype($p);
433 13 50       38 if ($prt ne 'HASH') {
434 0         0 $|=1;
435 0         0 print STDERR Dumper([caller(1)]);
436 0         0 die("StoredHash Not a HASH (is '$p'/'$prt')");
437             }
438 13 50       40 if (ref($p->{'pkey'}) ne 'ARRAY') {die("Primary keys not in an array");}
  0         0  
439             #return($p->{'pkey'});
440 13         17 return(@{$p->{'pkey'}});
  13         48  
441             }
442              
443             # Return Primary key values (as real array) from hash %$e passed as parameter.
444             # undef values are produced for non-existing keys.
445             # Mostly used for internal operations (and maybe debugging).
446             sub pkeyvals {
447 0     0 1 0 my ($p, $e) = @_;
448 0         0 my @pkeys = pkeys($p);
449 0         0 @$e{@pkeys};
450             }
451              
452             # TODO: Implement pulling last id from sequence
453             sub fetchautoid {
454 0     0 0 0 my ($p) = @_;
455 0         0 my $dbh;
456             #$dbh->{'Driver'}; # Need to test ?
457             #DEV:print("AUTOID FETCH TO BE IMPLEMENTED\n");return(69);
458 0         0 my $pot = $p->{'table'};
459 0 0       0 if (!$pot) {die("No table for fetching auto-ID");}
  0         0  
460 0 0       0 if (!($dbh = $p->{'dbh'})) {die("No Connection for fetching ID");}
  0         0  
461 0         0 $dbh->last_insert_id(undef, undef, $pot, undef);
462             }
463              
464             sub pkeycnt {
465 2     2 0 4 my ($p) = @_;
466             #if (ref($p->{'pkey'}) ne 'ARRAY') {die("Primary keys not in an array");}
467             #scalar(@{$p->{'pkey'}});
468 2         5 my @pkeys = pkeys($p);
469 2         6 scalar(@pkeys);
470             }
471              
472             # Internal method for checking for empty or undefined ID values.
473             # In all reasonable databases and apps these are not valid values.
474             sub invalidids {
475 2     2 0 4 my ($p, @idv) = @_;
476 2 50       5 my @badid = grep({!defined($_) || $_ eq '';} @idv);
  3         23  
477 2         9 return(@badid);
478             }
479             # Generate SQL WHERE Clause for UPDATE based on primary keys of current type.
480             # Return WHERE clause with id-attribute(s) and placeholder(s) (idkey = ?, ...), without the WHERE keyword.
481             sub whereid {
482 7     7 1 13 my ($p) = @_;
483             # # Allow IDs to be hash OR array ?? Not because hash would req. to store order
484 7         22 my @pka = pkeys($p);
485 7 50       21 if (@pka < 1) {die("No Pkeys to create where ID clause");}
  0         0  
486             # my $wstr =
487 7         13 return join(' AND ', map({"$_ = ?";} @pka));
  10         43  
488             }
489              
490             sub sqlvalesc {
491 0     0 0 0 my ($v) = @_;
492 0         0 $v =~ s/'/\\'/g;
493 0         0 $v;
494             }
495              
496             # TODO: Create list for WHERE IN Clause based on some assumptions
497             sub invalues {
498 0     0 0 0 my ($vals) = @_;
499             # Assume array ref validated outside
500 0 0       0 if (ref($vals) eq 'ARRAY') {die("Not an array for invals");}
  0         0  
501             # Escape within Quotes ?
502 0         0 join(',', map({
503 0 0       0 if (/^\d+$/) {$_;}
  0         0  
504             else {
505 0         0 my $v = sqlvalesc($_);
506 0         0 "'$v'";
507             }
508             } @$vals));
509             }
510              
511             sub rangefilter {
512 0     0 0 0 my ($attr, $v) = @_;
513             # Or just even and sort, grab 2 at the time ?
514 0 0       0 if (@$v != 2) {die("Range cannot be formed");}
  0         0  
515             # Auto-arrange ???
516             #if ($v->[1] < $v->[0]) {$v = [$v->[1],$v->[0]];}
517             # Detect need to escape (time vs. number)
518             #"($attr >= $v->[0]) AND ($attr <= $v->[0])";
519             }
520              
521             # Generate simple WHERE filter by hash %$e. The keys are assumed to be attributes
522             # of DB and values are embedded as values into SQL (as opposed to using placeholers).
523             # To be perfect in escaping per attribute type info would be needed.
524             # For now we do best effort heuristics (attr val \d+ is assumed
525             # to be a numeric field in SQL, however 000002345 could actually
526             # be content of a char/text/varchar field).
527             # Return WHERE filter clause without WHERE keyword.
528             sub wherefilter {
529 0     0 1 0 my ($e, %c) = @_;
530 0         0 my $w = '';
531 0         0 my $fop = ' AND ';
532             #my $rnga = $c{'rnga'}; # Range attributes
533 0 0       0 if (ref($e) ne 'HASH') {die("No hash for filter generation");}
  0         0  
534 0         0 my @keys = sort keys(%$e);
535 0         0 my @qc;
536             # Assume hard values, treat everything as string (?)
537             # TODO: forcestr ?
538 0         0 @qc = map({
539 0         0 my $v = $e->{$_};
540             #my $rv = ref($v);
541             #if ($rnga->{$_} && ($rv eq 'ARRAY') && (@$v == 2)) {rangefilter($_, $v);}
542 0 0       0 if (ref($v) eq 'ARRAY') {" $_ IN (".invalues($v).") ";}
  0 0       0  
  0 0       0  
543             # SQL Wildcard
544 0         0 elsif ($v =~ /%/) {"$_ LIKE '$v'";}
545             # Detect numeric (likely numeric, not perfect)
546 0         0 elsif ($v =~ /^\d+$/) {"$_ = $v";}
547             # Assume string
548             else {"$_ = '".sqlvalesc($v)."'";}
549            
550             } @keys);
551 0         0 return(join($fop, @qc));
552             }
553              
554             # Internal: Serialize all values (singles,multi) from a hash to an array
555             # based on sorted key order. Multi-valued keys (with value being array reference)
556             # add multiple items.
557             sub allentvals {
558 2     2 0 19 my ($h) = @_;
559 0         0 map({
560 2 50       14 if (ref($h->{$_}) eq 'HASH') {();}
  6 50       24  
  0         0  
561 0         0 elsif (ref($h->{$_}) eq 'ARRAY') {@{$h->{$_}};}
  6         19  
562             else {($h->{$_});}
563             } sort(keys(%$h)));
564             }
565             1;