File Coverage

blib/lib/CAM/SQLManager.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package CAM::SQLManager;
2              
3             =head1 NAME
4              
5             CAM::SQLManager - Encapsulated SQL statements in XML
6              
7             =head1 LICENSE
8              
9             Copyright 2005 Clotho Advanced Media, Inc.,
10              
11             This library is free software; you can redistribute it and/or modify it
12             under the same terms as Perl itself.
13              
14             =head1 SYNOPSIS
15              
16             We recommend that you do NOT use CAM::SQLManager directly, but instead
17             use the more convenient wrapper, CAM::SQLObject. In that case, you
18             can skip to the bottom to learn about the XML files. If you do choose
19             to use this directly, here's how it goes:
20              
21             use CAM::SQLManager;
22             use DBI;
23            
24             my $dbh = DBI->connect(blah blah);
25            
26             CAM::SQLManager->setDBH($dbh);
27             CAM::SQLManager->setDirectory("/var/www/sqlcmds");
28            
29             my $sql1 = CAM::SQLManager->new("user.xml");
30             my $sth = $sql1->query("search", username => "chris");
31            
32             my $dbh2 = DBI->connect(blah blah);
33             my $sql2 = CAM::SQLManager->new(-dbh => $dbh2, -cmd => "product.xml",
34             -dir => "/usr/share/sqlcmds");
35             my $result = $sql2->do("add", name => "vase", color => "red", price => "50.00");
36            
37             my $sql3 = CAM::SQLManager->new("product.xml");
38             my @towels = $sql3->retrieveObjects("search", "ACME::Towel", [], prodtype => "%towel%");
39            
40             my $sql4 = CAM::SQLManager->new("product.xml");
41             my $towel = ACME::Towel->new();
42             [... fill/edit object ...]
43             $sql4->storeObject("insert", $towel);
44              
45             Use these commands for testing the various SQL queries in a CGI script:
46              
47             CAM::SQLManager->setDirectory("/var/www/sqlcmds");
48             CAM::SQLManager->setDBH($dbh);
49             CAM::SQLManager->setBenchmarking(1); # optional
50             CAM::SQLManager->testCommands();
51              
52             =head1 DESCRIPTION
53              
54             This package implements SQL templates. This allows the programmer to
55             increase the separation between the SQL RDBMS and the Perl programming
56             logic of any project. This package has features that make it
57             particularly useful in a web environment, as it is quite easy to write
58             a CGI program to allow testing and evalutation of the SQL templates.
59              
60             =head1 PORTING
61              
62             As of v1.12, we have added support for non-Unix file systems via
63             File::Spec. This is intended to enable Win32 usage of this module.
64             As of v1.13, this is pretty well tested in production by the authors,
65             so we think it should work fine for you.
66              
67             =cut
68              
69             require 5.005_62;
70 1     1   45417 use strict;
  1         2  
  1         42  
71 1     1   6 use warnings;
  1         1  
  1         34  
72 1     1   6 use Carp;
  1         1  
  1         175  
73 1     1   6 use File::Spec;
  1         12  
  1         114  
74 1     1   499 use CAM::XML;
  0            
  0            
75              
76             our @ISA = qw();
77             our $VERSION = '1.13';
78              
79             our $global_directory = "";
80             our $global_dbh = undef;
81             our @global_extensions = (".xml");
82             our %global_cache = ();
83             our $global_benchmarking = 0; # boolean: should we time the SQL queries?
84             our $global_safe_functions = 1; # boolean: faster if false, but may die()
85             # if true, eval{} is used
86              
87             our $errstr = undef; # (rarely) used to pass error messages
88              
89             our %global_stats;
90             &_clearStats();
91             sub _clearStats
92             {
93             # Any changes to this data structure should be propagated into
94             # _incrStats() and the documentation for statistics()
95             %global_stats = (
96             queries => 0,
97             time => 0,
98             cmds => {},
99             );
100             }
101              
102             #------------------
103              
104             =head1 FUNCTIONS
105              
106             =over 4
107              
108             =cut
109              
110             #------------------
111              
112             =item new [CMD,] [ARG => VALUE, ...]
113              
114             Open and read an SQL template. Possible arguments (with example
115             values) are:
116              
117             -cmd => "user.xml"
118             -dir => "/some/sql/template/dir"
119             -dbh => $dbh (should be a DBI object)
120              
121             if -dir or -dbh are not specified, the global values are used (set by
122             setDirectory and setDBH below).
123              
124             The file /.xml should exist.
125              
126             =cut
127              
128             sub new
129             {
130             my $pkg = shift;
131              
132             my $self = bless({
133             # user settable parameters:
134             cmd => "",
135             dbh => undef,
136             dir => $global_directory,
137              
138             # Internal parameters:
139             filename => "",
140             filetime => 0,
141             tableName => "",
142             keyName => "",
143             queries => {},
144             defaultquery => undef,
145             }, $pkg);
146            
147             # pick up default arguments, if any
148             $self->{cmd} = shift if (@_ > 0 && $_[0] !~ /^\-[a-z]+$/);
149              
150             # process switched arguments
151             while (@_ > 0 && $_[0] =~ /^\-[a-z]+$/)
152             {
153             my $key = shift;
154             my $value = shift;
155             $key =~ s/^\-//;
156             $self->{$key} = $value;
157             }
158              
159             if (@_ > 0)
160             {
161             &carp("Too many arguments");
162             return undef;
163             }
164              
165             # Validate "dbh"
166             if (!$self->getDBH()) {
167             &carp("The DBH object is undefined");
168             return undef;
169             }
170             if (ref($self->getDBH()) !~ /^DBI\b/ && ref($self->getDBH()) !~ /^DBD\b/) {
171             &carp("The DBH object is not a valid DBI/DBD connection: " . ref($self->getDBH()));
172             return undef;
173             }
174              
175             # Validate "cmd"
176             if ($self->{cmd} !~ /^(\w+[\/\\])*\w+(|\.\w+)$/)
177             {
178             &carp("Command keyword is not alphanumeric: $$self{cmd}");
179             return undef;
180             }
181              
182             # Use "dir" and "cmd" to get the SQL template
183             $self->{filename} = File::Spec->catfile($self->{dir}, $self->{cmd});
184             local *FILE;
185             if (!open(FILE, $self->{filename}))
186             {
187             &carp("Cannot open sql command '$$self{filename}': $!");
188             return undef;
189             }
190             local $/ = undef;
191             $self->{sql} = ;
192             close(FILE);
193              
194             # Record the last-mod time of the file so we can notice if it changes
195             $self->{filetime} = (stat($self->{filename}))[9];
196              
197             # Set up the statistics data structures
198             if (!exists $global_stats{cmds}->{$self->{cmd}})
199             {
200             # Any changes to this data structure should be propagated into
201             # _incrStats() and the documentation for statistics()
202             $global_stats{cmds}->{$self->{cmd}} = {
203             queries => 0,
204             time => 0,
205             query => {},
206             };
207             }
208              
209             my $struct = CAM::XML->parse($self->{sql});
210              
211             if ((!$struct) || $struct->{name} ne "sqlxml")
212             {
213             &carp("XML parsing of the SQL query failed");
214             return undef;
215             }
216              
217             # Read the table data
218             my ($tabledata) = $struct->getNodes(-path => "/sqlxml/table");
219             if ($tabledata)
220             {
221             if ($tabledata->getAttribute("name"))
222             {
223             $self->{tableName} = $tabledata->getAttribute("name");
224             }
225             if ($tabledata->getAttribute("primarykey"))
226             {
227             $self->{keyName} = $tabledata->getAttribute("primarykey");
228             }
229             }
230              
231             # Extract all of the queries
232             my @queries = $struct->getNodes(-path => "/sqlxml/query");
233             if (@queries < 1)
234             {
235             &carp("There are no query tags in $$self{filename}");
236             return undef;
237             }
238            
239             foreach my $query (@queries)
240             {
241             my $name = $query->getAttribute("name");
242             $name = "_default" if (!$name);
243             if (exists $self->{queries}->{$name})
244             {
245             &carp("Multiple queries named $name in $$self{filename}");
246             return undef;
247             }
248            
249             # Throw away whitespace elements in the query body
250             my $queryarray = [grep({$_->isa("CAM::XML") || $_->{text} =~ /\S/} $query->getChildren())];
251            
252             $self->{queries}->{$name} = $queryarray;
253             if ((!$self->{defaultquery}) || $name eq "_default")
254             {
255             $self->{defaultquery} = $queryarray;
256             }
257             }
258              
259             # Set up statistics data structure
260             foreach my $queryname ("retrieveByKey", keys %{$self->{queries}})
261             {
262             # Any changes to this data structure should be propagated into
263             # _incrStats() and the documentation for statistics()
264             $global_stats{cmds}->{$self->{cmd}}->{query}->{$queryname} = {
265             queries => 0,
266             time => 0,
267             };
268             }
269              
270             return $self;
271             }
272              
273              
274             #------------------
275              
276             =item getMgr CMD, CMD, ...
277              
278             =item getMgr -dbh => DBH, CMD, CMD, ...
279              
280             Like new() above, but caches the manager objects for later
281             re-requests. Unlike new(), the database handle and SQL file directory
282             must already be set. Use this function like:
283              
284             CAM::SQLManager->getMgr("foo.xml");
285              
286             If more than one command is specified, the first one that results in a
287             real file is used.
288              
289             =cut
290              
291             sub getMgr
292             {
293             my $pkg = shift;
294             my @args = ();
295             if ($_[0] && $_[0] eq "-dbh")
296             {
297             push @args, shift, shift;
298             }
299             my @cmds = (@_);
300              
301             foreach my $cmd (@cmds)
302             {
303             if (-e File::Spec->catfile($global_directory, $cmd))
304             {
305             if (exists $global_cache{$cmd})
306             {
307             # Check to make sure the SQL file has not changed
308             if ($global_cache{$cmd}->{filetime} < (stat($global_cache{$cmd}->{filename}))[9])
309             {
310             $global_cache{$cmd} = $pkg->new($cmd, @args);
311             }
312             }
313             else
314             {
315             $global_cache{$cmd} = $pkg->new($cmd, @args);
316             }
317             return $global_cache{$cmd};
318             }
319             }
320             return undef;
321             }
322             #------------------
323              
324             =item getAllCmds
325              
326             Search the SQL directory for all command files. This is mostly just
327             useful for the testCommands() method.
328              
329             =cut
330              
331             sub getAllCmds
332             {
333             my $pkg = shift;
334              
335             my @files;
336             my $regex = join("|", map {quotemeta} @global_extensions);
337             my @dirs = ($global_directory);
338             my %seendirs;
339             while (@dirs > 0)
340             {
341             local *DIR;
342             my $dir = shift @dirs;
343             next if ($seendirs{$dir}++);
344            
345             if (!opendir(DIR, $dir))
346             {
347             if ($dir eq $global_directory)
348             {
349             &carp("Failed to read the SQL library directory '$dir': $!");
350             return ();
351             }
352             }
353             else
354             {
355             my @entries = readdir(DIR);
356             closedir(DIR);
357            
358             @entries = map {File::Spec->catfile($dir, $_)} grep !/^\.\.?$/, @entries;
359             push @files, grep /($regex)$/, @entries;
360             push @dirs, grep {-d $_} @entries;
361             }
362             }
363             return @files;
364             }
365             #------------------
366              
367             =item setDirectory DIRECTORY
368              
369             Set the global directory for this package. Use like this:
370              
371             CAM::SQLManager->setDirectory("/var/lib/sql");
372              
373             =cut
374              
375             sub setDirectory
376             {
377             my $pkg = shift; # unused
378             my $val = shift;
379              
380             $global_directory = $val;
381             return $pkg;
382             }
383             #------------------
384              
385             =item setDBH DBI-OBJECT
386              
387             As a class method, this sets the global database handle for this
388             package. Use like this:
389              
390             CAM::SQLManager->setDBH($dbh);
391              
392             As an object method, this sets the database handle for just that
393             instance.
394              
395             =cut
396              
397             sub setDBH
398             {
399             my $pkg_or_self = shift;
400             my $val = shift;
401              
402             if (ref($pkg_or_self))
403             {
404             my $self = $pkg_or_self;
405             $self->{dbh} = $val;
406             }
407             else
408             {
409             $global_dbh = $val;
410             }
411             return $pkg_or_self;
412             }
413             #------------------
414              
415             =item getDBH
416              
417             Get the current database handle. If a handle is not specifically set for an instance, the global database handle is returned.
418              
419             =cut
420              
421             sub getDBH
422             {
423             my $pkg_or_self = shift;
424              
425             my $dbh;
426              
427             if (ref($pkg_or_self))
428             {
429             my $self = $pkg_or_self;
430             $dbh = $self->{dbh};
431             }
432             $dbh ||= $global_dbh;
433             return $dbh;
434             }
435             #------------------
436              
437             =item setBenchmarking 0|1
438              
439             Specify whether to benchmark the SQL queries. The default is 0 (false). To retrieve the benchmarking data, use the statistics() method. Use like this:
440              
441             CAM::SQLManager->setBenchmarking(1);
442              
443             =cut
444              
445             sub setBenchmarking
446             {
447             my $pkg = shift; # unused
448             my $val = shift;
449              
450             $global_benchmarking = $val;
451              
452             if ($global_benchmarking) {
453             eval "use Time::HiRes";
454             if ($@)
455             {
456             &carp("Failed to load the Time::HiRes package, needed for benchmarking");
457             $global_benchmarking = 0;
458             }
459             }
460              
461             # Reset
462             &_clearStats();
463              
464             return $pkg;
465             }
466             #------------------
467              
468             =item validateXML
469              
470             Warning: this function relies on XML::Xerces. If XML::Xerces is not
471             installed, this routine will always indicate that the document is
472             invalid.
473              
474             Test the integrity of the XML encapsulation of the SQL statement(s).
475             Returns true of false to indicate success or failure. On failure, it
476             sets $CAM::SQLManager::errstr with an error description. Succeeds
477             automatically on a non-XML SQL file.
478              
479             =cut
480              
481             sub validateXML
482             {
483             my $self = shift;
484              
485             $errstr = undef;
486              
487             if (!$XML::Xerces::VERSION)
488             {
489             #print "loading XML::Xerces...
\n";
490             local $^W = 0;
491             no warnings;
492             # Just in case some version of Carp is in effect
493             local $SIG{__WARN__} = 'default';
494             local $SIG{__DIE__} = 'default';
495             eval('require XML::Xerces;' .
496             'require XML::Xerces::DOMParse if ($XML::Xerces::VERSION lt "2");');
497              
498             if ($@)
499             {
500             $errstr = "Failed to load XML::Xerces for the validation test";
501             return undef;
502             }
503            
504             &XML::Xerces::XMLPlatformUtils::Initialize();
505             }
506            
507             if ($XML::Xerces::VERSION lt "2")
508             {
509             my $valflag = $XML::Xerces::DOMParser::Val_Auto;
510            
511             my $parser = XML::Xerces::DOMParser->new();
512             $parser->setValidationScheme($valflag);
513             $parser->setDoNamespaces(1);
514             $parser->setCreateEntityReferenceNodes(1);
515             $parser->setDoSchema(1);
516            
517             my $ERROR_HANDLER = XML::Xerces::PerlErrorHandler->new();
518             $parser->setErrorHandler($ERROR_HANDLER);
519             eval {
520             # HACK: I don't understand this, but XML::Xerces doesn't like
521             # this variable unless it's been detainted through a regex.
522             $self->{filename} =~ /(.+)/ || die "No file specified";
523             my $filename = $1;
524            
525             $parser->parse(XML::Xerces::LocalFileInputSource->new($filename));
526             };
527             }
528             else
529             {
530             no warnings;
531             my $valflag = $XML::Xerces::AbstractParser::Val_Auto;
532            
533             my $parser = XML::Xerces::XercesDOMParser->new();
534             $parser->setValidationScheme($valflag);
535             $parser->setDoNamespaces(1);
536             $parser->setCreateEntityReferenceNodes(1);
537             $parser->setDoSchema(1);
538            
539             my $ERROR_HANDLER = XML::Xerces::PerlErrorHandler->new();
540             $parser->setErrorHandler($ERROR_HANDLER);
541              
542             my $filename = $self->{filename};
543             require File::Spec;
544             unless (File::Spec->file_name_is_absolute($filename))
545             {
546             $filename = File::Spec->rel2abs($filename);
547             }
548             # Xerces can't handle symlinks. Sigh...
549             if ($filename =~ /^\//) # Unix only
550             {
551             my $hasSymLinks;
552             do
553             {
554             $hasSymLinks = 0;
555             my @parts = split(/\//, $filename);
556             shift @parts if (!$parts[0]);
557             for (my $i=0; $i<@parts; $i++)
558             {
559             my $path = "/" . join("/", @parts[0..$i]);
560             if (-l $path)
561             {
562             my $link = readlink($path);
563             if ($link =~ /\//)
564             {
565             $filename = $link;
566             }
567             else
568             {
569             $filename = File::Spec->rel2abs($link, "/".join("/", @parts[0..$i-1]));
570             }
571             if ($#parts > $i)
572             {
573             $filename .= "/" . join("/", @parts[$i+1..$#parts]);
574             }
575             next;
576             }
577             }
578             }
579             while ($hasSymLinks);
580             }
581             #print "Filename: $filename
\n";
582             eval {
583             $parser->parse(XML::Xerces::LocalFileInputSource->new($filename));
584             #$parser->parse($filename);
585             };
586             }
587              
588             if ($@) {
589             if (ref $@) {
590             $errstr = $@->getMessage();
591             } else {
592             $errstr = $@;
593             }
594              
595             # Remove "at .pm line " message
596             $errstr =~ s/\s*$//s;
597             $errstr =~ s/\s*at [\/\\]\S+ line \d+$//s;
598              
599             #&XML::Xerces::XMLPlatformUtils::Terminate();
600             return undef;
601             }
602              
603             #&XML::Xerces::XMLPlatformUtils::Terminate();
604             return 1;
605             }
606             #------------------
607              
608             =item tableName
609              
610             Returns the name of the SQL table, as specified in the XML file. If
611             the XML file does not specify a table name, this returns the empty
612             string.
613              
614             =cut
615              
616             sub tableName
617             {
618             my $self = shift;
619             return $self->{tableName};
620             }
621             #------------------
622              
623             =item keyName
624              
625             Returns the name of the primary key SQL table, as specified in the XML
626             file. If the XML file does not specify a key name, this returns the
627             empty string.
628              
629             =cut
630              
631             sub keyName
632             {
633             my $self = shift;
634             return $self->{keyName};
635             }
636             #------------------
637              
638             =item query QUERYNAME [VAR => VALUE, ...]
639              
640             Run a SELECT style query called , substituting the
641             parameter list into the SQL template. Returns an executed DBI
642             statement handle object, or undef on failure.
643              
644             if is undefined or the empty string, the default query
645             will be used. The default is either a query with no name, if one
646             exists, or the first query in the query definition file. If a
647             nonexistent query is requested, undef is returned.
648              
649             =cut
650              
651             sub query
652             {
653             my $self = shift;
654             my $queryname = shift;
655              
656             my ($sqls, $binds) = $self->_prepare_params($queryname, @_);
657             return undef if ((!$sqls) || @$sqls == 0);
658             $self->{laststatement} = $sqls->[0];
659             $self->{laststatements} = $sqls;
660             $self->{lastbinds} = $binds->[0];
661             $self->{lastbindss} = $binds;
662             my @sths = ();
663             my @results = ();
664             foreach my $iSQL (0 .. $#$sqls)
665             {
666             $self->_startclock() if ($global_benchmarking);
667              
668             my $sth = $self->getDBH()->prepare($sqls->[$iSQL]) or return wantarray ? () : undef;
669             $sth->execute(@{$binds->[$iSQL]}) or return wantarray ? () : undef;
670              
671             $self->_stopclock() if ($global_benchmarking);
672             $self->_incrStats($queryname) if ($global_benchmarking);
673              
674             my $result = $sth->rows();
675             $result = "0E0" if (defined $result && $result eq "0");
676             push @sths, $sth;
677             push @results, $result;
678             }
679             if (wantarray)
680             {
681             return map {($sths[$_],$results[$_])} 0 .. $#sths;
682             }
683             else
684             {
685             return $sths[0];
686             }
687             }
688             #------------------
689              
690             =item do QUERYNAME [VAR => VALUE, ...]
691              
692             Run a INSERT/UPDATE/DELETE style query, substituting the parameter
693             list into the SQL template. Returns a scalar indicating the result of
694             the statement (false for failure, number of rows affected on success).
695              
696             QUERYNAME behaves as described in query().
697              
698             =cut
699              
700             sub do
701             {
702             my $self = shift;
703              
704             my @params = $self->query(@_);
705             return $self->_computeResult(@params);
706             }
707             sub _computeResult
708             {
709             my $self = shift;
710             my @params = @_;
711              
712             my $result = 0;
713             for (my $i=0; $i < @params; $i+=2)
714             {
715             my $thisresult = $params[$i+1];
716             if ($thisresult)
717             {
718             $result += $thisresult;
719             }
720             else
721             {
722             return undef;
723             }
724             }
725             return $result || "0E0";
726             }
727             #------------------
728              
729             =item getLastInsertID
730              
731             After an insert statement into a table with an autoincremented primary
732             key, this command returns the ID number that was auto-generated for
733             the new row.
734              
735             Warning: This is specific to MySQL. I do not believe this function
736             will work on other database platforms.
737              
738             =cut
739              
740             sub getLastInsertID
741             {
742             my $self = shift;
743              
744             my $sth = $self->getDBH()->prepare("select LAST_INSERT_ID()") or return undef;
745             $sth->execute() or return undef;
746             $self->_incrStats() if ($global_benchmarking);
747             my ($id) = $sth->fetchrow_array();
748             $sth->finish();
749             return $id;
750             }
751             #------------------
752              
753             =item storeObject QUERYNAME, OBJECT
754              
755             Save an object to backend storage, using the specified query. The
756             object methods indicated in accessors will be called to fill in
757             the SQL statement.
758              
759             QUERYNAME behaves as described in query().
760              
761             =cut
762              
763             sub storeObject
764             {
765             my $self = shift;
766             my $queryname = shift;
767             my $object = shift;
768              
769             my $result = $self->do($queryname, $object, @_);
770             $self->_set_obj_result($object, $queryname, 0, $result);
771             return $result;
772             }
773             #------------------
774              
775             =item fillObject QUERYNAME, OBJECT
776              
777             Run the specified query and fill in the object with the returned
778             fields. The object should already exist and should have enough fields
779             filled in to make the query return a unique object. If any command in
780             the query returns zero or more than one rows, this request will fail.
781              
782             QUERYNAME behaves as described in query().
783              
784             =cut
785              
786             sub fillObject
787             {
788             my $self = shift;
789             my $queryname = shift;
790             my $obj = shift;
791              
792             my @params = $self->query($queryname, $obj, @_);
793             return undef if (@params == 0 || (!$params[0]));
794              
795             my $query = $self->_get_query($queryname);
796             my @sqlstructs = _gettag($query, "sql");
797              
798             for (my $i=0; $i < @params; $i+=2)
799             {
800             my $sth = $params[$i];
801             return undef if (!$sth);
802             my $result = $params[$i+1];
803             my $sqlstruct = $sqlstructs[$i/2];
804              
805             if ($sth->rows() == 0)
806             {
807             $errstr = "Did not find any matches";
808             return undef;
809             }
810             elsif ($sth->rows() > 1)
811             {
812             $errstr = "Found too many matches";
813             return undef;
814             }
815              
816             my @fields = _gettag($sqlstruct, "retrieve");
817              
818             while (my $row = $sth->fetchrow_hashref)
819             {
820             foreach my $field (@fields)
821             {
822             # If the requested fields are "*" or ".*" load them all
823             if ($field->getAttribute("key") =~ /^(\w+\.)?\*/)
824             {
825             foreach my $dbFieldName (keys %$row)
826             {
827             _obj_set($obj, {key => $dbFieldName},
828             $row->{$dbFieldName});
829             }
830             }
831             else
832             {
833             my $dbFieldName = $field->getAttribute("as");
834             if (!$dbFieldName)
835             {
836             $dbFieldName = $field->getAttribute("key");
837             $dbFieldName =~ s/^\w+\.//; # remove table name if present
838             }
839             _obj_set($obj, $field->{attributes},
840             $row->{$dbFieldName});
841             }
842             }
843             $self->_set_obj_result($obj, $queryname, $i/2, $result);
844             }
845             }
846             return $self->_computeResult(@params);
847             }
848             #------------------
849              
850             =item retrieveObjects QUERYNAME, PACKAGE, NEW_ARGS [ARGUMENTS]
851              
852             Run the specified query and return an array of objects of class
853             PACKAGE. The objects will be created by calling PACKAGE->new(). Any
854             extra arguments to this function will be passed as arguments to new().
855             The objects will be filled with the values from the rows returned by
856             the query.
857              
858             NEW_ARGS is an array reference of arguments passed to the 'new'
859             function of PACKAGE.
860              
861             QUERYNAME behaves as described in query().
862              
863             =cut
864              
865             sub retrieveObjects
866             {
867             my $self = shift;
868             my $queryname = shift;
869             my $pkg = shift;
870             my $newargs = shift;
871              
872             my @params = $self->query($queryname, @_);
873             return () if (@params == 0 || (!$params[0]));
874              
875             my $query = $self->_get_query($queryname);
876             my @sqlstructs = _gettag($query, "sql");
877              
878             my @list = ();
879             for (my $i=0; $i < @params; $i+=2)
880             {
881             my $sth = $params[$i];
882             my $result = $params[$i+1];
883             my $sqlstruct = $sqlstructs[$i/2];
884              
885             # If not a SELECT, or if the SELECT has no records, skip this $sth
886             if (!$sth->FETCH('NAME') || $sth->rows <= 0)
887             {
888             next;
889             }
890              
891             my @fields = _gettag($sqlstruct, "retrieve");
892              
893             while (my $row = $sth->fetchrow_hashref)
894             {
895             my $obj = $pkg->new(@$newargs);
896             foreach my $field (@fields)
897             {
898             # If the requested fields are "*" or ".*" load them all
899             if ($field->getAttribute("key") =~ /^(\w+\.)?\*/)
900             {
901             foreach my $dbFieldName (keys %$row)
902             {
903             _obj_set($obj, {key => $dbFieldName},
904             $row->{$dbFieldName});
905             }
906             }
907             else
908             {
909             my $dbFieldName = $field->getAttribute("as");
910             if (!$dbFieldName)
911             {
912             $dbFieldName = $field->getAttribute("key");
913             $dbFieldName =~ s/^\w+\.//; # remove table name if present
914             }
915             _obj_set($obj, $field->{attributes},
916             $row->{$dbFieldName});
917             }
918             }
919             push @list, $obj;
920             $self->_set_obj_result($obj, $queryname, $i/2, $result);
921             }
922             }
923             return @list;
924             }
925              
926             #------------------
927             # PRIVATE function:
928             # Tell an object the result of the SQL query, if applicable
929              
930             sub _set_obj_result
931             {
932             my $self = shift;
933             my $object = shift;
934             my $queryname = shift;
935             my $i = shift || 0;
936             my $result = shift;
937              
938             my $query = $self->_get_query($queryname);
939             return undef if (!$query);
940             my $sqlstruct = (_gettag($query, "sql"))[$i];
941             my $rescmd = (_gettag($sqlstruct, "result"))[0];
942             if ($rescmd)
943             {
944             return _obj_set($object, $rescmd->{attributes}, $result);
945             }
946             return 1;
947             }
948              
949             #------------------
950             # PRIVATE function:
951             # Given a CAM::XML object, or its child arrayref,
952             # return the all tags of a given type
953              
954             sub _gettag
955             {
956             my $obj = shift;
957             my $tag = shift;
958              
959             $obj = $obj->{children} if (ref($obj) ne "ARRAY" && $obj->isa("CAM::XML"));
960             return grep {$_->{name} && $_->{name} eq $tag} @$obj;
961             }
962              
963             #------------------
964             # PRIVATE function:
965             # Find a query with the given name
966              
967             sub _get_query
968             {
969             my $self = shift;
970             my $queryname = shift;
971              
972             my $query;
973             if ((!defined $queryname) || $queryname eq "")
974             {
975             $queryname = "_default";
976             $query = $self->{defaultquery};
977             }
978             else
979             {
980             $query = $self->{queries}->{$queryname};
981             }
982              
983             if (!$query)
984             {
985             return undef;
986             }
987             return $query;
988             }
989              
990             #------------------
991             # PRIVATE function:
992             # Replace parameter place holders in the SQL template. Bind values
993             # are returned for later use in execution.
994              
995             sub _prepare_params
996             {
997             my $self = shift;
998             my $queryname = shift;
999              
1000             my $query = $self->_get_query($queryname);
1001             if (!$query)
1002             {
1003             &carp("There is no such query named '$queryname' in $$self{filename}");
1004             return ();
1005             }
1006              
1007             my $binds = [];
1008             my $sqls = [];
1009              
1010             # TODO: check for unset params? Or just leave them undef?
1011             my $obj;
1012             if ($_[0] && ref($_[0]))
1013             {
1014             $obj = shift;
1015             }
1016             my %params = (@_);
1017              
1018             my @sqlstructs = _gettag($query, "sql");
1019             foreach my $sqlstruct (@sqlstructs)
1020             {
1021             my $bind = [];
1022             my $sql = "";
1023             foreach my $part ($sqlstruct->getChildren())
1024             {
1025             if ($part->isa("CAM::XML::Text"))
1026             {
1027             $sql .= $part->{text};
1028             }
1029             else
1030             {
1031             # Policy: if we have an object, prefer the passed
1032             # parameter over the object, ALWAYS
1033              
1034             my $type = $part->{name};
1035             if ($type eq "retrieve")
1036             {
1037             $sql .= $part->getAttribute("key");
1038             if ($part->getAttribute("as"))
1039             {
1040             $sql .= " as " . $part->getAttribute("as");
1041             }
1042             }
1043             elsif ($type eq "replace")
1044             {
1045             my $val;
1046             if ($obj && (!exists $params{$part->getAttribute("key")}))
1047             {
1048             $val = &_obj_get($obj, $part->{attributes});
1049             }
1050             else
1051             {
1052             $val= $params{$part->getAttribute("key")};
1053             }
1054             $sql .= defined $val ? $val : "";
1055             }
1056             else
1057             {
1058             if ($obj && (!exists $params{$part->getAttribute("key")}))
1059             {
1060             push @$bind, &_obj_get($obj, $part->{attributes});
1061             }
1062             else
1063             {
1064             my $key = $part->getAttribute("key");
1065             my $default = $part->getAttribute("default");
1066             my $val = defined $params{$key} ? $params{$key} : $default;
1067             push @$bind, $val;
1068             }
1069             $sql .= "?";
1070             }
1071             }
1072             }
1073             push @$sqls, $sql;
1074             push @$binds, $bind;
1075             }
1076              
1077             return ($sqls, $binds);
1078             }
1079              
1080             #------------------
1081             # PRIVATE function:
1082             # call the accessor of an object, return the result
1083             # if the accessor fails, try to retrieve the field directly
1084              
1085             sub _obj_get
1086             {
1087             my $object = shift;
1088             my $s = shift;
1089              
1090             my $result;
1091             no strict 'refs';
1092             if ($global_safe_functions)
1093             {
1094             if ($s->{accessor})
1095             {
1096             my $function = $s->{accessor};
1097             $result = eval {$object->$function()};
1098             if ($@)
1099             {
1100             my $function = "get".$s->{key};
1101             $result = eval {$object->$function()};
1102             if ($@)
1103             {
1104             $result = $object->{$s->{key}};
1105             }
1106             }
1107             }
1108             else
1109             {
1110             my $function = "get".$s->{key};
1111             $result = eval {$object->$function()};
1112             if ($@)
1113             {
1114             $result = $object->{$s->{key}};
1115             }
1116             }
1117             }
1118             else
1119             {
1120             if ($s->{accessor})
1121             {
1122             my $function = $s->{accessor};
1123             $result = $object->$function();
1124             }
1125             else
1126             {
1127             my $function = "get".$s->{key};
1128             $result = $object->$function();
1129             }
1130             }
1131             if (!defined $result)
1132             {
1133             $result = $s->{default};
1134             }
1135             return $result;
1136             }
1137              
1138             #------------------
1139             # PRIVATE function:
1140             # call the mutator of an object with the specified value, return the result
1141             # if the mutator fails, try to set the field directly
1142              
1143             sub _obj_set
1144             {
1145             my $object = shift;
1146             my $s = shift;
1147             my $value = shift;
1148            
1149             if (!$s->{key})
1150             {
1151             warn "this object has no key";
1152             return 0;
1153             }
1154             no strict 'refs';
1155             if ($global_safe_functions)
1156             {
1157             if ($s->{mutator})
1158             {
1159             my $function = $s->{mutator};
1160             my $result = eval {$object->$function($value)};
1161             return 1 if (!$@);
1162             }
1163             if ($s->{as})
1164             {
1165             my $function = "set".$s->{as};
1166             my $result = eval {$object->$function($value)};
1167             return 1 if (!$@);
1168             }
1169             my $function = "set".$s->{key};
1170             my $result = eval {$object->$function($value)};
1171             return 1 if (!$@);
1172            
1173             $object->{$s->{key}} = $value;
1174             }
1175             else
1176             {
1177             if ($s->{mutator})
1178             {
1179             my $function = $s->{mutator};
1180             $object->$function($value);
1181             }
1182             elsif ($s->{as})
1183             {
1184             my $function = "set".$s->{as};
1185             $object->$function($value);
1186             }
1187             else
1188             {
1189             my $function = "set".$s->{key};
1190             $object->$function($value);
1191             }
1192             }
1193             return 1;
1194             }
1195              
1196             #------------------
1197             # PRIVATE function:
1198             # update the stats data structure for this query
1199              
1200             sub _incrStats
1201             {
1202             my $self = shift;
1203             my $queryname = shift;
1204              
1205             $global_stats{queries}++;
1206             my $cmdData = $global_stats{cmds}->{$self->{cmd}};
1207             $cmdData->{queries}++;
1208             if ($queryname)
1209             {
1210             my $queryData = $cmdData->{query}->{$queryname};
1211             $queryData->{queries}++;
1212             if ($self->{_time})
1213             {
1214             $global_stats{time} += $self->{_time};
1215             $cmdData->{time} += $self->{_time};
1216             $queryData->{time} += $self->{_time};
1217             }
1218             }
1219             }
1220              
1221             #------------------
1222             # PRIVATE functions:
1223             # measure elapsed time
1224              
1225             sub _startclock
1226             {
1227             my $self = shift;
1228              
1229             delete $self->{_time};
1230            
1231             $self->{_clock} = [Time::HiRes::gettimeofday()];
1232             }
1233             sub _stopclock
1234             {
1235             my $self = shift;
1236              
1237             if (defined $self->{_clock})
1238             {
1239             $self->{_time} = Time::HiRes::tv_interval($self->{_clock});
1240             delete $self->{_clock};
1241             }
1242             }
1243             #------------------
1244              
1245             =item statistics
1246              
1247             Return a data structure of statistics for this package. The data
1248             structure looks like this:
1249              
1250             $stats = {
1251             queries => ,
1252             time => ,
1253             cmds => {
1254             "sqlone.xml" => {
1255             queries => ,
1256             time => ,
1257             query => {
1258             "queryone" => {
1259             queries => ,
1260             time => ,
1261             },
1262             "querytwo" => {
1263             queries => ,
1264             time => ,
1265             },
1266             }
1267             },
1268             "sqltwo.xml" => {
1269             queries => ,
1270             time => ,
1271             query => {
1272             "queryone" => {
1273             queries => ,
1274             time => ,
1275             },
1276             }
1277             },
1278             },
1279             };
1280              
1281             The returned structure is a reference to live data so DO NOT alter it
1282             in any way! Treat it as read-only data.
1283              
1284             =cut
1285              
1286             sub statistics
1287             {
1288             my $pkg = shift;
1289              
1290             if ($global_benchmarking)
1291             {
1292             return \%global_stats;
1293             }
1294             else
1295             {
1296             return undef;
1297             }
1298             }
1299             #------------------
1300              
1301             =item statisticsHTML
1302              
1303             This class method returns an HTML string that renders the statistics
1304             data in a human readable format.
1305              
1306             =cut
1307              
1308             sub statisticsHTML
1309             {
1310             my $pkg = shift;
1311              
1312             my $stats = $pkg->statistics();
1313             return "" if (!$stats);
1314             my $html = "
"; 
1315             $html .= "queries ".$stats->{queries}."\n";
1316             $html .= "time ".$stats->{time}."\n";
1317             foreach my $cmd (sort keys %{$stats->{cmds}})
1318             {
1319             my $cmdData = $stats->{cmds}->{$cmd};
1320             $html .= " $cmd\n";
1321             $html .= " queries ".$cmdData->{queries}."\n";
1322             $html .= " time ".$cmdData->{time}."\n";
1323             foreach my $queryname (sort keys %{$cmdData->{query}})
1324             {
1325             my $queryData = $cmdData->{query}->{$queryname};
1326             next if ($queryData->{queries} == 0);
1327             $html .= " $queryname\n";
1328             $html .= " queries ".$queryData->{queries}."\n";
1329             $html .= " time ".$queryData->{time}."\n";
1330             }
1331             }
1332             $html .= "\n";
1333             return $html;
1334             }
1335             sub _statsKeySort
1336             {
1337             my %order = (
1338             queries => 1,
1339             time => 2,
1340             cmds => 3,
1341             query => 4,
1342             other => 5,
1343             );
1344             ($order{$a} || $order{other}) <=> ($order{$a} || $order{other}) || $a cmp $b;
1345             }
1346             #------------------
1347              
1348             =item toForm QUERYNAME
1349              
1350             Return the body of an HTML form useful for testing and evaluting the
1351             SQL template. Use it something like this:
1352              
1353             my $sql = CAM::SQLManager->new("somecommand");
1354             print "
";
1355             print $sql->toForm();
1356             print "";
1357             print "";
1358              
1359             =cut
1360              
1361             sub toForm
1362             {
1363             my $self = shift;
1364             my $queryname = shift;
1365              
1366             my $form = "";
1367             my $query = $self->_get_query($queryname);
1368             return undef if (!$query);
1369             my @sqlstructs = _gettag($query, "sql");
1370             foreach my $i (0 .. $#sqlstructs)
1371             {
1372             my $sqlstruct = $sqlstructs[$i];
1373             foreach my $part ($sqlstruct->getChildren())
1374             {
1375             if ($part->isa("CAM::XML::Text"))
1376             {
1377             $form .= &_html_escape($part->{text});
1378             }
1379             else
1380             {
1381             my $type = $part->{name};
1382             if ($type eq "retrieve")
1383             {
1384             $form .= &_html_escape($part->getAttribute("key"));
1385             if ($part->getAttribute("as"))
1386             {
1387             $form .= &_html_escape(" as " . $part->getAttribute("as"));
1388             }
1389             #$form .= &_html_escape("<% ".$part->getAttribute("key").":".$part->getAttribute("mutator")." %>");
1390             }
1391             else
1392             {
1393             $form .= "getAttribute("key")."\"> (".$part->getAttribute("key").")";
1394             }
1395             }
1396             }
1397             }
1398            
1399             $form .= "
Optionally, limit output to rows between and
\n";
1400              
1401             return $form;
1402             }
1403             #------------------
1404              
1405             =item fromForm [CGI object]
1406              
1407             Accept input from an HTML form like the one output by toForm() and
1408             return HTML formatted output.
1409              
1410             =cut
1411              
1412             sub fromForm
1413             {
1414             my $self = shift;
1415             my $queryname = shift;
1416             my $cgi = shift;
1417              
1418             my $html = "";
1419              
1420             my $start = $cgi->param('sqlform_startrow');
1421             my $end = $cgi->param('sqlform_endrow');
1422              
1423             my %params = ();
1424             foreach my $key ($cgi->param)
1425             {
1426             $params{$key} = $cgi->param($key);
1427             }
1428             my @results = $self->query($queryname, %params);
1429             my @explains = ();
1430             for (my $i = 0; $i < @results; $i+=2)
1431             {
1432             my $sth = $results[$i];
1433             if (!$sth)
1434             {
1435             $html .= "Query failed: ";
1436             }
1437             else
1438             {
1439             my $sql = $sth->{Statement};
1440             my $sqlst = &_html_escape($sql);
1441             if ($self->{lastbindss}->[$i/2])
1442             {
1443             my @binds = @{$self->{lastbindss}->[$i/2]};
1444             my $i=0;
1445             $sql =~ s/\?/$self->getDBH()->quote($binds[$i++])/ge;
1446             $i=0;
1447             $sqlst =~ s/\?/"".$self->getDBH()->quote($binds[$i++])."<\/strong>"/ge;
1448             }
1449             push @explains, "explain $sql" if ($sql =~ /^\s*select\s/si);
1450             $html .= "Final query (reconstructed):
$sqlst

\n";
1451             my $rows = $sth->rows();
1452             $rows = undef if (defined $rows && $rows eq "");
1453             $rows = "(undefined)" if (!defined $rows);
1454             $html .= "Rows: $rows
\n";
1455             my $row = 0;
1456             if ($sth->FETCH('NAME'))
1457             {
1458             $html .= "\n"; \n"; \n";
1459             $html .= "
" . join("", "Row", @{$sth->FETCH('NAME')}) . "
1460             while (my $ref = $sth->fetchrow_arrayref)
1461             {
1462             $row++;
1463             next if ($start && $row < $start);
1464             last if ($end && $row > $end);
1465             my @data = map {&_html_escape($_)} @$ref;
1466             @data = map {$_ eq "" ? " " : $_} @data;
1467             $html .= "
" . join("", $row, @data) . "
1468             }
1469             $html .= "
\n";
1470             }
1471             }
1472             }
1473              
1474             $html .= "
Explain queries:
\n";
1475             foreach my $explain (@explains)
1476             {
1477             my $sth = $self->getDBH()->prepare($explain);
1478             $sth->execute();
1479             $html .= "\n"; \n"; \n";
1480             $html .= "
" . join("", @{$sth->FETCH('NAME')}) . "
1481             while (my $ref = $sth->fetchrow_arrayref)
1482             {
1483             my @data = map {&_html_escape($_)} @$ref;
1484             @data = map {$_ eq "" ? " " : $_} @data;
1485             $html .= "
" . join("", @data) . "
1486             }
1487             $html .= "
\n";
1488             }
1489              
1490             return $html;
1491             }
1492             #------------------
1493              
1494             =item testCommands
1495              
1496             =item testCommands CGIobject
1497              
1498             A nearly complete CGI program to run tests on your library SQL
1499             commands. You may optionally pass it a CGI object, if you want it to
1500             work as part of a larger framework. Otherwise, the function
1501             instantiates it's own CGI object. Here is an complete CGI program
1502             using this function:
1503              
1504             #!/usr/bin/perl
1505             use CAM::SQLManager;
1506             use DBI;
1507             my $dbh = DBI->connect(blah blah);
1508             CAM::SQLManager->setDBH($dbh);
1509             CAM::SQLManager->setDirectory("/path/to/sql/library");
1510             CAM::SQLManager->setBenchmarking(1); # optional
1511             CAM::SQLManager->testCommands();
1512              
1513              
1514             =cut
1515              
1516             sub testCommands
1517             {
1518             my $pkg = shift;
1519             my $cgi = shift;
1520              
1521             if (!$global_dbh)
1522             {
1523             die "You must call CAM::SQLManager::setDBH first";
1524             }
1525             if (!$global_directory)
1526             {
1527             die "You must call CAM::SQLManager::setDirectory first";
1528             }
1529              
1530             if (!$cgi)
1531             {
1532             require CGI;
1533             $cgi = CGI->new;
1534             print $cgi->header();
1535             }
1536             my $url = $cgi->url();
1537             my $novalidate = $cgi->param("novalidate");
1538             my $validatearg = $novalidate ? "&novalidate=1" : "";
1539             my $cmd = $cgi->param('cmd');
1540             $cgi->delete('novalidate');
1541             $cgi->delete('cmd');
1542              
1543             if (!$cmd)
1544             {
1545             print qq[Test all XML files
\n];
1546             print qq[
\n];
1547              
1548             foreach my $file ($pkg->getAllCmds())
1549             {
1550             my $name = $file;
1551             $name =~ s,^$global_directory[/\\]?,,;
1552              
1553             print qq[$name
\n];
1554             }
1555             }
1556             elsif ($cmd eq "_testall")
1557             {
1558             foreach my $file ($pkg->getAllCmds())
1559             {
1560             $file =~ s,^$global_directory[/\\]?,,;
1561             print "$file: ";
1562             my $sql = CAM::SQLManager->new($file);
1563             if ($sql)
1564             {
1565             if ($sql->validateXML())
1566             {
1567             print "OK
\n";
1568             }
1569             elsif (!$XML::Xerces::VERSION)
1570             {
1571             print "This command was not validated (XML::Xerces is not installed)
\n";
1572             }
1573             else
1574             {
1575             print "This command did not pass the validation test:
$errstr
\n";
1576             }
1577             }
1578             else
1579             {
1580             print "Failed to create new sql object
\n";
1581             }
1582             }
1583             }
1584             else
1585             {
1586             my $sql = CAM::SQLManager->new($cmd);
1587             die "Failed to create new sql object" if (!$sql);
1588              
1589             if ($novalidate)
1590             {
1591             print "Validation tests disabled

\n";

1592             }
1593             else
1594             {
1595             if (!$sql->validateXML())
1596             {
1597             if (!$XML::Xerces::VERSION)
1598             {
1599             print "This command was not validated (XML::Xerces is not installed)

\n";

1600             }
1601             else
1602             {
1603             print "This command did not pass the validation test:
$errstr

\n";

1604             }
1605             }
1606             }
1607              
1608             my $queryname = $cgi->param('queryname');
1609             $cgi->delete('queryname');
1610             if (!defined $queryname)
1611             {
1612             if (keys(%{$sql->{queries}}) == 1)
1613             {
1614             ($queryname) = keys(%{$sql->{queries}});
1615             }
1616             else
1617             {
1618             print "Select a query to test for $cmd:
\n";
1619             foreach my $name (sort keys %{$sql->{queries}})
1620             {
1621             print qq[$name
\n];
1622             }
1623             print "

Original document:
" . &_html_escape($sql->{sql}) . "

\n";
1624             return;
1625             }
1626             }
1627              
1628             my @parms = $cgi->param();
1629             if (@parms == 0)
1630             {
1631             print "
\n";
1632             if ($novalidate)
1633             {
1634             print "\n";
1635             }
1636             print "\n";
1637             print "\n";
1638             print $sql->toForm($queryname);
1639             print "\n";
1640             print "\n";
1641             }
1642             else
1643             {
1644             print $sql->fromForm($queryname, $cgi);
1645             print "
Statistics:\n";
1646             print $pkg->statisticsHTML();
1647             }
1648             }
1649             }
1650              
1651             #------------------
1652             # PRIVATE function:
1653             # Convert a block of text so it displays nicely in HTML
1654              
1655             sub _html_escape
1656             {
1657             my $text = shift;
1658              
1659             $text = "NULL" if (!defined $text);
1660             $text =~ s/&/&/g;
1661             $text =~ s/"/"/g;
1662             $text =~ s/
1663             $text =~ s/>/>/g;
1664             $text =~ s/\r?\n/
\n/g;
1665             $text =~ s/\r/
\n/g;
1666             $text =~ s/^ / /gm;
1667             $text =~ s/ /  /g;
1668              
1669             return $text;
1670             }
1671              
1672             1;
1673             #------------------
1674             __END__