1# DBIWrapper.pm - The DataBase Wrapper Class that provides the DBI database
2# connection and core functions for working with DBI databases.
3# Created by James Pattie, 11/02/2000.
4
5# Copyright (c) 2000-2006 Xperience, Inc. http://www.pcxperience.com/
6# All rights reserved.  This program is free software; you can redistribute it
7# and/or modify it under the same terms as Perl itself.
8
9package DBIWrapper;
10use strict;
11use DBI;
12use IO::Scalar;
13use Spreadsheet::WriteExcel;
14use DateTime;
15use DateTime::HiRes;
16use DBIWrapper::Time::Now::HiRes;
17use Digest::SHA qw( sha1_hex );
18use POSIX qw(floor);
19use vars qw($AUTOLOAD $VERSION @ISA @EXPORT @EXPORT_OK);
20
21require Exporter;
22
23@ISA = qw(Exporter AutoLoader);
24@EXPORT = qw();
25
26$VERSION = '0.30';
27
28use vars qw($formUnEncodedCharacters %formUnEncodedCharactersHash);
29$formUnEncodedCharacters = '<>"';
30%formUnEncodedCharactersHash = ( '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', '&' => '&amp;' );
31
32my $deadlockEncountered = 0;  # global variable to allow sybase to detect when a deadlock happened so code can be re-run.
33
34=head1 NAME
35
36DBIWrapper - Perl extension for generic DBI database access.
37
38=head1 SYNOPSIS
39
40  use DBIWrapper;
41  my $db = DBIWrapper->new(dbType => "Pg",
42                           dbName => "test_db",
43                           dbHost => "localhost",
44                           dbUser => "nobody",
45                           dbPasswd => "",
46                           dbPort => "5432",
47                           predefinedDSN => "",
48                           printError => 1,
49                           raiseError => 1,
50                           autoCommit => 0);
51  if ($db->error())
52  {
53    die $db->errorMessage();
54  }
55
56  my $sth = $db->read("SELECT * FROM test_tb");
57  my $result = $db->write(sql => "INSERT INTO test_tb (name, value) VALUES (?, ?)",
58    plug => [ $name, $value ]);
59  # this used DBI's substitution features to plugin the name and value.
60
61  $db->close();  # close down the database connection.  Any read()'s
62  # or write()'s will no longer be valid with this object until a new() is
63  # issued again.
64
65=head1 DESCRIPTION
66
67DBIWrapper is the generic database Object for accessing the DBI database
68interface.  It provides the lowest level of functionality needed by any
69program wanting to access databases via the DBI.  Currently, DBIWrapper
70is only aware of Pg (PostgreSQL), mysql (MySQL), Sybase and ODBC DBD
71modules and how to work with them correctly.
72
73Support for transactions on MySQL is now checked for and if found to be
74available, the AutoCommit flag is turned off so that transactions will
75be used.
76
77The substitution array (if used) will cause each ##?1##, ##?2##, etc.
78string in the sql string to be substituted for the corresponding value
79in the substitution array.  It must start at ?1.  It is up to the user
80to pass in the correct number of elements for both the plug and
81substitution arrays.  The plug array is used to pass in the values for
82DBI to replace in the sql string of ? which is standard DBI notation.
83
84=head1 DATABASE VERSION INFO
85The version info for the database in use, if determinable, is now
86stored as:
87B<serverVersion> = I<complete version string>
88Ex: on Debian, PostgreSQL returns I<8.1.0>
89MySQL returns I<4.1.11-Debian_4sarge5-log>
90
91B<serverVerMajor> = I<The Major release number>
92From the previous examples, PostgreSQL would be I<8>, MySQL would be I<4>.
93
94B<serverVerMinor> = I<The Minor release number>
95From the previous examples, PostgreSQL would be I<1>, MySQL would be I<1>.
96
97B<serverVerRelease> = I<The Point release number>.  This does not include
98any text after the point release value that may be included by the distro.
99From the previous examples, PostgreSQL would be I<0>, MySQL would be I<11>.
100
101The stored database version info is used to determine if we can still do
102I<oid> based lastID lookups in PostgreSQL or if we have to do something
103that doesn't depend on the I<oid> since PostgreSQL B<8.1> no longer enables
104I<oid>s by default.
105
106=head1 Sybase NOTES
107
108The getDataArray(), getDataArrayHeader(), getDataHash(),
109getDataHashHeader(), readXML(), readHTML() methods all properly handle
110multiple result sets being returned from Sybase.  This could be the
111result of multiple select statements or a compute clause.  In the
112case of the Header() methods, the header row is based on the first
113returned select statement, which may not be correct for the following
114statements or compute blocks.
115
116=head1 Deadlock Detection NOTES
117
118Initial support for detecting a deadlock scenario when using Sybase is
119now implemented.  The code will attempt to retry the sql in question,
120either a read or write call, upto deadlockNumTries tries and sleeping
121for deadlockSleep seconds between tries.  If deadlockRampSleep is enabled,
122which it is not by default, then we multiply the deadlockSleep by the
123current try #, if > 1, thus sleeping in multiples of deadlockSleep seconds.
124
125There are currently no helper methods to change the values, but you can
126just assign new values to the dbObj instance you create as they are
127encapsulated within the object.  The only thing that is not encapsulated
128is the deadlockEncountered global variable, due to the way the DBI error
129handler is defined.  You should not have to touch this variable unless you
130wanted to know if a deadlock had been detected in your last read()/write()
131command.  It is reset to false whenever a read()/write() is issued.
132
133=head1 Long Running SQL Detection NOTES
134
135You can now define read and write thresholds (in seconds) that if a read()
136or write() ran for >= the threshold then it will be logged to the long
137running log file you specified or '/var/log/dbiwrapper-long-running-sql.log'.
138
139By default the longRunningRead and longRunningWrite thresholds are 10 seconds.
140
141The format of the logged entries is:
142$0|$$|start timestamp (formatted)|end timestamp (formatted)|# seconds ran|read or write|deadlock Encountered|numTries|server or dbHost|dbName|sql statement|plug arguments|uniqueID
143
144  See Logging Notes below for timestamp and duration changes.
145
146sql statement has all newlines turned into spaces so it will fit on a single line.
147plug arguments is a comma delimited list.
148
149=head1 Logging SQL statements NOTES
150
151B<LOG FORMAT CHANGES>: timestamps are now using DateTime::HiRes and include milliseconds.
152
153logDateFormat is now ignored and will be removed in a future version.  Date format is
154hardcoded as YYYY-MM-DD HH:MM:SS.milliseconds (0 to 999).
155
156specify myTimeZone if you want something other than 'America/Phoenix'.
157
158A hopefully unique ID will be generated that consists of SHA1'ing the concatenation of:
159$0 . $$ . timestamp formatted . action . sql statement (newlines replaced)
160I'll store the hexified SHA1 value for the uniqueID.
161
162-------------
163
164All sql statements will now default to being logged to
165/var/log/dbiwrapper-sql-statements.log.
166
167This can be turned off and an alternate log file specified.
168
169Log file format is:
170
171$0|$$|timestamp (formatted)|read or write|server or dbHost|dbName|sql statement|plug arguments|uniqueID
172
173sql statement has all newlines turned into spaces so it will fit on a single line.
174plug arguments is a comma delimited list.
175
176Specify sqlNewlineReplacement if you want a different \n replacement in the sql statement log.
177Specify sqlPlugNewlineReplacement if you want a different \n replacement in the plug arguments.
178
179sqlNewlineReplacement = ' '
180sqlPlugNewlineReplacement = '\\n'
181
182All |'s are \ escaped in the sql and plug strings that are logged to disk.
183
1842 additional log files will be created to track the start/stop and duration of the
185executing sql.
186
187I take the sqlStatementLog and insert -start and -stop before the .log extension.
188
189Start SQL file format is:
190
191uniqueID|$0|$$|start_timestamp (formatted)|first 20 chars of sql statement|length of sql statement|plug arguments
192
193Stop SQL file format is:
194
195uniqueID|$0|$$|start_timestamp (formatted)|stop_timestamp (formatted)|duration.milliseconds|first 20 chars of sql statement|length of sql statement|plug arguments|deadlock Encountered|numTries
196
197duration.milliseconds is the duration in whole seconds plus the # of milliseconds difference.  It is not a fractional value.
198
199=head1 Exported FUNCTIONS
200
201B<NOTE>: I<bool> = 1(true), 0(false)
202
203=over 4
204
205=item scalar new(dbType, dbName, dbHost, dbUser, dbPasswd, dbPort, printError, raiseError, autoCommit, predefinedDSN, setDateStyle, logLevel, server, interfaces, longRunningRead, longRunningWrite, longRunningLog, logSQLStatements, sqlStatementLog, logSQLDurations, myTimeZone)
206
207 Creates a new instance of the DBIWrapper object and opens
208 a connection to the specified database.  If predefinedDSN is
209 specified then it is used instead of the dbName, dbHost, dbPort
210 values.  This is mainly to support ODBC easier.
211 If setDateStyle is 1 (default) and dbType = Pg, then the datestyle
212 for PostgreSQL is set to US (MM/DD/YYYY).
213 logLevel defaults to 0.  There are 4 levels 0, 1, 2 and 3 which log
214 the following items when an error occurs:
215 0) Nothing is output
216 1) dbType, dbHost, dbName, printError, raiseError, autoCommit,
217   setDateStyle, supportsTransactions, transactionType, server,
218   interfaces
219 2) all of 1 plus dbUser, dbPort, predefinedDSN
220 3) all of 2 plus dbPasswd
221
222 Sybase specific:
223 server allows you to specify the database server to connect to by name
224   and must be defined in your interfaces file.
225 interfaces allows you to specify the Sybase interfaces file needed
226   to properly connect to the Sybase database.
227
228 If you do not specify server and interfaces, then dbHost and dbPort
229 will be used.
230
231=cut
232sub new
233{
234  my $that = shift;
235  my $class = ref($that) || $that;
236  my $self = bless {}, $class;
237  my %args = ( dbType => 'Pg', dbHost => 'localhost', dbUser => 'nobody', dbPasswd => '', dbPort => '5432', predefinedDSN => "", printError => 1, raiseError => 1, autoCommit => 0, setDateStyle => 1, logLevel => 0, interfaces => "", server => "", longRunningRead => 10, longRunningWrite => 10, longRunningLog => "/var/log/dbiwrapper-long-running-sql.log", logSQLStatements => 1, sqlStatementLog => "/var/log/dbiwrapper-sql-statements.log", sqlNewlineReplacement => ' ', sqlPlugNewlineReplacement => '\\n', logSQLDurations => 1, myTimeZone => 'America/Phoenix', @_ );
238  my ($dbType, $dbName, $dbHost, $dbUser, $dbPasswd, $dbPort, $predefinedDSN, $printError, $raiseError, $autoCommit, $setDateStyle, $logLevel, $interfaces, $server);
239
240  $dbType = $args{dbType};
241  $dbHost = $args{dbHost};
242  $dbName = $args{dbName};
243  $dbUser = $args{dbUser};
244  $dbPasswd = $args{dbPasswd};
245  $dbPort = $args{dbPort};
246  $predefinedDSN = $args{predefinedDSN};
247  $printError = $args{printError};
248  $raiseError = $args{raiseError};
249  $autoCommit = $args{autoCommit};
250  $setDateStyle = $args{setDateStyle};
251  $logLevel = $args{logLevel};
252  $interfaces = $args{interfaces};
253  $server = $args{server};
254  $self->{supportsTransactions} = 1;  # by default all Databases support Transactions, except for MySQL.
255  $self->{transactionType} = "";  # This is only set by MySQL so we know what type of transaction support is available.
256  if ($dbType eq "mysql")
257  {
258    $autoCommit = 1;  # it may not do transactions yet.
259    $self->{supportsTransactions} = 0;
260  }
261
262  $self->{error} = 0;  # nothing wrong yet.
263  $self->{errorString} = "";
264  $self->{errorPhrase} = "() - Error!<br />\n";
265
266  $self->{dbType} = $dbType;
267  $self->{dbHost} = $dbHost;
268  $self->{dbName} = $dbName;
269  $self->{dbUser} = $dbUser;
270  $self->{dbPasswd} = $dbPasswd;
271  $self->{dbPort} = $dbPort;
272  $self->{predefinedDSN} = $predefinedDSN;
273  $self->{printError} = $printError;
274  $self->{raiseError} = $raiseError;
275  $self->{autoCommit} = $autoCommit;
276  $self->{setDateStyle} = $setDateStyle;
277  $self->{logLevel} = $logLevel;
278  $self->{interfaces} = (length $interfaces > 0 ? $interfaces : undef);
279  $self->{server} = (length $server > 0 ? $server : undef);
280  $self->{dbh} = undef;  # set this explicitly now so that we have something to check if an error occurs later.
281
282  # deadlock detection code for sybase.
283  $self->{deadlockSleep} = 5;  # number of seconds to sleep before retrying.
284  $self->{deadlockNumTries} = 3;  # total # of times to retry sql that deadlocked.
285  $self->{deadlockRampSleep} = 0;  # bool indicating if we should increment the amount of sleep between tries.
286
287  # long running sql detection settings
288  $self->{longRunningRead} = $args{longRunningRead};
289  $self->{longRunningWrite} = $args{longRunningWrite};
290  $self->{longRunningLog} = $args{longRunningLog};
291
292  # log sql statements
293  $self->{myTimeZone} = $args{myTimeZone};
294  $self->{logSQLStatements} = $args{logSQLStatements};
295  $self->{logSQLDurations} = $args{logSQLDurations};
296  $self->{sqlStatementLog} = $args{sqlStatementLog};
297  $self->{sqlNewlineReplacement} = $args{sqlNewlineReplacement};
298  $self->{sqlPlugNewlineReplacement} = $args{sqlPlugNewlineReplacement};
299
300  $self->validate_and_connect();
301
302  return $self;
303}
304
305=item void validate_and_connect()
306
307=cut
308sub validate_and_connect
309{
310  my $self = shift;
311
312  if (!$self->isValid(new => 1))
313  {
314    $self->error(errorString => "Error!<br />\n" . $self->errorMessage);
315    return $self;
316  }
317
318  $self->changeSQLLogFiles($self->{sqlStatementLog}, $self->{longRunningLog});
319
320  my $dbh;
321  my $dsn;
322  my %errorHandlers = ();
323  my $dbType        = $self->{dbType};
324  my $predefinedDSN = $self->{predefinedDSN};
325  my $interfaces    = $self->{interfaces};
326
327  $dsn = "dbi:$dbType:";
328
329  if ($dbType =~ /^(Pg|mysql)$/)
330  {
331    if (length $predefinedDSN > 0)
332    {
333      $dsn .= $predefinedDSN;
334    }
335    else
336    {
337      $dsn .= "dbname=$self->{dbName};host=$self->{dbHost};port=$self->{dbPort}";
338    }
339  }
340  elsif ($dbType eq "Sybase")
341  {
342    if (length $predefinedDSN > 0)
343    {
344      $dsn .= $predefinedDSN;
345    }
346    else
347    {
348      $dsn .= "database=$self->{dbName}";
349      if (defined $interfaces)
350      {
351        $dsn .= ";server=$self->{server};interfaces=$self->{interfaces}";
352      }
353      else
354      {
355        # use host and port
356        $dsn .= ";host=$self->{dbHost};port=$self->{dbPort}";
357      }
358    }
359    $errorHandlers{syb_err_handler} = \&sybaseErrorHandler;
360  }
361  elsif ($dbType eq "ODBC")
362  {
363    if (length $predefinedDSN > 0)
364    {
365      $dsn .= $predefinedDSN;
366    }
367    else
368    {
369      $self->error(errorString => "Error!<br />\nYou must specify the 'predefinedDSN' for dbType = '$dbType'!<br />\n" . $self->debugMessage);
370      return $self;
371    }
372  }
373
374  eval {
375    $dbh = DBI->connect($dsn, $self->{dbUser}, $self->{dbPasswd}, { PrintError => $self->{printError}, RaiseError => $self->{raiseError}, AutoCommit => $self->{autoCommit}, %errorHandlers });
376  };
377  if ($@)
378  {
379    $self->{dbh} = undef;
380    $self->error(errorString => "Eval of connect failed!<br />\nError = '$@'.<br />\nDBIError = '" . $DBI::errstr . "'.<br />\n" . $self->debugMessage);
381  }
382  else
383  {
384    if ($dbh)
385    {
386      if (!$DBI::err)
387      {
388        $self->{dbh} = $dbh;
389        if ($dbType eq "Pg")
390        {
391          # figure out what version we are working with.
392          my @result = $self->getDataArray(sql => "SHOW server_version");
393          $self->{serverVersion} = $result[0]->[0];
394          ($self->{serverVerMajor} = $self->{serverVersion}) =~ s/^(\d+)\.(\d+\.\d+)$/$1/;
395          ($self->{serverVerMinor} = $self->{serverVersion}) =~ s/^(\d+)\.(\d+)(\.\d+)$/$2/;
396          ($self->{serverVerRelease} = $self->{serverVersion}) =~ s/^(\d+)\.(\d+)\.(\d+)$/$3/;
397
398          #print "PostgreSQL $self->{serverVersion}\nMajor = '$self->{serverVerMajor}'\nMinor = '$self->{serverVerMinor}'\nRelease = '$self->{serverVerRelease}'\n";
399
400          if ($self->{setDateStyle})
401          {
402            $self->write(sql => "SET datestyle TO 'POSTGRES,US'");
403          }
404        }
405        if ($dbType eq "mysql")
406        {
407          # figure out what version we are working with.
408          my @result = $self->getDataArray(sql => "SHOW VARIABLES LIKE 'version'");
409          $self->{serverVersion} = $result[0]->[1];
410          ($self->{serverVerMajor} = $self->{serverVersion}) =~ s/^(\d+)\.(\d+\.\d+)(\-.+)?$/$1/;
411          ($self->{serverVerMinor} = $self->{serverVersion}) =~ s/^(\d+)\.(\d+)(\.\d+)(\-.+)?$/$2/;
412          ($self->{serverVerRelease} = $self->{serverVersion}) =~ s/^(\d+)\.(\d+)\.(\d+)(\-.+)?$/$3/;
413
414          #print "MySQL $self->{serverVersion}\nMajor = '$self->{serverVerMajor}'\nMinor = '$self->{serverVerMinor}'\nRelease = '$self->{serverVerRelease}'\n";
415
416          # check for Transaction support, and if present disable the AutoCommit flag.
417          if ($self->mysqlHasTransactions())
418          {
419            $self->{supportsTransactions} = 1;
420            $self->{dbh}->{AutoCommit} = 0;
421          }
422          if ($self->error)
423          {
424            $self->{dbh} = undef;
425            $self->error(errorString => "Checking for Transactions with MySQL failed!<br />\n" . $DBI::errstr . "<br />\n" . $self->debugMessage);
426          }
427        }
428      }
429      else
430      {
431        $self->{dbh} = undef;
432        $self->error(errorString => "connect failed!<br />\n" . $DBI::errstr . "<br />\n" . $self->debugMessage);
433      }
434    }
435    else
436    {
437      $self->{dbh} = undef;
438      $self->error(errorString => "connect failed!<br />\n" . $DBI::errstr . "<br />\n" . $self->debugMessage);
439    }
440  }
441}
442
443=item bool isValid()
444
445 Returns 1 if the DBI object is valid, else 0 if invalid.
446
447=cut
448sub isValid
449{
450  my $self = shift;
451  my %args = ( new => 0, @_ );
452  my $new = $args{new};
453  my $valid = 1;
454  my $errorString = "";
455
456  if (!$new)
457  {
458    if (!defined $self->{dbh})
459    {
460      $errorString .= "dbh is not defined!<br />\n";
461      $valid = 0;
462    }
463  }
464  if ($self->{dbType} !~ /^(Pg|mysql|ODBC|Sybase)$/)
465  {
466    $errorString .= "dbType = '$self->{dbType}' is invalid!<br />\n";
467    $valid = 0;
468  }
469  if (length $self->{predefinedDSN} == 0) # only check the dbHost, dbName and dbPort values if not using predefinedDSN
470  {
471    if (length $self->{dbName} == 0)
472    {
473      $errorString .= "dbName = '$self->{dbName}' is invalid!<br />\n";
474      $valid = 0;
475    }
476    if ($self->{dbType} eq "Sybase")
477    {
478      if (defined $self->{server} && !defined $self->{interfaces})
479      {
480        $errorString .= "server = '$self->{server}'.  interfaces must be specified!<br />\n";
481        $valid = 0;
482      }
483      if (defined $self->{interfaces} && !defined $self->{server})
484      {
485        $errorString .= "interfaces = '$self->{interfaces}'.  server must be specified!<br />\n";
486        $valid = 0;
487      }
488    }
489    if ($self->{dbType} ne "Sybase" || ($self->{dbType} eq "Sybase" && (!defined $self->{server} && !defined $self->{interfaces})))
490    {
491      if (length $self->{dbHost} == 0)
492      {
493        $errorString .= "dbHost = '$self->{dbHost}' is invalid!<br />\n";
494        $valid = 0;
495      }
496      if ($self->{dbPort} !~ /^(\d+)$/)
497      {
498        $errorString .= "dbPort = '$self->{dbPort}' is invalid!<br />\n";
499        $valid = 0;
500      }
501    }
502  }
503  if (length $self->{dbUser} == 0)
504  {
505    $errorString .= "dbUser = '$self->{dbUser}' is invalid!<br />\n";
506    $valid = 0;
507  }
508  if ($self->{autoCommit} !~ /^(0|1)$/)
509  {
510    $errorString .= "autoCommit = '$self->{autoCommit}' is invalid!<br />\n";
511    $valid = 0;
512  }
513  if ($self->{printError} !~ /^(0|1)$/)
514  {
515    $errorString .= "printError = '$self->{printError}' is invalid!<br />\n";
516    $valid = 0;
517  }
518  if ($self->{raiseError} !~ /^(0|1)$/)
519  {
520    $errorString .= "raiseError = '$self->{raiseError}' is invalid!<br />\n";
521    $valid = 0;
522  }
523  if ($self->{setDateStyle} !~ /^(0|1)$/)
524  {
525    $errorString .= "setDateStyle = '$self->{setDateStyle}' is invalid!<br />\n";
526    $valid = 0;
527  }
528  if ($self->{logLevel} !~ /^(0|1|2|3)$/)
529  {
530    $errorString .= "logLevel = '$self->{logLevel}' is invalid!<br />\n";
531    $valid = 0;
532  }
533  if ($self->{longRunningRead} !~ /^\d+$/ || $self->{longRunningRead} < 1)
534  {
535    $errorString .= "longRunningRead = '$self->{longRunningRead}' is invalid!  Must be >= 1.<br />\n";
536    $valid = 0;
537  }
538  if ($self->{longRunningWrite} !~ /^\d+$/ || $self->{longRunningWrite} < 1)
539  {
540    $errorString .= "longRunningWrite = '$self->{longRunningWrite}' is invalid!  Must be >= 1.<br />\n";
541    $valid = 0;
542  }
543  if ($self->{longRunningLog} !~ /^\/.+\.log$/)
544  {
545    $errorString .= "longRunningLog = '$self->{longRunningLog}' is invalid!  Must start with a / and have at least 1 character for the filename and end in .log.<br />\n";
546    $valid = 0;
547  }
548  if ($self->{logSQLStatements} !~ /^[10]$/)
549  {
550    $errorString .= "logSQLStatements = '$self->{logSQLStatements}' is invalid!  Must be 1 or 0.<br />\n";
551    $valid = 0;
552  }
553  if ($self->{logSQLDurations} !~ /^[10]$/)
554  {
555    $errorString .= "logSQLDurations = '$self->{logSQLDurations}' is invalid!  Must be 1 or 0.<br />\n";
556    $valid = 0;
557  }
558  if ($self->{myTimeZone} eq '')
559  {
560    $errorString .= "myTimeZone = '$self->{myTimeZone}' is invalid!<br />\n";
561    $valid = 0;
562  }
563  if ($self->{sqlStatementLog} !~ /^\/.+\.log$/)
564  {
565    $errorString .= "sqlStatementLog = '$self->{sqlStatementLog}' is invalid!  Must start with a / and have at least 1 character for the filename and end in .log.<br />\n";
566    $valid = 0;
567  }
568
569  if (!$valid)
570  {
571    $self->error(errorString => "$errorString" . $self->debugMessage);
572  }
573
574  return $valid;
575}
576
577=item void changeSQLLogFiles(sqlStatementLog, longRunningLog)
578
579Changes the internal variables and re-computes the -start and -stop
580log file names.
581
582=cut
583sub changeSQLLogFiles
584{
585  my $self = shift;
586  my $sqlStatementLog = shift;
587  my $longRunningLog = shift;
588
589  $self->{sqlStatementLog} = $sqlStatementLog;
590  $self->{longRunningLog} = $longRunningLog;
591
592  my $errorString = "";
593  my $valid = 1;
594  if ($self->{longRunningLog} !~ /^\/.+\.log$/)
595  {
596    $errorString .= "longRunningLog = '$self->{longRunningLog}' is invalid!  Must start with a / and have at least 1 character for the filename and end in .log.<br />\n";
597    $valid = 0;
598  }
599
600  if ($self->{sqlStatementLog} !~ /^\/.+\.log$/)
601  {
602    $errorString .= "sqlStatementLog = '$self->{sqlStatementLog}' is invalid!  Must start with a / and have at least 1 character for the filename and end in .log.<br />\n";
603    $valid = 0;
604  }
605  if (!$valid)
606  {
607    $self->error(errorString => "$errorString" . $self->debugMessage);
608  }
609
610  # compute the start and stop log files.
611  ($self->{sqlStatementLogStart} = $self->{sqlStatementLog}) =~ s/\.log$/-start.log/;
612  ($self->{sqlStatementLogStop} = $self->{sqlStatementLog}) =~ s/\.log$/-stop.log/;
613}
614
615=item void close()
616
617  Closes the connection to the database.
618
619=cut
620sub close
621{
622  my $self = shift;
623
624  if (defined $self->{dbh} && ref $self->{dbh} eq "DBI::db")
625  {
626    my $result;
627    eval { $result = $self->{dbh}->disconnect; };
628    if ($@)
629    {
630      $self->error(errorString => "Eval of disconnect failed!<br />\nError = '$@'.<br />\n" . $self->debugMessage);
631      return;
632    }
633    else
634    {
635      eval {
636        if (!$result || $self->{dbh}->err)
637        {
638          $self->error(errorString => "disconnect failed!<br />\n" . $self->{dbh}->errstr . "<br />\n" . $self->debugMessage);
639          return;
640        }
641      };
642      if ($@)
643      {
644        $self->error(errorString => "Eval of result check failed!<br />\nError = '$@'.<br />\n" . $self->debugMessage);
645        return;
646      }
647    }
648    $self->{dbh} = undef;  # signal it is no longer valid!
649  }
650}
651
652=item bool error(errorString)
653
654 This method will set the error condition if an argument is
655 specified.
656
657 The current error state is returned, regardless of if we are
658 setting an error or not.
659
660 A \n is appended to the errorString so you don't have to provide it.
661 errorString is prefixed with the caller's full method name followed
662 by the errorPhrase string.
663
664 You can either specify the errorString value by name:
665
666 $self->error(errorString => "This is an error!");
667
668 or by value:
669
670 $self->error("This is an error!");
671
672 If you specify multiple arguments (in pass by value mode), then
673 we check to see if the first argument contains %'s that are not
674 \ escaped and are not %%.  If this is the case, then the incoming
675 arguments will be passed through sprintf() for formatting, else we
676 just join them with a space ' ' and append them to the current
677 errorString.
678
679
680 To see if an error happened:
681
682 if ($self->error) { die "Error: " . $self->errorMessage; }
683
684=cut
685sub error
686{
687  my $self = shift;
688  my @callerArgs = caller(1);
689  (my $subName = $callerArgs[3]) =~ s/^(.+)(::)([^:]+)$/$1->$3/;
690  my $callerErrStr = "$subName$self->{errorPhrase}";
691
692  if (scalar @_ > 0)
693  {
694    # we are setting an error condition.
695    if (scalar @_ == 1)
696    {
697      $self->{errorString} .= $callerErrStr . $_[0];
698    }
699    else
700    {
701      if ($_[0] eq "errorString")
702      {
703        my %args = ( @_ );
704        if (!exists $args{errorString})  # make sure we get the errorString argument!
705        {
706          $self->error($callerErrStr . "<b>errorString</b> is missing!<br />\n");
707          return;
708        }
709        else
710        {
711          $self->{errorString} .= $callerErrStr . $args{errorString};
712        }
713      }
714      else
715      {
716        # handle the sprintf case.
717        if ($_[0] =~ /(?<!\\)%[^%]/)
718        {
719          # build up the string to eval for the sprintf.
720          my $str = "\"$_[0]\"";
721          for (my $i=1; $i < scalar @_; $i++)
722          {
723            $str .= ", \"$_[$i]\"";
724          }
725          $self->{errorString} .= $callerErrStr;
726          eval "\$self->{errorString} .= sprintf($str);";
727          if ($@)
728          {
729            $self->error($callerErrStr . $@);
730            return;
731          }
732        }
733        else
734        {
735          $self->{errorString} .= $callerErrStr . join(" ", @_);
736        }
737      }
738    }
739    $self->{errorString} .= "\n";
740    $self->{error} = 1;
741  }
742
743  return $self->{error};
744}
745
746=item void setError(errorString)
747
748 DEPRECATED: see error()
749
750 optional: errorString
751 returns: nothing
752 Sets error = 1 and errorString = string passed in.
753 The errorString is prefixed with the caller's full
754 method name followed by the errorPhrase string.
755
756 You can either call as
757 setError(errorString => $string)
758 or setError($string)
759
760 If you do not specify anything, we blow an error
761 telling you to specify errorString.
762
763 \n is appended to the contents of the errorString
764 passed in.
765
766=cut
767
768sub setError
769{
770  my $self = shift;
771  my @callerArgs = caller(1);
772  (my $subName = $callerArgs[3]) =~ s/^(.+)(::)([^:]+)$/$1->$3/;
773  my $callerErrStr = "$subName$self->{errorPhrase}";
774  my $deprecated = "DEPRECATED call to setError!  Convert to using error().<br />\n";
775
776  if (scalar @_ == 1)
777  {
778    $self->{errorString} = $deprecated . $callerErrStr . $_[0];
779  }
780  else
781  {
782    my %args = ( @_ );
783    if (!exists $args{errorString})  # make sure we get the errorString argument!
784    {
785      $self->setError($callerErrStr . "<b>errorString</b> is missing!<br />\n");
786      return;
787    }
788    else
789    {
790      $self->{errorString} = $deprecated . $callerErrStr . $args{errorString};
791    }
792  }
793  $self->{errorString} .= "\n";
794  $self->{error} = 1;
795}
796
797=item void prefixError(errorString)
798
799 optional: errorString
800 returns: nothing
801 Sets error = 1 and prefixes errorString with string passed in.
802 The errorString is prefixed with the caller's full
803 method name followed by the errorPhrase string.
804
805 You can either specify the errorString value by name:
806
807 $self->prefixError(errorString => "This is an error!");
808
809 or by value:
810
811 $self->prefixError("This is an error!");
812
813 If you specify multiple arguments (in pass by value mode), then
814 we check to see if the first argument contains %'s that are not
815 \ escaped and are not %%.  If this is the case, then the incoming
816 arguments will be passed through sprintf() for formatting, else we
817 just join them with a space ' ' and append them to the current
818 errorString.
819
820
821 If you don't specify anything then
822   If you have a previous error, we prefix the caller info to
823     that error message.
824
825=cut
826
827sub prefixError
828{
829  my $self = shift;
830  my @callerArgs = caller(1);
831  (my $subName = $callerArgs[3]) =~ s/^(.+)(::)([^:]+)$/$1->$3/;
832  my $callerErrStr = "$subName$self->{errorPhrase}";
833
834  if (scalar @_ == 1)
835  {
836    $self->{errorString} = $callerErrStr . $_[0] . $self->{errorString} . "\n";
837  }
838  else
839  {
840    if ($_[0] eq "errorString")
841    {
842      my %args = ( @_ );
843      if (!exists $args{errorString})  # make sure we get the errorString argument!
844      {
845        if ($self->{errorString})
846        {
847          # prefix the old errorString value.
848          $self->{errorString} = $callerErrStr . $self->{errorString};
849        }
850        else
851        {
852          $self->error($callerErrStr . "<b>errorString</b> is missing!<br />\n");
853          return;
854        }
855      }
856      else
857      {
858        $self->{errorString} = $callerErrStr . $args{errorString} . "\n" . $self->{errorString};
859      }
860    }
861    else
862    {
863      # handle the sprintf case.
864      if ($_[0] =~ /(?<!\\)%[^%]/)
865      {
866        # build up the string to eval for the sprintf.
867        my $str = "\"$_[0]\"";
868        for (my $i=1; $i < scalar @_; $i++)
869        {
870          $str .= ", \"$_[$i]\"";
871        }
872        my $oldErrorStr = $self->{errorString};
873        $self->{errorString} = $callerErrStr;
874        eval "\$self->{errorString} .= sprintf($str);";
875        if ($@)
876        {
877          $self->error($callerErrStr . $@);
878          return;
879        }
880        $self->{errorString} .= "\n" . $oldErrorStr;
881      }
882      else
883      {
884        $self->{errorString} = $callerErrStr . join(" ", @_) . "\n" . $self->{errorString};
885      }
886    }
887  }
888  $self->{error} = 1;
889}
890
891=item scalar didErrorOccur(void)
892
893 DEPRECATED: see error()
894
895 Returns the value of error.
896
897=cut
898
899sub didErrorOccur
900{
901  my $self = shift;
902
903  return $self->{error};
904}
905
906=item scalar errorMessage(void)
907
908 Returns the value of errorString.
909
910=cut
911
912sub errorMessage
913{
914  my $self = shift;
915
916  return $self->{errorString};
917}
918
919=item scalar errorStr(void)
920
921 Returns the value of errorString.
922
923 Alternative to errorMessage().
924
925=cut
926
927sub errorStr
928{
929  my $self = shift;
930
931  return $self->{errorString};
932}
933
934=item void resetError(void)
935
936 Resets the error condition flag and string.
937
938=cut
939
940sub resetError
941{
942  my $self = shift;
943
944  $self->{error} = 0;
945  $self->{errorString} = "";
946}
947
948=item void commit()
949
950 causes the database to commit the current transaction.  Only works
951 if AutoCommit is set to 0 and the database supports Transactions.
952
953=cut
954sub commit
955{
956  my $self = shift;
957
958  if (!$self->{supportsTransactions})
959  {
960    return;
961  }
962  eval { $self->{dbh}->commit; };
963  if ($@)
964  {
965    $self->error(errorString => "commit failed!<br />\nError = $@" . "<br />\n" . $self->debugMessage);
966  }
967  elsif ($DBI::err)
968  {
969    $self->error(errorString => "commit failed!<br />\nError = $DBI::errstr" . "<br />\n" . $self->debugMessage);
970  }
971}
972
973=item void rollback()
974
975 causes the database to rollback the current transaction.  Only
976 works if AutoCommit is set to 0 and the database supports
977 Transactions.
978
979=cut
980sub rollback
981{
982  my $self = shift;
983
984  if (!$self->{supportsTransactions})
985  {
986    return;
987  }
988  eval { $self->{dbh}->rollback; };
989  if ($@)
990  {
991    $self->error(errorString => "rollback failed!<br />\nError = $@" . "<br />\n" . $self->debugMessage);
992  }
993  elsif ($DBI::err)
994  {
995    $self->error(errorString => "rollback failed!<br />\nError = $DBI::errstr" . "<br />\n" . $self->debugMessage);
996  }
997}
998
999=item ref read(sql => "", plug => [], substitute => [])
1000
1001 (This function should only be called for SELECT statements).
1002 executes the specified sql statement passing in any values in plug
1003 to the execute method after doing any substitutions that are in
1004 substitute.  The resulting sql data is passed back to the user as a
1005 reference for them to do with as they please.
1006
1007=cut
1008sub read
1009{
1010  my $self = shift;
1011  my $sql = "";
1012  my @plug = ();
1013  my @substitute = ();
1014  if (scalar @_ == 1)
1015  {
1016    $sql = shift;
1017  }
1018  else
1019  {
1020    my %args = ( plug => [], substitute => [], @_ );
1021    @plug = @{$args{'plug'}};
1022    @substitute = @{$args{'substitute'}};
1023
1024    $sql = $args{'sql'};
1025  }
1026  # validate we got a sql statement to work with.
1027  if (length $sql == 0)
1028  {
1029    $self->error(errorString => "SQL string not passed in!" . "<br />\n" . $self->debugMessage);
1030
1031    return undef;
1032  }
1033
1034  # check and see if we need to do any substitutions.
1035  if (scalar @substitute > 0)
1036  {
1037    for (my $i=0; $i < scalar @substitute; $i++)
1038    {
1039      my $temp_string = "\\#\\#\\?" . ($i+1) . "\\#\\#";
1040      $sql =~ s/$temp_string/$substitute[$i]/g;
1041    }
1042  }
1043
1044  my $timerStart = $self->getCurrTime();
1045  $self->logSQLStatement("read", $timerStart, $sql, \@plug) if ($self->{logSQLStatements});
1046
1047  # now prepare the statement
1048  my $sth;
1049  eval {
1050    $sth = $self->{dbh}->prepare($sql);
1051  };
1052  if ($@)
1053  {
1054    $self->error(errorString => "Eval of prepare failed!<br />\nError = '$@'.<br />\nsql='$sql'.<br />\nplug='" . join("', '", @plug) . "'.<br />\n" . $self->debugMessage);
1055
1056    return undef;
1057  }
1058  elsif (!$sth || $DBI::err)
1059  {
1060    $self->error(errorString => "Preparing failed!<br />\n" . $DBI::errstr . "<br />\nsql='$sql'.<br />\nplug='" . join("', '", @plug) . "'.<br />\n" . $self->debugMessage);
1061
1062    return undef;
1063  }
1064
1065  # now execute the sql statement passing in any parameters given via plug
1066  my $rc;
1067  my $done = 0;
1068  my $numTries = 0;
1069  while (!$done)
1070  {
1071    $deadlockEncountered = 0;  # make sure we turn it off.
1072    eval {
1073      $rc = $sth->execute(@plug);
1074    };
1075    if (!$deadlockEncountered)
1076    {
1077      $done = 1;
1078      if ($@)
1079      {
1080        $self->error(errorString => "Eval of execute failed!<br />\nError = '$@'.<br />\nsql='$sql'.<br />\nplug='" . join("', '", @plug) . "'.<br />\n" . $self->debugMessage);
1081
1082        $self->checkLongRunningSQL("read", $timerStart, $numTries, $sql, \@plug);
1083        return undef;
1084      }
1085      elsif (!$rc || $DBI::err)
1086      {
1087        $self->error(errorString => "Execute failed!<br />\n" . $DBI::errstr . "<br />\nsql='$sql'.<br />\nplug='" . join("', '", @plug) . "'.<br />\n" . $self->debugMessage);
1088
1089        $self->checkLongRunningSQL("read", $timerStart, $numTries, $sql, \@plug);
1090        return undef;
1091      }
1092    }
1093    else
1094    {
1095      $numTries++;
1096      if ($numTries >= $self->{deadlockNumTries})
1097      {
1098        $self->checkLongRunningSQL("read", $timerStart, $numTries, $sql, \@plug);
1099        return undef;
1100      }
1101      my $sleep = $self->{deadlockSleep};
1102      if ($numTries > 1 && $self->{deadlockRampSleep})
1103      {
1104        $sleep *= $numTries;
1105      }
1106      print "ALERT:  deadlock encountered!  Retrying sql.  Attempt #$numTries.  Sleeping for $sleep seconds.\n";
1107      sleep($sleep);
1108    }
1109  }
1110
1111  $self->checkLongRunningSQL("read", $timerStart, $numTries, $sql, \@plug);
1112  return $sth;
1113}
1114=item $ getCurrTime()
1115
1116Returns the current time as a DBIWrapper::Time::Now::HiRes instance.
1117
1118=cut
1119sub getCurrTime
1120{
1121  my $self = shift;
1122  my $dt = DBIWrapper::Time::Now::HiRes->new;
1123
1124  return $dt;
1125}
1126
1127=item $ getCurrTimeFormatted(offset, showMillisecond)
1128
1129offset must be specified and is the number of seconds from now
1130that the time should be computed for.
1131
1132If showMillisecond is defined then I output the .mmm part.
1133
1134Returns a formatted timestamp as YYYY-MM-DD HH:MM:SS.mmm.
1135
1136Normal usage is getCurrTimeFormatted(0, 1).
1137
1138NOTE: offset is currently being ignored and is deprecated.
1139
1140=cut
1141sub getCurrTimeFormatted
1142{
1143  my $self = shift;
1144  my $offset = shift;
1145  my $showMillisecond = shift;
1146
1147  my $dt = DBIWrapper::Time::Now::HiRes->new;
1148  my $currTS_AZ = $dt->ymd . " " . $dt->hms . (defined $showMillisecond && $showMillisecond ? "." . $dt->millisecond : "");
1149
1150  return $currTS_AZ;
1151}
1152
1153=item $ getCurrTimeFormattedFromObj(dt, showMillisecond)
1154
1155dt must be specified and is the DBIWrapper::Time::Now::HiRes object
1156to work with.
1157
1158If showMillisecond is defined then I output the .mmm part.
1159
1160Returns a formatted timestamp as YYYY-MM-DD HH:MM:SS.mmm.
1161
1162Normal usage is getCurrTimeFormattedFromObj($dt, 1).
1163
1164=cut
1165sub getCurrTimeFormattedFromObj
1166{
1167  my $self = shift;
1168  my $dt = shift;
1169  my $showMillisecond = shift;
1170  $dt->set_time_zone($self->{myTimeZone});  # just to be sure.
1171  # I can't rely on millisecond being <= 999 until all code is only using DBIWrapper::Time::Now::HiRes modules.
1172  my $millisecond = ($dt->millisecond > 999 ? 999 : $dt->millisecond);  # Have to get around sybase precision issues.
1173  my $currTS_AZ = $dt->ymd . " " . $dt->hms . (defined $showMillisecond && $showMillisecond ? "." . $millisecond : "");
1174
1175  return $currTS_AZ;
1176}
1177
1178=item $ getCurrTimeFormattedFromEpoch(epoch, showMillisecond)
1179
1180epoch must be specified and is the epoch timestamp
1181to work with.
1182
1183If showMillisecond is defined then I output the .mmm part.
1184
1185Returns a formatted timestamp as YYYY-MM-DD HH:MM:SS.mmm.
1186
1187Normal usage is getCurrTimeFormattedFromEpoch($timestampEpoch, 1).
1188
1189=cut
1190sub getCurrTimeFormattedFromEpoch
1191{
1192  my $self = shift;
1193  my $epoch = shift;
1194  my $showMillisecond = shift;
1195  my $dt = DateTime->from_epoch(epoch => $epoch);
1196  $dt->set_time_zone($self->{myTimeZone});  # just to be sure.
1197  # I can't rely on millisecond being <= 999 until all code is only using DBIWrapper::Time::Now::HiRes modules.
1198  my $millisecond = ($dt->millisecond > 999 ? 999 : $dt->millisecond);  # Have to get around sybase precision issues.
1199  my $currTS_AZ = $dt->ymd . " " . $dt->hms . (defined $showMillisecond && $showMillisecond ? "." . $millisecond : "");
1200
1201  return $currTS_AZ;
1202}
1203
1204sub prefix
1205{
1206  my $self = shift;
1207
1208  return "[$$ " . $self->getCurrTimeFormatted(0, 1) . "]";
1209}
1210
1211=item $ computeDuration(startTime, endTime)
1212
1213Required:
1214
1215startTime - DBIWrapper::Time::Now::HiRes object
1216
1217Optional:
1218
1219endTime - DBIWrapper::Time::Now::HiRes object
1220
1221Computes the duration between the given timestamps, down
1222to the millisecond level, and displays as the # of seconds it took.
1223
1224Returns a string containing the duration.
1225
1226=cut
1227sub computeDuration
1228{
1229  my $self = shift;
1230  my $startTime = shift;
1231  my $endTime = shift;
1232
1233  if (!defined $startTime)
1234  {
1235    die "computeDuration() did not specify startTime!\n";
1236  }
1237
1238  $endTime = $self->getCurrTime() if (!defined $endTime);
1239  my $dur = $endTime->subtract_datetime_absolute($startTime);
1240  my $durStr = $dur->seconds() . "." . floor($dur->nanoseconds() / 1000000) . " seconds";
1241  return $durStr;
1242}
1243
1244=item void checkLongRunningSQL(method, timerStart, numTries, sql, plug)
1245
1246Compares $self->getCurrTime() - timerStart against the longRunningRead/Write threshold and
1247if it's >= then logs to longRunningLog.
1248
1249Doesn't return anything.
1250
1251=cut
1252sub checkLongRunningSQL
1253{
1254  my $self = shift;
1255  my $method = shift;
1256  my $timerStart = shift;
1257  my $timerStop = $self->getCurrTime();
1258  my $numTries = shift;
1259  my $sql = shift;
1260  my $plug = shift;  # arrayref
1261
1262  my $duration = $self->computeDuration($timerStart, $timerStop);
1263  my $timerStartFormatted = $self->getCurrTimeFormattedFromObj($timerStart, 1);
1264  my $timerStopFormatted = $self->getCurrTimeFormattedFromObj($timerStop, 1);
1265  (my $methodUpper = $method) =~ s/^(.)(.+)$/uc($1) . $2/e;
1266  # turn all newlines in the sql statement into spaces.
1267  $sql =~ s/\n/$self->{sqlNewlineReplacement}/mg;
1268  $sql =~ s/\|/\\\|/mg;  # make sure to escape the |'s.
1269  (my $sqlShort = $sql) =~ s/^(.{1,20})(.+)$/$1/;
1270  my $plugStr = join(', ', @{$plug});
1271  $plugStr =~ s/\n/$self->{sqlPlugNewlineReplacement}/mg;
1272  $plugStr =~ s/\|/\\\|/mg;  # make sure to escape the |'s.
1273  #print "checkLongRunningSQL($method [$methodUpper], $timerStart, $timerStop, $duration, $numTries, $sql)\n";
1274  my $uniqueID = sha1_hex($0 . $$ . $timerStartFormatted . $method . $sql);
1275  if (int($duration) >= $self->{"longRunning$methodUpper"})  # only compare the seconds part.
1276  {
1277    # we have a long running sql statement!  Log
1278    open F, ">>$self->{longRunningLog}" or die "Couldn't open '$self->{longRunningLog}' for append!  $!\n";
1279    print F "$0|$$|" . $timerStart->hires_epoch() . " ($timerStartFormatted)|" . $timerStop->hires_epoch() . " ($timerStopFormatted)|$duration|$method|$deadlockEncountered|$numTries|" . ($self->{dbType} eq "Sybase" ? $self->{server} : $self->{dbHost}) . "|$self->{dbName}|$sql|$plugStr|$uniqueID\n";
1280    close F;
1281  }
1282
1283  if ($self->{logSQLStatements} && $self->{logSQLDurations})
1284  {
1285    # generate the stop file entry.
1286    open F, ">>$self->{sqlStatementLogStop}" or die "Couldn't open '$self->{sqlStatementLogStop}' for append!  $!\n";
1287    print F "$uniqueID|$0|$$|" . $timerStart->hires_epoch() . " ($timerStartFormatted)|" . $timerStop->hires_epoch() . " ($timerStopFormatted)|$duration|$sqlShort|" . int(length $sql) . "|$plugStr|$deadlockEncountered|$numTries\n";
1288    close F;
1289  }
1290}
1291
1292=item void logSQLStatement(method, timerStart, sql, plug)
1293
1294Logs the sql being run.
1295
1296Doesn't return anything.
1297
1298=cut
1299sub logSQLStatement
1300{
1301  my $self = shift;
1302  my $method = shift;
1303  my $timerStart = shift;
1304  my $sql = shift;
1305  my $plug = shift;  # arrayref
1306
1307  my $timerStartFormatted = $self->getCurrTimeFormattedFromObj($timerStart, 1);
1308  # turn all newlines in the sql statement into spaces.
1309  $sql =~ s/\n/$self->{sqlNewlineReplacement}/mg;
1310  $sql =~ s/\|/\\\|/mg;  # make sure to escape the |'s.
1311  (my $sqlShort = $sql) =~ s/^(.{1,20})(.+)$/$1/;
1312  my $plugStr = join(', ', @{$plug});
1313  $plugStr =~ s/\n/$self->{sqlPlugNewlineReplacement}/mg;
1314  $plugStr =~ s/\|/\\\|/mg;  # make sure to escape the |'s.
1315  #print "logSQLStatement($method, $timerStart, $sql)\n";
1316  my $uniqueID = sha1_hex($0 . $$ . $timerStartFormatted . $method . $sql);
1317  open F, ">>$self->{sqlStatementLog}" or die "Couldn't open '$self->{sqlStatementLog}' for append!  $!\n";
1318  print F "$0|$$|" . $timerStart->hires_epoch() . " ($timerStartFormatted)|$method|" . ($self->{dbType} eq "Sybase" ? $self->{server} : $self->{dbHost}) . "|$self->{dbName}|$sql|$plugStr|$uniqueID\n";
1319  close F;
1320
1321  if ($self->{logSQLDurations})
1322  {
1323    # generate the start file entry.
1324    open F, ">>$self->{sqlStatementLogStart}" or die "Couldn't open '$self->{sqlStatementLogStart}' for append!  $!\n";
1325    print F "$uniqueID|$0|$$|" . $timerStart->hires_epoch() . " ($timerStartFormatted)|$sqlShort|" . int(length $sql) . "|$plugStr\n";
1326    close F;
1327  }
1328}
1329
1330=item @ getDataArray(sql, plug, substitute)
1331
1332 requires: sql
1333 optional: plug, substitute
1334 returns: array of arrayrefs as the result of
1335   $sth->fetchall_arrayref
1336
1337 See read() for argument info.
1338
1339=cut
1340sub getDataArray
1341{
1342  my $self = shift;
1343  my $sql = "";
1344  my $sth = undef;
1345  if (scalar @_ == 1)
1346  {
1347    $sql = shift;
1348    $sth = $self->read($sql);
1349  }
1350  else
1351  {
1352    my %args = ( plug => [], substitute => [], @_ );
1353    $sth = $self->read(%args);
1354  }
1355
1356  my @data = ();
1357
1358  if ($self->error)
1359  {
1360    $self->prefixError();
1361    return @data;
1362  }
1363
1364  while (my $row = $sth->fetchrow_arrayref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results}))
1365  {
1366    if (defined $row && ref($row) eq "ARRAY" && scalar @{$row} > 0)
1367    {
1368      # don't process the return status of a stored procedure from Sybase.
1369      next if ($self->{dbType} eq "Sybase" && $sth->{syb_result_type} == 4043);
1370
1371      my @row = @{$row};
1372      push @data, \@row;
1373    }
1374  }
1375
1376  $sth->finish;
1377
1378  return @data;
1379}
1380
1381=item @ getDataHash(sql, plug, substitute, case)
1382
1383 requires: sql
1384 optional: plug, substitute, case
1385 returns: array of hashrefs where the column names are
1386   case preserved if case = 1, or lowercased if case = 0.
1387
1388 case defaults to 0 (lowercase).
1389
1390 See read() for argument info.
1391
1392=cut
1393sub getDataHash
1394{
1395  my $self = shift;
1396  my $sql = "";
1397  my $case = 0;
1398  my $sth = undef;
1399  if (scalar @_ == 1)
1400  {
1401    $self->{dbh}->{FetchHashKeyName} = 'NAME_lc';
1402    $sql = shift;
1403    $sth = $self->read($sql);
1404  }
1405  else
1406  {
1407    my %args = ( plug => [], substitute => [], case => 0, @_ );
1408    my $case = $args{case};
1409    $self->{dbh}->{FetchHashKeyName} = ($case == 1 ? 'NAME' : 'NAME_lc');
1410    $sth = $self->read(%args);
1411  }
1412
1413  my @data = ();
1414
1415  if ($self->error)
1416  {
1417    $self->prefixError();
1418    return @data;
1419  }
1420
1421  while (my $row = $sth->fetchrow_hashref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results}))
1422  {
1423    if (defined $row && ref($row) eq "HASH" && scalar keys %{$row} > 0)
1424    {
1425      # don't process the return status of a stored procedure from Sybase.
1426      next if ($self->{dbType} eq "Sybase" && $sth->{syb_result_type} == 4043);
1427
1428      my %row = %{$row};
1429      push @data, \%row;
1430    }
1431  }
1432
1433  $sth->finish;
1434
1435  return @data;
1436}
1437
1438=item @ getDataArrayHeader(sql, plug, substitute, case)
1439
1440 requires: sql
1441 optional: plug, substitute, case
1442 returns: array of arrayrefs
1443
1444   The first row of the array is an array containing the
1445   column names in the order returned by the database.
1446   The column names are case preserved if case = 1, or
1447   lowercased if case = 0.
1448
1449   NOTE:
1450   If 0 rows were returned, we still return an array with
1451   1 row in it, which is the header row.
1452
1453 case defaults to 0 (lowercase).
1454
1455 See read() for argument info.
1456
1457=cut
1458sub getDataArrayHeader
1459{
1460  my $self = shift;
1461  my $sql = "";
1462  my $case = 0;
1463  my $sth = undef;
1464  if (scalar @_ == 1)
1465  {
1466    $sql = shift;
1467    $sth = $self->read($sql);
1468  }
1469  else
1470  {
1471    my %args = ( plug => [], substitute => [], case => 0, @_ );
1472    my $case = $args{case};
1473    $sth = $self->read(%args);
1474  }
1475
1476  my @data = ();
1477
1478  if ($self->error)
1479  {
1480    $self->prefixError();
1481    return @data;
1482  }
1483
1484  # get the column NAMES
1485  push @data, $sth->{($case == 1 ? 'NAME' : 'NAME_lc')};
1486
1487  while (my $row = $sth->fetchrow_arrayref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results}))
1488  {
1489    if (defined $row && ref($row) eq "ARRAY" && scalar @{$row} > 0)
1490    {
1491      # don't process the return status of a stored procedure from Sybase.
1492      next if ($self->{dbType} eq "Sybase" && $sth->{syb_result_type} == 4043);
1493
1494      my @row = @{$row};
1495      push @data, \@row;
1496    }
1497  }
1498
1499  $sth->finish;
1500
1501  return @data;
1502}
1503
1504=item @ getDataHashHeader(sql, plug, substitute, case)
1505
1506 requires: sql
1507 optional: plug, substitute, case
1508 returns: array of hashrefs where the column names are
1509   case preserved if case = 1, or lowercased if case = 0.
1510
1511   The first row of the array is an array containing the
1512   column names in the order returned by the database.
1513   The column names respect the case flag.
1514
1515   NOTE:
1516   If 0 rows were returned, we still return an array with
1517   1 row in it, which is the header row.
1518
1519 case defaults to 0 (lowercase).
1520
1521 See read() for argument info.
1522
1523=cut
1524sub getDataHashHeader
1525{
1526  my $self = shift;
1527  my $sql = "";
1528  my $case = 0;
1529  my $sth = undef;
1530  if (scalar @_ == 1)
1531  {
1532    $self->{dbh}->{FetchHashKeyName} = 'NAME_lc';
1533    $sql = shift;
1534    $sth = $self->read($sql);
1535  }
1536  else
1537  {
1538    my %args = ( plug => [], substitute => [], case => 0, @_ );
1539    my $case = $args{case};
1540    $self->{dbh}->{FetchHashKeyName} = ($case == 1 ? 'NAME' : 'NAME_lc');
1541    $sth = $self->read(%args);
1542  }
1543
1544  my @data = ();
1545
1546  if ($self->error)
1547  {
1548    $self->prefixError();
1549    return @data;
1550  }
1551
1552  # get the column NAMES
1553  push @data, $sth->{($case == 1 ? 'NAME' : 'NAME_lc')};
1554
1555  while (my $row = $sth->fetchrow_hashref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results}))
1556  {
1557    if (defined $row && ref($row) eq "HASH" && scalar keys %{$row} > 0)
1558    {
1559      # don't process the return status of a stored procedure from Sybase.
1560      next if ($self->{dbType} eq "Sybase" && $sth->{syb_result_type} == 4043);
1561
1562      my %row = %{$row};
1563      push @data, \%row;
1564    }
1565  }
1566
1567  $sth->finish;
1568
1569  return @data;
1570}
1571
1572=item scalar readXML(sql, plug, substitute, columns, displayNULLAs,
1573ignoreTags, sequence, displaySQL)
1574
1575 requires: sql
1576 optional: plug, substitute, columns = 0, displayNULLAs, ignoreTags,
1577           sequence, displaySQL = 1
1578 returns:  valid XML document describing the data selected from the
1579  database.  Uses getDataHashHeader() to actually validate the data and
1580  execute the SELECT statement.  The resulting XML document
1581  will either have an error condition set (if read() signaled
1582  an error occured) or will be the result of traversing the
1583  data returned from getDataHashHeader().
1584
1585  If displaySQL = 0, then we do not output the <select />
1586  tag in the xml, thus allowing you to send the xml to a web browser
1587  without potentially giving out sensitive information.
1588
1589  Any undefined values (NULL) will be output using the displayNULLAs
1590  variable which defaults to 'NULL'.
1591
1592  All values are run through the formEncodeString() method to
1593  make sure that any html/xml tags are properly encoded.  If you
1594  do not want certain tags encoded, use the ignoreTags and/or
1595  sequence arguments to affect how the formEncodeString() method
1596  fixes up the value.  See the formEncodeString() documentation for
1597  more details.
1598
1599  If columns = 0, then all info will be returned in the <row>
1600  tag as attributes where the column name = column value.
1601  Ex.  <row name="test" value="testing" other="something else"/>
1602  When the column names were name, value and other.
1603
1604  If columns = 1, then all info will be returned in <column>
1605  tags which are children of the <row> tag.  A column tag has
1606  attributes name and value.  name = column name and value =
1607  column value.
1608  Ex.
1609  <row>
1610    <column name="name" value="test"/>
1611    <column name="value" value="testing"/>
1612  </row>
1613
1614  If columns = 2, then each row has tags defined named after the
1615  column with the contents being the value.  They are output in the
1616  order that the database returned them in.  The column value is not
1617  encoded, but is wrapped in <![CDATA[ ]]> tags so that any html/xml
1618  tags are safely ignored without having to be encoded.
1619  Ex:
1620  <row>
1621    <name><![CDATA[test]]></name>
1622    <value><![CDATA[testing]]></value>
1623  </row>
1624
1625  Where there were 2 columns returned with names of name and value, in
1626  that order.
1627
1628  The XML format is as follows:
1629  <?xml version="1.0" encoding="ISO-8859-1"?>
1630  <resultset version="1.2">
1631    <select sql="" plug=""/>
1632    <status result="" error=""/>
1633    <rows numRows="" columns="0|1|2">
1634      <row/>
1635    </rows>
1636  </resultset>
1637
1638  If the XML document is an error document, then:
1639  <status result="Error" error="Error message"/>
1640  else
1641  <status result="Ok" error=""/>
1642
1643  In <select> tag, sql = The sql SELECT string, plug = the
1644  string made when joining all the plug array entries
1645  together and comma seperating them.  The entries are
1646  single quoted.  Ex. plug="''" would represent no plug
1647  entries used.  plug="'x', 'y'" would mean that 2 entries
1648  were passed in: x, y.
1649
1650  In <rows> numRows will be equal to the number of rows
1651  being returned or 0 if an error had occured.
1652
1653  The <row> tag will depend on the value of columns.
1654
1655=cut
1656sub readXML
1657{
1658  my $self = shift;
1659  my $sql = "";
1660  my @plug = ();
1661  my $plug = "";
1662  my $columns = 0;
1663  my $displayNULLAs = 'NULL';
1664  my $ignoreTags = "";
1665  my $sequence = "";
1666  my $displaySQL = 1;
1667  my $sth = undef;
1668  my @data = ();
1669  if (scalar @_ == 1)
1670  {
1671    $sql = shift;
1672    @data = $self->getDataHashHeader($sql);
1673  }
1674  else
1675  {
1676    my %args = ( plug => [], substitute => [], columns => 0, displayNULLAs => 'NULL', displaySQL => 1, @_ );
1677    @plug = @{$args{plug}};
1678    $plug = "'" . join("', '", @plug) . "'";
1679    $sql = $args{sql};
1680    $columns = $args{columns};
1681    $displayNULLAs = $args{displayNULLAs};
1682    $ignoreTags = $args{ignoreTags};
1683    $sequence = $args{sequence};
1684    $displaySQL = $args{displaySQL};
1685    @data = $self->getDataHashHeader(%args);
1686  }
1687  my $xmlDoc = "";
1688
1689  my $resultSetVersion = "1.2";  # Update whenever the xml format changes.
1690
1691  # make sure we don't have any invalid XML characters in the sql or plug strings.
1692  $sql = $self->formEncodeString(string => $sql, ignoreTags => $ignoreTags, sequence => $sequence);
1693  $plug = $self->formEncodeString(string => $plug, ignoreTags => $ignoreTags, sequence => $sequence);
1694
1695  if ($self->error)
1696  {
1697    $self->prefixError();
1698    $self->{error} = 0;  # turn off the error message as the XML file will convey it.
1699    my $errorString = $self->formEncodeString(string => $self->errorString, ignoreTags => $ignoreTags, sequence => $sequence);
1700
1701    $xmlDoc = <<"END_OF_CODE";
1702<?xml version="1.0" encoding="ISO-8859-1"?>
1703<resultset version="$resultSetVersion">
1704END_OF_CODE
1705    $xmlDoc .= qq(  <select sql="$sql" plug="$plug"/>\n) if $displaySQL;
1706    $xmlDoc .= <<"END_OF_CODE";
1707  <status result="Error" error="$errorString"/>
1708  <rows numRows="0" columns="$columns"/>
1709</resultset>
1710END_OF_CODE
1711  }
1712  else
1713  { # now process the result set returned and generate the XML document.
1714    $xmlDoc = <<"END_OF_CODE";
1715<?xml version="1.0" encoding="ISO-8859-1"?>
1716<resultset version="$resultSetVersion">
1717END_OF_CODE
1718    $xmlDoc .= qq(  <select sql="$sql" plug="$plug"/>\n) if $displaySQL;
1719    $xmlDoc .= <<"END_OF_CODE";
1720  <status result="Ok" error=""/>
1721  <rows numRows="#NUMROWS#" columns="$columns">
1722END_OF_CODE
1723    my $names = $data[0];  # get the list of column names returned.
1724
1725    # fixup the names to make sure they don't have any invalid XML characters in them.
1726    my %encodedNames = ();
1727    foreach my $name (@{$names})
1728    {
1729      $encodedNames{$name} = $self->fixupAttributes(string => $name);
1730    }
1731
1732    for(my $rowIndex = 1; $rowIndex < @data; $rowIndex++)
1733    {
1734      my $row = $data[$rowIndex];
1735
1736      if ($columns)
1737      {
1738        # start the <row>
1739        $xmlDoc .= "    <row>\n";
1740
1741        # now iterate over each entry in the row and create it's column tag.
1742        if (exists $row->{$names->[0]} && scalar @{$names} == keys %{$row})
1743        {  # they hopefully are the same row/header info.
1744          foreach my $name (@{$names})
1745          {
1746            my $data = $row->{$name};
1747            $data = $displayNULLAs if (not defined $data);  # set NULL entries to be NULL
1748            my $encodedData = $self->formEncodeString(string => $data, ignoreTags => $ignoreTags, sequence => $sequence);
1749            $encodedNames{$name} = $self->fixupAttributes(string => $name) if (!exists $encodedNames{$name});
1750
1751            if ($columns == 1)
1752            {
1753              $xmlDoc .= "      <column name=\"$encodedNames{$name}\" value=\"$encodedData\"/>\n";
1754            }
1755            elsif ($columns == 2)
1756            {
1757              $xmlDoc .= "      <$encodedNames{$name}><![CDATA[$data]]></$encodedNames{$name}>\n";
1758            }
1759          }
1760        }
1761        else
1762        {
1763          foreach my $name (keys(%{$row}))
1764          {
1765            my $data = $row->{$name};
1766            $data = $displayNULLAs if (not defined $data);  # set NULL entries to be NULL
1767            my $encodedData = $self->formEncodeString(string => $data, ignoreTags => $ignoreTags, sequence => $sequence);
1768            $encodedNames{$name} = $self->fixupAttributes(string => $name) if (!exists $encodedNames{$name});
1769
1770            if ($columns == 1)
1771            {
1772              $xmlDoc .= "      <column name=\"$encodedNames{$name}\" value=\"$encodedData\"/>\n";
1773            }
1774            elsif ($columns == 2)
1775            {
1776              $xmlDoc .= "      <$encodedNames{$name}><![CDATA[$data]]></$encodedNames{$name}>\n";
1777            }
1778          }
1779        }
1780
1781        # end the <row>
1782        $xmlDoc .= "    </row>\n";
1783      }
1784      else
1785      {
1786        # start the <row>
1787        $xmlDoc .= "    <row";
1788
1789        if (exists $row->{$names->[0]} && scalar @{$names} == keys %{$row})
1790        {  # they hopefully are the same row/header info.
1791          foreach my $name (@{$names})
1792          {
1793            my $data = $row->{$name};
1794            $data = $displayNULLAs if (not defined $data);  # set NULL entries to be NULL
1795            $data = $self->formEncodeString(string => $data, ignoreTags => $ignoreTags, sequence => $sequence);
1796            $encodedNames{$name} = $self->fixupAttributes(string => $name) if (!exists $encodedNames{$name});
1797
1798            $xmlDoc .= " $encodedNames{$name}=\"$data\"";
1799          }
1800        }
1801        else
1802        {
1803          foreach my $name (keys %{$row})
1804          {
1805            my $data = $row->{$name};
1806            $data = $displayNULLAs if (not defined $data);  # set NULL entries to be NULL
1807            $data = $self->formEncodeString(string => $data, ignoreTags => $ignoreTags, sequence => $sequence);
1808            $encodedNames{$name} = $self->fixupAttributes(string => $name) if (!exists $encodedNames{$name});
1809
1810            $xmlDoc .= " $encodedNames{$name}=\"$data\"";
1811          }
1812        }
1813
1814        $xmlDoc .= "/>\n"; # end the <row>
1815      }
1816    }
1817    $xmlDoc .= <<"END_OF_CODE";
1818  </rows>
1819</resultset>
1820END_OF_CODE
1821
1822    # now update the numRows value.
1823    my $numRows = (scalar(@data) - 1);
1824    $xmlDoc =~ s/#NUMROWS#/$numRows/;
1825  }
1826
1827  return $xmlDoc;
1828}
1829
1830=item scalar fixupAttributes(string)
1831
1832Attempts to make sure that the given string can be a valid attribute in
1833an xml document.
1834
1835Converts (, ), -, \, /, =, >, <, & to _
1836Deletes ', ", \n
1837
1838=cut
1839sub fixupAttributes
1840{
1841  my $self = shift;
1842  my $string = "";
1843  if (scalar @_ == 1)
1844  {
1845    $string = shift;
1846  }
1847  else
1848  {
1849    my %args = ( string => "", @_ );
1850    $string = $args{string};
1851  }
1852
1853  my @replace = ('&', '<', '>', '-', '\\(', '\\)', '\\\\', '/', '=', ' ', '\\+', '\\*');
1854  my @delete = ("'", '"', '\n');
1855
1856  return $string if (length $string == 0);
1857
1858  foreach my $entity (@replace)
1859  {
1860    $string =~ s/$entity/_/g;
1861  }
1862
1863  foreach my $entity (@delete)
1864  {
1865    $string =~ s/$entity//g;
1866  }
1867
1868  return $string;
1869}
1870
1871=item scalar readHTML(sql, plug, substitute, tableClass, alternateRows, displayNumRows, displayNULLAs, ignoreTags, sequence, headers, footer)
1872
1873 requires: sql
1874 optional: plug, substitute, tableClass, alternateRows,
1875  displayNumRows, displayNULLAs, ignoreTags, sequence, headers, footer
1876 returns:  valid HTML <table> describing the data selected from the
1877  database.  Uses getDataHashHeader() to actually validate the data and
1878  execute the SELECT statement.  The resulting HTML <table>
1879  will either have the error displayed (if read() signaled
1880  an error occured) or will be the result of traversing the
1881  data returned from getDataHashHeader().
1882
1883  Any undefined values (NULL) will be output using the displayNULLAs
1884  variable which defaults to 'NULL'.
1885
1886  All values are run through the formEncodeString() method to
1887  make sure that any html/xml tags are properly encoded.  If you
1888  do not want certain tags encoded, use the ignoreTags and/or
1889  sequence arguments to affect how the formEncodeString() method
1890  fixes up the value.  See the formEncodeString() documentation for
1891  more details.
1892
1893 If an error occured, then the generated tr and td will have
1894 class="sqlError" assigned to them so you can change the way the
1895 sql Error row is displayed.  The error output will also be
1896 wrapped in a <span class="sqlError"></span> so you can change
1897 the display behaviour.
1898
1899 tableClass defines the class to assign to this table so it knows
1900 how to display itself.  Defaults to "".  This allows you to have
1901 different readHTML generated tables on the same page and let them
1902 look different (border, width, cellspacing, cellpadding, etc.).
1903
1904 alternateRows (boolean) lets the caller indicate they want to
1905 possibly have different row characteristics on every other row.
1906 Defaults to 1.
1907
1908 displayNumRows (boolean) lets the caller indicate they want a <div>
1909 above the generated table that tells the user how many rows were
1910 returned.  Defaults to 1.  The generated div has
1911 class="sqlNumRows" assigned to it so the caller can affect the
1912 characteristics of the output and the NumRows statement is wrapped
1913 in a <span class="sqlNumRows"></span>.
1914
1915 The table header will be made up from the returned columns in the
1916 sql statement.  Each <th> will have the css class="column_name"
1917 defined so that the callers style can have th.column_name defined
1918 to dictate how the <th> is to be displayed.  The <tr> for the table
1919 header will have class="sqlHeader" assigned to it.  **
1920
1921 The headings can be specified by passing in a reference to a hash
1922 called headers.  If you wish to use special characters and/or
1923 simply change the label for a column that was used in the SQL assign
1924 it to the hash entry with the column as key.
1925   $headers{column1} = "Some other text";
1926   readHTML(headers => \%headers);
1927 Any columns not specified in the hash will default to the name used
1928 in the sql query.
1929
1930 The footer flag is boolean (0|1) and defaults to 0.  If set to 1 the
1931 heading row will be duplicated as a footer row inside of <tfoot> tags.
1932
1933 Each <tr> will have class="sqlRow" assigned, unless alternateRows
1934 is enabled, which then causes the even rows to have
1935 class="sqlRow sqlEven" and the odd rows to have class="sqlRow sqlOdd"
1936 assigned. Each <td> will have the css class="column_name" defined so
1937 the callers style can have td.column_name defined to dictate how the
1938 <td> is to be displayed. The contents of the <td> entry will be
1939 wrapped in <span class="column_name"></span> to allow even more
1940 display control.  **
1941
1942 ** The column_name is run through the fixupAttributes()
1943 method to remove any bad values and convert all illegal css
1944 characters in a name to _.  You should run your column names
1945 through the fixupAttributes() method to have the same class
1946 name to work with.
1947
1948=cut
1949sub readHTML
1950{
1951  my $self = shift;
1952  my $sql = "";
1953  my @plug = ();
1954  my $plug = "";
1955  my $tableClass = "";
1956  my $alternateRows = 1;
1957  my $displayNumRows = 1;
1958  my $displayNULLAs = 'NULL';
1959  my $ignoreTags = "";
1960  my $sequence = "";
1961  my $sth = undef;
1962  my @data = ();
1963  my $headings;
1964  my $footer = 0;
1965  if (scalar @_ == 1)
1966  {
1967    $sql = shift;
1968    @data = $self->getDataHashHeader($sql);
1969  }
1970  else
1971  {
1972    my %args = (
1973      plug => [],
1974      substitute => [],
1975      tableClass => "",
1976      alternateRows => 1,
1977      displayNumRows => 1,
1978      displayNULLAs => 'NULL',
1979      headers => undef,
1980      footer => 0,
1981      @_
1982    );
1983    @plug = @{$args{plug}};
1984    $plug = "'" . join("', '", @plug) . "'";
1985    $sql = $args{sql};
1986    $tableClass = $args{tableClass};
1987    $alternateRows = $args{alternateRows};
1988    $displayNumRows = $args{displayNumRows};
1989    $displayNULLAs = $args{displayNULLAs};
1990    $ignoreTags = $args{ignoreTags};
1991    $sequence = $args{sequence};
1992    $headings = $args{headers};
1993    $footer = $args{footer};
1994    @data = $self->getDataHashHeader(%args);
1995  }
1996  my $html = "";
1997
1998  # make sure the defaults are sane.  The error handling will drop through and be caught so we output the table, etc.
1999  if ($alternateRows !~ /^(0|1)$/)
2000  {
2001    $self->error("alternateRows = '$alternateRows' is invalid!");
2002  }
2003  if ($displayNumRows !~ /^(0|1)$/)
2004  {
2005    $self->error("displayNumRows = '$displayNumRows' is invalid!");
2006  }
2007
2008  if ($self->error)
2009  {
2010    $self->prefixError();
2011    $self->{error} = 0;  # turn off the error message as the HTML Table will convey it.
2012    my $errorString = $self->errorMessage;
2013
2014    if ($displayNumRows)
2015    {
2016      $html .= <<"END_OF_CODE";
2017<div class="sqlNumRows"><span class="sqlNumRows"><b>0</b> rows returned.</span></div>
2018END_OF_CODE
2019    }
2020    $html .= <<"END_OF_CODE";
2021<table class="$tableClass">
2022  <tr class="sqlError">
2023    <td class="sqlError"><span class="sqlError">$errorString</span></td>
2024  </tr>
2025</table>
2026END_OF_CODE
2027  }
2028  else
2029  { # now process the result set returned and generate the HTML Table.
2030    if ($displayNumRows)
2031    {
2032      $html .= <<"END_OF_CODE";
2033<div class="sqlNumRows"><span class="sqlNumRows"><b>#NUMROWS#</b> rows returned.</span></div>
2034END_OF_CODE
2035    }
2036    $html .= "<table class=\"$tableClass\">\n";
2037
2038    my $names = $data[0];  # get list of the names of the columns returned. (Lowercased)
2039    # display the headers
2040    my $headerstring = "    <tr class=\"sqlHeader\">\n";
2041    my %encodedNames = ();
2042    foreach my $name (@{$names})
2043    {
2044      $encodedNames{$name} = $self->fixupAttributes(string => $name);
2045      my $tname = $name;
2046      if (defined $headings->{$name}) { $tname = $headings->{$name}; }
2047      $headerstring .= "    <th class=\"$encodedNames{$name}\">$tname</th>\n";
2048    }
2049    $headerstring .= "    </tr>\n";
2050
2051    $html .= "  <thead>\n$headerstring";
2052    $html .= "  </thead>\n";
2053    if ($footer)
2054    {
2055      $html .= "  <tfoot>\n$headerstring</tfoot>\n";
2056    }
2057    $html .= "  <tbody>\n";
2058
2059    my $counter = 0;
2060    for (my $rowIndex = 1; $rowIndex < @data; $rowIndex++)
2061    {
2062      my $row = $data[$rowIndex];
2063
2064      my $class = ($alternateRows ? ($counter % 2 == 0 ? "sqlRow sqlEven" : "sqlRow sqlOdd") : "sqlRow");
2065      $html .= "  <tr class=\"$class\">\n";
2066      # now iterate over each entry in the row and create it's column tag.
2067      if (exists $row->{$names->[0]} && scalar @{$names} == keys %{$row})
2068      {  # they hopefully are the same row/header info.
2069        foreach my $name (@{$names})
2070        {
2071          my $data = $row->{$name};
2072          $data = $displayNULLAs if (not defined $data);  # set NULL entries to be NULL
2073          $data = $self->formEncodeString(string => $data, ignoreTags => $ignoreTags, sequence => $sequence);
2074          $encodedNames{$name} = $self->fixupAttributes(string => $name) if (!exists $encodedNames{$name});
2075
2076          $html .= "    <td class=\"" . $encodedNames{$name} . "\"><span class=\"" . $encodedNames{$name} . "\">$data</span></td>\n";
2077        }
2078      }
2079      else
2080      {
2081        foreach my $name (keys(%{$row}))
2082        {
2083          my $data = $row->{$name};
2084          $data = $displayNULLAs if (not defined $data);  # set NULL entries to be NULL
2085          $data = $self->formEncodeString(string => $data, ignoreTags => $ignoreTags, sequence => $sequence);
2086          $encodedNames{$name} = $self->fixupAttributes(string => $name) if (!exists $encodedNames{$name});
2087
2088          $html .= "    <td class=\"" . $encodedNames{$name} . "\"><span class=\"" . $encodedNames{$name} . "\">$data</span></td>\n";
2089        }
2090      }
2091
2092      # end the <row>
2093      $html .= "  </tr>\n";
2094      $counter++;
2095    }
2096    $html .= "  </tbody>\n";
2097    $html .= "</table>\n";
2098
2099    # now update the numRows value.
2100    my $numRows = (scalar(@data) - 1);
2101    $html =~ s/#NUMROWS#/$numRows/;
2102  }
2103
2104  return $html;
2105}
2106
2107=item scalar readCSV(sql =>, plug =>, quote =>, quoteAll =>, delimeter =>, sep =>, header =>, computeRowHeader =>, case => ) or readCSV('SELECT foo FROM bar')
2108
2109  This returns the data selected in sql query in comma separated value
2110  format.  Returns undef if an error occured.
2111
2112  Optional parameters:
2113  sep - defaults to ', ', but can be whatever you want to seperate fields
2114    with.
2115  header - (boolean) defaults to 0.  If 1 (true), then we output the
2116    column names as the first line of the output.  If using Sybase and
2117    COMPUTE rows, then at each detected compute row, we output the
2118    compute rows headers before the compute rows data and prefix the
2119    compute row headers with '(!!COMPUTE ROW!!)'.  This is so scripts
2120    can detect a compute row and handle accordingly.
2121  computeRowHeader - (boolean) defaults to 1. determines if the string
2122    '(!!COMPUTE ROW!!)' should be prefixed to compute row headers
2123    if using Sybase and header => 1.
2124  case - (boolean) defaults to 0.  If 1 then we preserve the case for
2125    column names in the header row.  If 0 then we lowercase all column
2126    names in the header row.
2127  quote - defaults to single quotes.  will escape any found in data with
2128    backslash.
2129  quoteAll - (boolean) defaults to 1.  If 1 (true), then all data is
2130    quoted using the quote value.  If 0 (false), then only those fields
2131    detected to be non-numeric are quoted.
2132  delimeter - defaults to newline (\n).  Will escape any found in data
2133    with backslash (\\n)
2134
2135=cut
2136sub readCSV
2137{
2138  my $self = shift;
2139  my $sep = ', ';
2140  my $quote = "'";
2141  my $quoteAll = 1;
2142  my $header = 0;
2143  my $computeRowHeader = 1;
2144  my $case = 0;
2145  my $delim = "\n";
2146  my $plug = [];
2147  my $sql;
2148  if (scalar @_ > 1)
2149  {
2150    my %args = (sql => undef, @_);
2151    if (defined $args{quote}) { $quote = $args{quote}; }
2152    if (defined $args{quoteAll}) { $quoteAll = $args{quoteAll}; }
2153    if (defined $args{delimeter}) { $delim = $args{delimeter}; }
2154    if (defined $args{plug}) { $plug = $args{plug}; }
2155    if (defined $args{sep}) { $sep = $args{sep}; }
2156    if (defined $args{header}) { $header = $args{header}; }
2157    if (defined $args{computeRowHeader}) { $computeRowHeader = $args{computeRowHeader}; }
2158    if (defined $args{case}) { $case = $args{case}; }
2159
2160    $sql = $args{sql};
2161  }
2162  else { $sql = shift; }
2163  if (!defined $sql || length $sql == 0) { $self->error(errorString => "SQL string not passed in!\n"); return undef; }
2164  if (length $quote == 0) { $self->error(errorString => "Invalid quote passed to readCSV!\n"); return undef; }
2165  if (length $delim == 0) { $self->error(errorString => "Invalid delimeter passed to readCSV!\n"); return undef; }
2166  if (length $sep == 0) { $self->error(errorString => "Invalid seperator passed to readCSV!\n"); return undef; }
2167  if ($header !~ /^(0|1)$/) { $self->error(errorString => "header must be 0 or 1!\n"); return undef; }
2168  if ($header && $computeRowHeader !~ /^(0|1)$/) { $self->error(errorString => "computeRowHeader must be 0 or 1!\n"); return undef; }
2169  if ($header && $case !~ /^(0|1)$/) { $self->error("case must be 0 or 1!\n"); return undef; }
2170
2171  # make sure the user isn't trying to do something that will cause a csv reader to choke.
2172  if ($sep eq $quote) { $self->error(errorString => "seperator can not equal quote value!"); return undef; }
2173
2174  if ($sep eq $delim) { $self->error(errorString => "seperator can not equal delimeter value!"); return undef; }
2175
2176  if ($delim eq $quote) { $self->error(errorString => "delimeter can not equal quote value!"); return undef; }
2177
2178  my $r = '';  # the string to build up.
2179
2180  my $sth = $self->read(sql => $sql, plug => $plug);
2181  if ($self->error) { return undef; }
2182
2183  if ($header)
2184  {
2185    my $headers = $sth->{($case == 1 ? 'NAME' : 'NAME_lc')};
2186    # I should probably make sure that sep and delim do not exist in the returned column names.
2187    $r .= join($sep, @{$headers}) . $delim;
2188  }
2189
2190  while (my $info = $sth->fetchrow_arrayref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results}))
2191  {
2192    if (defined $info && ref($info) eq "ARRAY" && scalar @{$info} > 0)
2193    {
2194      # don't process the return status of a stored procedure from Sybase.
2195      next if ($self->{dbType} eq "Sybase" && $sth->{syb_result_type} == 4043);
2196
2197      my $headers = $sth->{($case == 1 ? 'NAME' : 'NAME_lc')};
2198      if ($self->{dbType} eq "Sybase" && $sth->{syb_result_type} == 4045 && $header)
2199      {
2200        # generate the Compute row header
2201        $r .= "(!!COMPUTE ROW!!)" if ($computeRowHeader);
2202        # I should probably make sure that sep and delim do not exist in the returned column names.
2203        $r .= join($sep, @{$headers}) . $delim;
2204      }
2205      # use the length of the headers array to determine how many actual columns we are working with,
2206      # since the sybase compute row apparently returns the same number of columns as the main select,
2207      # but the headers for the compute row shows the correct number.
2208      for (my $i = 0; $i < scalar @{$headers}; $i++)
2209      {
2210        if ($i > 0) { $r .= $sep; }
2211        my $t = $info->[$i];
2212        $t =~ s/$quote/\\${quote}/;
2213        $t =~ s/$delim/\\${delim}/;
2214        if ($quoteAll)
2215        {
2216          $r .= $quote . $t . $quote;
2217        }
2218        else
2219        {
2220          # if this doesn't look like a number, trying to cover all bases here, and it's not the empty string, then quote, else don't quote.
2221          if ($t !~ /^([-\+]?\d+(\.\d+)?([Ee]\d+)?)$/ && $t !~ /^$/)
2222          {
2223            $r .= $quote . $t . $quote;
2224          }
2225          else
2226          {
2227            $r .= $t;
2228          }
2229        }
2230      }
2231      $r .= $delim;
2232    }
2233  }
2234  return $r;
2235}
2236
2237=item @ readSpreadSheet(sql =>, plug =>, case =>, sheetName =>, formats => {}, returnFile =>, workbook => )
2238
2239=item @ readSpreadSheet('SELECT foo FROM bar')
2240
2241  This returns the data selected in sql query in a Excel(R) SpreadSheet.
2242  NOTE:  You must call binmode() on whatever file handle you are
2243  planning on printing the results to.  This is an IO::Scalar instance.
2244
2245  The returned array contains the following entries:
2246    [0] = excel spreadsheet or the Spreadsheet::WriteExcel object
2247    [1] = # rows of data processed
2248    [2] = # rows changed by callback handler
2249    [3] = # compute rows encountered
2250    [4] = # compute rows changed by callback handler
2251
2252  Returns undef if an error occured.
2253
2254  Optional parameters:
2255  returnFile - (boolean) defaults to 1.  If 1, then we return the
2256    spreadsheet data ready to be written to a file.
2257    If 0, then we return the Spreadsheet::WriteExcel object to allow
2258    the caller to continue modifying things or to pass back into us
2259    for another sheets worth of data.
2260  workbook - Spreadsheet::WriteExcel object to work with.
2261    Defaults to undef.
2262  case - (boolean) defaults to 0.  If 1 then we preserve the case for
2263    column names in the header row.  If 0 then we lowercase all column
2264    names in the header row.
2265  sheetName - defaults to "Sheet1".  Specify the name of the sheet
2266    being created.  If you specify workbook, then I check to make sure
2267    that the given sheetName has not already been created.  If it has,
2268    then I let the module pick the next valid name, else I use the
2269    name you specified.
2270  formats - (hash ref) defaults to {}.  Define the name of the format
2271    and then the attributes you want it to have, where each format
2272    is a hashref containing the attributes.
2273    Ex:  { header => { bold => 1, align => "center" },
2274           date => { num_format => "yyyy-mm-dd", align => "right" }
2275         }
2276    If you define an entry called 'header', it will be used as the
2277    format when displaying the header row and outputting the
2278    column headers, otherwise no formatting will be done.
2279  headers - (hash ref) defaults to {}.  Allows you to override the
2280    displayed name for each data column.
2281    Ex:  { start_date => "Start Date" }
2282  columnWidths - (hash ref) defaults to {}.  Allows you to specify
2283    which columns need a specific width set.  Only those columns
2284    that have an index value defined, will have their width set.
2285    The index value is based on the column name returned by the
2286    result set using the case flag to determine if it is all
2287    lowercase or left alone.  This allows you to re-order your
2288    output and still have the correct column widths defined.
2289    Ex:  { start_date => 35, num_users => 12, whatever => 15 }
2290  types - (hash ref) defaults to {}.  Allows you to specify the
2291    type of each column, indexed by the column name.  If you
2292    don't specify a type for a column, then it defaults to 'string'.
2293    See columnWidths for a description of the column name index.
2294    The possible types are:
2295      string, date, time, date_time, number, url, 0number
2296
2297    0number displays this field as a string, thus keeping any leading 0's.
2298
2299    date appends the "T" required by excel to indicate there is no time part.
2300
2301    time prepends the "T" required by excel to indicate there is no date part.
2302
2303    date_time assumes you have inserted the "T" between the date and time parts,
2304    with no surrounding spaces, otherwise it won't display properly.
2305
2306    You must also specify a format so that your date and/or time values
2307    display properly.
2308
2309    Ex:  { start_date => "date", num_users => "number",
2310           whatever => "string" }
2311
2312  Callback handlers:
2313
2314    rowHandler - anonymous sub that will be called and passed in
2315      a hashref that contains the following entries:
2316        data - array ref with each columns value indexed from 0
2317        format - array ref with each columns format to be applied
2318        type - array ref with each columns default type
2319        row - int containing the current excel row # being processed.
2320      You can delete, modify or insert entries into each of the arrays.
2321      Make sure you also delete or insert the appropriate entries from
2322      the format and type arrays so they stay in sync with the data
2323      array.
2324      If you make changes, return the hashref, else return undef to
2325      indicate we should use the original values.
2326
2327    computeRowHandler - callback handler that handles compute rows.
2328      See rowHandler for details on the input and output handling.
2329
2330  CAVEATS
2331    If you want to run multiple sql queries and generate a seperate
2332    sheet per query, you must instantiate your own instance of the
2333    Spreadsheet::WriteExcel module and pass it in, specifying
2334    returnFile => 0.  You then must close() the workbook before
2335    trying to work with the result, otherwise you won't get any
2336    output as desired.
2337
2338    Example:
2339
2340    # instantiate my workbook instance.
2341    my $workbook;
2342    unless ($workbook = new Spreadsheet::WriteExcel("test.xls"))
2343    {
2344      die "Failed to instantiate Spreadsheet::WriteExcel instance!  $!";
2345    }
2346
2347    # do a loop that passes in the workbook and turns off returnFile.
2348    .
2349    .
2350    .
2351
2352    $workbook->close();
2353
2354=cut
2355sub readSpreadSheet
2356{
2357  my $self = shift;
2358  my $case = 0;
2359  my $sheetName = "Sheet1";
2360  my $formats = {};
2361  my $headers = {};
2362  my $columnWidths = {};
2363  my $types = {};
2364  my $rowHandler = undef;
2365  my $computeRowHandler = undef;
2366  my $plug = [];
2367  my $sql;
2368  my $returnFile = 1;
2369  my $workbook;
2370
2371  if (scalar @_ > 1)
2372  {
2373    my %args = (sql => undef, @_);
2374    if (defined $args{plug}) { $plug = $args{plug}; }
2375    if (defined $args{case}) { $case = $args{case}; }
2376    if (defined $args{sheetName}) { $sheetName = $args{sheetName}; }
2377    if (defined $args{formats}) { $formats = $args{formats}; }
2378    if (defined $args{headers}) { $headers = $args{headers}; }
2379    if (defined $args{columnWidths}) { $columnWidths = $args{columnWidths}; }
2380    if (defined $args{types}) { $types = $args{types}; }
2381    if (defined $args{rowHandler}) { $rowHandler = $args{rowHandler}; }
2382    if (defined $args{computeRowHandler}) { $computeRowHandler = $args{computeRowHandler}; }
2383    if (defined $args{returnFile}) { $returnFile = $args{returnFile}; }
2384    if (defined $args{workbook}) { $workbook = $args{workbook}; }
2385
2386    $sql = $args{sql};
2387  }
2388  else { $sql = shift; }
2389  if (!defined $sql || length $sql == 0) { $self->error(errorString => "SQL string not passed in!\n"); return undef; }
2390  if ($case !~ /^(0|1)$/) { $self->error("case must be 0 or 1!\n"); return undef; }
2391  if ($sheetName eq "") { $self->error("sheetName can not be empty!\n"); return undef; }
2392  if (ref($formats) ne "HASH") { $self->error("formats must be a hash ref, not a '" . ref($formats) . "'!"); return undef; }
2393  if (ref($headers) ne "HASH") { $self->error("headers must be a hash ref, not a '" . ref($headers) . "'!"); return undef; }
2394  if (ref($columnWidths) ne "HASH") { $self->error("columnWidths must be a hash ref, not a '" . ref($columnWidths) . "'!"); return undef; }
2395  if (ref($types) ne "HASH") { $self->error("types must be a hash ref, not a '" . ref($types) . "'!"); return undef; }
2396  if (defined $rowHandler && ref($rowHandler) ne "CODE") { $self->error("rowHandler must be an anonymous sub, not a '" . ref($rowHandler) . "'!"); return undef; }
2397  if (defined $computeRowHandler && ref($computeRowHandler) ne "CODE") { $self->error("computeRowHandler must be an anonymous sub, not a '" . ref($computeRowHandler) . "'!"); return undef; }
2398
2399  my $objSpecified = (defined $workbook ? 1 : 0);
2400
2401  if ($objSpecified && $returnFile)
2402  {
2403    $self->error("You can not specify the workbook object and ask for the file to be returned!");
2404    return undef;
2405  }
2406
2407  my $sth = $self->read(sql => $sql, plug => $plug);
2408  if ($self->error) { return undef; }
2409
2410  my $result;
2411  my $numRows = 0;
2412  my $numComputeRows = 0;
2413  my $numRowsChanged = 0;
2414  my $numComputeRowsChanged = 0;
2415
2416  #print "workbook specified = $objSpecified, ref = '" . ref($workbook) . "'\n";
2417
2418  if (!$objSpecified)
2419  {
2420    tie(*EXCEL, 'IO::Scalar', \$result);
2421    unless ($workbook = new Spreadsheet::WriteExcel(\*EXCEL))
2422    {
2423      $self->error("Failed to instantiate Spreadsheet::WriteExcel instance!  $!");
2424      return undef;
2425    }
2426  }
2427
2428  # create the worksheet we are going to populate.
2429  my $worksheet;
2430
2431  my $sheetNameDefined = 0;
2432  if ($objSpecified)
2433  {
2434    foreach my $tmpSheet ($workbook->sheets())
2435    {
2436      if ($tmpSheet->get_name() eq $sheetName)
2437      {
2438        $sheetNameDefined = 1;
2439        last;
2440      }
2441    }
2442  }
2443
2444  if ($sheetNameDefined)
2445  {
2446    #print "Letting workbook pick sheet name...\n";
2447    $worksheet = $workbook->add_worksheet();
2448  }
2449  else
2450  {
2451    $worksheet = $workbook->add_worksheet($sheetName);
2452  }
2453
2454  # now define all the user supplied formats.
2455  my %formats = ();
2456  foreach my $format ( keys %{$formats} )
2457  {
2458    $formats{$format} = $workbook->add_format(%{$formats->{$format}});
2459  }
2460
2461  # now display the header row
2462  my $colHeaders = $sth->{($case == 1 ? 'NAME' : 'NAME_lc')};
2463  my $row = 0;
2464  my $col = 0;
2465  foreach my $header (@{$colHeaders})
2466  {
2467    my $format = (exists $formats{header} ? $formats{header} : undef);
2468    # define the column width, if so desired.
2469    if (exists $columnWidths->{$header})
2470    {
2471      # define the width.
2472      $worksheet->set_column($col, $col, $columnWidths->{$header});
2473    }
2474    # see if we have a header display override.
2475    my $headerValue = (exists $headers->{$header} ? $headers->{$header} : $header);
2476    # actually display the header value.
2477    $worksheet->write($row, $col, $headerValue, $format);
2478    $col++;
2479  }
2480
2481  # now to actually gather the data and display it.
2482  # allow the user to do any processing of the data as needed, by calling any handlers they
2483  # specified.  If we call a handler, it will be provided with a hashref containing
2484  # data, format, type arrayrefs that represent the
2485  # data being displayed for that row and the formats being applied to each column.
2486  # By default all types will be "string", though you can define:
2487  #   string, date, time, date_time, number, url, 0number
2488  # 0number displays this field as a string, thus keeping any leading 0's.
2489  # date appends the "T" required by excel to indicate there is no time part.
2490  # time prepends the "T" required by excel to indicate there is no date part.
2491  # date_time assumes you have inserted the "T" between the date and time parts,
2492  # with no surrounding spaces, otherwise it won't display properly.  You must also
2493  # specify a format so that your date and/or time values display properly.
2494  #
2495  # The formats are stored as just the name of the format to be applied, not the actual
2496  # format object that the Spreadsheet::WriteExcel module created.
2497  # They can either delete, modify or insert data and formats.
2498  # They return the hashref to indicate changes have been made or undef to indicate use
2499  # the original values.
2500
2501  while (my $info = $sth->fetchrow_arrayref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results}))
2502  {
2503    if (defined $info && ref($info) eq "ARRAY" && scalar @{$info} > 0)
2504    {
2505      # don't process the return status of a stored procedure from Sybase.
2506      next if ($self->{dbType} eq "Sybase" && $sth->{syb_result_type} == 4043);
2507
2508      $row++;  # increment our row counter.
2509      $numRows++;
2510
2511      my $colHeaders = $sth->{($case == 1 ? 'NAME' : 'NAME_lc')};
2512      my $computeRow = 0;
2513      if ($self->{dbType} eq "Sybase" && $sth->{syb_result_type} == 4045)
2514      {
2515        $computeRow = 1;
2516        $numComputeRows++;
2517      }
2518
2519      # build up the data structure that will be passed into the callback method, if the user specified one.
2520      my %row = ( data => [], format => [], type => [], row => $row );
2521
2522      # use the length of the headers array to determine how many actual columns we are working with,
2523      # since the sybase compute row apparently returns the same number of columns as the main select,
2524      # but the headers for the compute row shows the correct number.
2525      for (my $i = 0; $i < scalar @{$colHeaders}; $i++)
2526      {
2527        push @{$row{data}}, $info->[$i];
2528        if (exists $formats->{$colHeaders->[$i]})
2529        {
2530          # only store the name of the format, not the actual object.
2531          $row{format}->[$i] = $colHeaders->[$i];
2532        }
2533        else
2534        {
2535          $row{format}->[$i] = undef;
2536        }
2537        if (exists $types->{$colHeaders->[$i]})
2538        {
2539          $row{type}->[$i] = $types->{$colHeaders->[$i]};
2540        }
2541        else
2542        {
2543          $row{type}->[$i] = "string";
2544        }
2545      }
2546
2547      # now we see if the user gave us any callback methods to run.
2548      my $callbackResult = undef;
2549      if ($computeRow)
2550      {
2551        if (defined $computeRowHandler)
2552        {
2553          $callbackResult = &$computeRowHandler(\%row);
2554        }
2555      }
2556      else
2557      {
2558        if (defined $rowHandler)
2559        {
2560          $callbackResult = &$rowHandler(\%row);
2561        }
2562      }
2563
2564      if (defined $callbackResult && ref($callbackResult) eq "HASH")
2565      {
2566        $row{data} = $callbackResult->{data};
2567        $row{format} = $callbackResult->{format};
2568        $row{type} = $callbackResult->{type};
2569        if ($computeRow)
2570        {
2571          $numComputeRowsChanged++;
2572        }
2573        else
2574        {
2575          $numRowsChanged++;
2576        }
2577      }
2578
2579      # now we walk the row hash and generate output.
2580      for (my $col=0; $col < scalar @{$row{data}}; $col++)
2581      {
2582        my $type = $row{type}->[$col];
2583        my $value = $row{data}->[$col];
2584        my $format = (defined $row{format}->[$col] && exists $formats{$row{format}->[$col]} ? $formats{$row{format}->[$col]} : undef);
2585
2586        if ($type eq "string")
2587        {
2588          $worksheet->write_string($row, $col, $value, $format);
2589        }
2590        elsif ($type eq "number")
2591        {
2592          $worksheet->write_number($row, $col, $value, $format);
2593        }
2594        elsif ($type eq "0number")
2595        {
2596          $worksheet->write_string($row, $col, $value, $format);
2597        }
2598        elsif ($type eq "date")
2599        {
2600          $worksheet->write_date_time($row, $col, $value . "T", $format);
2601        }
2602        elsif ($type eq "time")
2603        {
2604          $worksheet->write_date_time($row, $col, "T" . $value, $format);
2605        }
2606        elsif ($type eq "date_time")
2607        {
2608          # They have to already have fixed this up to include the T seperator.
2609          $worksheet->write_date_time($row, $col, $value, $format);
2610        }
2611        elsif ($type eq "url")
2612        {
2613          $worksheet->write_url($row, $col, $value, $format);
2614        }
2615        else  # unknown type, but let write() figure it out.
2616        {
2617          $worksheet->write($row, $col, $value, $format);
2618        }
2619      }
2620
2621      # skip a row if we just output a compute row.
2622      $row++ if ($computeRow);
2623    }
2624  }
2625
2626  # make the header row always visible.
2627  $worksheet->freeze_panes(1, 0);
2628
2629  # wrap things up.
2630  $workbook->close if ($returnFile == 1);
2631
2632  # They need to binmode() the filehandle before this is displayed.
2633  return (($returnFile ? $result : $workbook), $numRows, $numRowsChanged, $numComputeRows, $numComputeRowsChanged);
2634}
2635
2636=item int write(sql => "", plug => [], substitute => [])
2637
2638 (This function should be called for any sql statement other than
2639 SELECT).
2640 executes the specified sql statement passing in any values in plug
2641 to the execute method after doing any substitutions that are in
2642 substitute.
2643
2644 Returns the number of rows affected.
2645
2646 If the sql to execute is an INSERT statement, then the oid or
2647 insertid (Postgresql or MySQL) values will be stored in the
2648 oid value in this object, for later access by getID().
2649
2650=cut
2651sub write
2652{
2653  my $self = shift;
2654  my $sql = "";
2655  my @plug = ();
2656  my @substitute = ();
2657  if (scalar @_ == 1)
2658  {
2659    $sql = shift;
2660  }
2661  else
2662  {
2663    my %args = ( plug => [], substitute => [], @_ );
2664    @plug = @{$args{'plug'}};
2665    @substitute = @{$args{'substitute'}};
2666    $sql = $args{'sql'};
2667  }
2668
2669  # validate we got a sql statement to work with.
2670  if (length $sql == 0)
2671  {
2672    $self->error(errorString => "SQL string not passed in!" . "<br />\n" . $self->debugMessage);
2673
2674    return 0;
2675  }
2676
2677  # check and see if we need to do any substitutions.
2678  if (scalar @substitute > 0)
2679  {
2680    for (my $i=0; $i < scalar @substitute; $i++)
2681    {
2682      my $temp_string = "\\#\\#\\?" . ($i+1) . "\\#\\#";
2683      $sql =~ s/$temp_string/$substitute[$i]/g;
2684    }
2685  }
2686
2687  my $timerStart = $self->getCurrTime();
2688  $self->logSQLStatement("write", $timerStart, $sql, \@plug) if ($self->{logSQLStatements});
2689
2690  # now prepare the statement
2691  my $sth;
2692  eval {
2693    $sth = $self->{dbh}->prepare($sql);
2694  };
2695  if ($@)
2696  {
2697    $self->error(errorString => "Eval of prepare failed!<br />\nError = '$@'.<br />\nsql='$sql'.<br />\nplug='" . join("', '", @plug) . "'.<br />\n" . $self->debugMessage);
2698
2699    return 0;
2700  }
2701  elsif (!$sth || $DBI::err)
2702  {
2703    $self->error(errorString => "Preparing failed!<br />\n" . $DBI::errstr . "<br />\nsql='$sql'.<br />\nplug='" . join("', '", @plug) . "'.<br />\n" . $self->debugMessage);
2704
2705    return 0;
2706  }
2707
2708  # now execute the sql statement passing in any parameters given via plug
2709  my $rv;
2710  my $done = 0;
2711  my $numTries = 0;
2712  while (!$done)
2713  {
2714    $deadlockEncountered = 0;  # make sure we turn it off.
2715    eval {
2716      $rv = $sth->execute(@plug);
2717    };
2718    if (!$deadlockEncountered)
2719    {
2720      $done = 1;
2721      if ($@)
2722      {
2723        $self->error(errorString => "Eval of execute failed!<br />\nError = '$@'.<br />\nsql='$sql'.<br />\nplug='" . join("', '", @plug) . "'.<br />\n" . $self->debugMessage);
2724
2725        $self->checkLongRunningSQL("write", $timerStart, $numTries, $sql, \@plug);
2726        return 0;
2727      }
2728      elsif (!$rv || $DBI::err)
2729      {
2730        $self->error(errorString => "Execute failed!<br />\n" . $DBI::errstr . "<br />\nsql='$sql'.<br />\nplug='" . join("', '", @plug) . "'.<br />\n" . $self->debugMessage);
2731
2732        $self->checkLongRunningSQL("write", $timerStart, $numTries, $sql, \@plug);
2733        return 0;
2734      }
2735    }
2736    else
2737    {
2738      $numTries++;
2739      if ($numTries >= $self->{deadlockNumTries})
2740      {
2741        $self->checkLongRunningSQL("write", $timerStart, $numTries, $sql, \@plug);
2742        return undef;
2743      }
2744      my $sleep = $self->{deadlockSleep};
2745      if ($numTries > 1 && $self->{deadlockRampSleep})
2746      {
2747        $sleep *= $numTries;
2748      }
2749      print "ALERT:  deadlock encountered!  Retrying sql.  Attempt #$numTries.  Sleeping for $sleep seconds.\n";
2750      sleep($sleep);
2751    }
2752  }
2753
2754  # store the oid in the object, if this was an INSERT statement.
2755  if ($sql =~ /^INSERT/i)
2756  {
2757    if ($self->{dbType} eq "Pg")
2758    {
2759      if ($self->{serverVerMajor} < 8 || ($self->{serverVerMajor} == 8 && $self->{serverVerMinor} < 1))
2760      {
2761        $self->{oid} = $sth->{pg_oid_status};
2762      }
2763      else
2764      {
2765        # have to figure out the table name to specify to the last_insert_id function.
2766        if ($sql =~ /^(INSERT INTO\s+)([^\(\s]+)\s*(\(|SELECT )(.+)$/i)
2767        {
2768          my $tableName = $2;
2769          #print "You just inserted into '$tableName'...\n";
2770
2771          local $self->{dbh}->{PrintError};  # don't print an error when last_insert_id can't find the sequence on a table.
2772
2773          $self->{oid} = $self->{dbh}->last_insert_id(undef, undef, $tableName, undef);
2774        }
2775      }
2776    }
2777    elsif ($self->{dbType} eq "mysql")
2778    {
2779      $self->{oid} = $sth->{mysql_insertid};
2780    }
2781    elsif ($self->{dbType} eq "Sybase")
2782    {
2783      # kill the $sth so this hopefully works properly.
2784      #$sth = undef;
2785      $self->{oid} = 0; #$self->{dbh}->last_insert_id();
2786    }
2787  }
2788  else
2789  {
2790    $self->{oid} = 0;
2791  }
2792
2793  $self->checkLongRunningSQL("write", $timerStart, $numTries, $sql, \@plug);
2794  return $rv;
2795}
2796
2797=item scalar getID("table.column")
2798
2799This method will attempt to return the ID value of the just
2800INSERTed statement as implemented by MySQL, Sybase and PostgreSQL.
2801This is assuming that you just used the write() method and
2802that is was able to update the oid value.
2803
2804This method requires a string value specifying the table.column that
2805is the ID field for the INSERT statement that just executed, if you
2806are using a PostgreSQL backend.
2807
2808If using PostgreSQL <= 8.0, then the old oid lookup code will be run,
2809otherwise the ID has already been looked-up in the write() method and
2810will be returned.
2811
2812If using MySQL, you do not need to specify the table.column value,
2813but your table must have an AUTO_INCREMENT field defined.
2814
2815If using Sybase, you do not need to specify the table.column value,
2816but you may not get a valid ID back if you used ? substitution or
2817the INSERT was in a stored procedure.  See the DBD::Sybase man-page
2818for more information.
2819
2820UPDATE:  At this point, I can't reliably get this to work with
2821Sybase, so it will always return a value of 0 until I can get this
2822figured out.  Sorry if this causes an issue.
2823
2824If the database type is unsupported or an error happened, a value
2825of 0 will be returned.
2826
2827=cut
2828sub getID
2829{
2830  my $self = shift;
2831  my $arg = shift;
2832  my $id = 0;
2833
2834  if ($self->{dbType} eq "Pg")
2835  {
2836    my $oid = $self->{oid};
2837
2838    if ($self->{serverVerMajor} < 8 || ($self->{serverVerMajor} == 8 && $self->{serverVerMinor} < 1))
2839    {
2840      # now I have to split the table.column apart and then do the lookup.
2841      if ($arg !~ /^(\w+\.\w+)$/)
2842      {
2843        $self->error("table.column = '$arg' is invalid!");
2844        return 0;
2845      }
2846      my ($table, $column) = split /\./, $arg;
2847      my $sth = $self->read(sql => "SELECT $column FROM $table WHERE oid = ?", plug => [ $oid ]);
2848      if ($self->error)
2849      {
2850        $self->prefixError();
2851        return 0;
2852      }
2853      my @result = $sth->fetchrow_array();
2854      if (defined $result[0])
2855      {
2856        $id = $result[0];
2857      }
2858    }
2859    else
2860    {
2861      $id = $oid;
2862    }
2863  }
2864  elsif ($self->{dbType} =~ /^mysql|Sybase$/)
2865  {
2866    $id = $self->{oid};
2867  }
2868
2869  return $id;
2870}
2871
2872# This routine checks to see if the mysql database (currently connected) supports transactions
2873# and sets the transaction type variable.
2874sub mysqlHasTransactions
2875{
2876  my $self = shift;
2877  return 0 if (!defined $self->{dbh});
2878
2879  return 1 if ($self->{dbType} ne "mysql");  # make sure we are working with a MySQL database.
2880  my $sth = $self->{dbh}->prepare("SHOW VARIABLES");
2881  my $rv;
2882  eval { $rv = $sth->execute(); };
2883  if ($@ || $DBI::err)
2884  {
2885    $self->error(errorString => "Execute failed!<br />\n" . $DBI::errstr . "<br />\n" . $self->debugMessage);
2886    return 0;
2887  }
2888  while (my $row = $sth->fetchrow_hashref())
2889  {
2890    if ($row->{Variable_name} eq 'have_bdb' && $row->{Value} eq 'YES')
2891    {
2892      $self->{transactionType} = "bdb";
2893      last;
2894    }
2895    if ($row->{Variable_name} eq 'have_innobase' && $row->{Value} eq 'YES')
2896    {
2897      $self->{transactionType} = "innobase";
2898      last;
2899    }
2900    if ($row->{Variable_name} eq 'have_gemini' && $row->{Value} eq 'YES')
2901    {
2902      $self->{transactionType} = "gemini";
2903      last;
2904    }
2905  }
2906  return $self->{transactionType};
2907}
2908
2909=item string debugMessage()
2910
2911 Returns the string that contains all the info that is to be logged
2912 at the current logLevel level.  If logLevel is not 0, 1, 2 or 3
2913 then a default of 3 is used.
2914
2915=cut
2916sub debugMessage
2917{
2918  my $self = shift;
2919  my $logLevel = $self->{logLevel};
2920  my $result = "";
2921  $logLevel = 3 if ($logLevel !~ /^(0|1|2|3)$/);
2922
2923  return $result if ($logLevel == 0);  # jump out now since they don't want any debug info displayed.
2924
2925  # output the stuff that will always be done (level = 1)
2926  $result .= "dbType = '$self->{dbType}', dbHost = '$self->{dbHost}', dbName = '$self->{dbName}', printError = '$self->{printError}', raiseError = '$self->{raiseError}'";
2927  $result .= ", autoCommit = '$self->{autoCommit}', setDateStyle = '$self->{setDateStyle}', supportsTransactions = '$self->{supportsTransactions}', transactionType = '$self->{transactionType}'";
2928  $result .= ", server = '$self->{server}', interfaces = '$self->{interfaces}'";
2929
2930  # output level 2 stuff
2931  if ($logLevel >= 2)
2932  {
2933    $result .= ", dbUser = '$self->{dbUser}', dbPort = '$self->{dbPort}', predefinedDSN = '$self->{predefinedDSN}'";
2934  }
2935
2936  # output level 3 stuff
2937  if ($logLevel == 3)
2938  {
2939    $result .= ", dbPasswd = '$self->{dbPasswd}'";
2940  }
2941
2942  $result .= "<br />\n";  # tack on the newline ending
2943
2944  return $result;
2945}
2946
2947=item int getLogLevel()
2948
2949 returns the current logLevel value.
2950
2951=cut
2952sub getLogLevel
2953{
2954  my $self = shift;
2955
2956  return $self->{logLevel};
2957}
2958
2959=item int setLogLevel(logLevel => 1)
2960
2961 sets the logLevel value.  If the value is not specified then it
2962 defaults to logLevel 1.
2963
2964 Returns 1 on Success, 0 on Error.
2965
2966 We validate that the logLevel is 1, 2 or 3.
2967
2968=cut
2969sub setLogLevel
2970{
2971  my $self = shift;
2972  my $logLevel = 1;
2973  if (scalar @_ == 1)
2974  {
2975    $logLevel = shift;
2976  }
2977  else
2978  {
2979    my %args = ( logLevel => 1, @_ );
2980    $logLevel = $args{logLevel};
2981  }
2982
2983  if ($logLevel !~ /^(0|1|2|3)$/)
2984  {
2985    $self->error(errorString => "logLevel = '$logLevel' is invalid!<br />\n" . $self->debugMessage);
2986    return 0;
2987  }
2988
2989  $self->{logLevel} = $logLevel;
2990  return 1;
2991}
2992
2993=item string boolToDBI(string)
2994
2995 Takes the string and returns a 1 for 1|t|true,
2996 returns a 0 for anything else.
2997
2998 This method basically will output a true or false
2999 value that any database should recognize based upon
3000 the input string.
3001
3002=cut
3003sub boolToDBI
3004{
3005  my $self = shift;
3006  my $string = "";
3007  if (scalar @_ == 1)
3008  {
3009    $string = shift;
3010  }
3011  else
3012  {
3013    my %args = ( string => "", @_ );
3014    $string = $args{string};
3015  }
3016
3017  if ($string =~ /^(1|t|true)$/i)
3018  {
3019    return 1;
3020  }
3021  # must be false!
3022  return 0;
3023}
3024
3025=item string dbiToBool(string)
3026
3027 Takes the 1 or 0 from the DBI and returns
3028 true or false.
3029
3030=cut
3031sub dbiToBool
3032{
3033  my $self = shift;
3034  my $string = "";
3035  if (scalar @_ == 1)
3036  {
3037    $string = shift;
3038  }
3039  else
3040  {
3041    my %args = ( string => "", @_ );
3042    $string = $args{string};
3043  }
3044
3045  if ($string =~ /^(1)$/i)
3046  {
3047    return "true";
3048  }
3049  # must be false!
3050  return "false";
3051}
3052
3053=item scalar formEncodeString(string, ignoreTags, sequence)
3054
3055=item scalar formEncodeString(scalar)
3056
3057 In scalar mode, takes the incoming string and encodes it to
3058 escape all <, > values as &lt;, &gt; unless they are \ escaped.
3059
3060 To have the \ showup, you will have to do a \\ when defining this
3061 in perl, otherwise perl interprets the \whatever internally.
3062
3063 In non-scalar mode, you specify the arguments by name.
3064
3065 optional:
3066   string - string to encode all &, <, > characters to their html
3067     equivalents of &amp;, &lt;, &gt;.
3068   ignoreTags - string of pipe (|) seperated tag names that should not
3069     be encoded.  Ex:  ignoreTags => "b|i|u|span" would ignore all
3070     <b>, </b>, <i>, </i>, <u>, </u>, <span>, </span> tags that were
3071     not \ escaped.
3072   sequence - a named set of ignoreTags values that you want used.
3073     If both sequence and ignoreTags are specified, the ignoreTags
3074     value is used.  If you want to apply multiple sequences, specify
3075     them in a comma delimited format.
3076     Ex: sequence => 'formatting,seperator'
3077
3078     available sequences are:
3079       formatting - "b|i|u|span|sub|sup|big|code|font|h1|h2|h3|h4|h5|h6|pre|small|strike|strong"
3080       block - "p|div|form"
3081       tables - "table|tr|td|th|tbody|tfoot|thead"
3082       seperator - "br|hr"
3083       formItems - "input|textarea|select|option"
3084       grouping - "ol|ul|li"
3085
3086 returns: form encoded string ignoring those entries defined in
3087   ignoreTags or sequence and where the &, <, > was not \ escaped.
3088
3089   Any &, <, > that were \ escaped will have the \ removed on output.
3090
3091=cut
3092sub formEncodeString
3093{
3094  my $string;
3095  my $ignoreTags;
3096  my $sequence;
3097  my %sequences = (
3098       "formatting" => "b|i|u|span|sub|sup|big|code|font|h1|h2|h3|h4|h5|h6|pre|small|strike|strong",
3099       "block" => "p|div|form",
3100       "tables" => "table|tr|td|th|tbody|tfoot|thead",
3101       "seperator" => "br|hr",
3102       "formItems" => "input|textarea|select|option",
3103       "grouping" => "ol|ul|li",
3104       );
3105
3106  if (scalar @_ == 1)  # handle being called as function
3107  {
3108    $string = shift;
3109  }
3110  else # handle being called as method
3111  {
3112    my $self = shift;
3113    if (scalar @_ == 1)
3114    {
3115      $string = shift;
3116    }
3117    else
3118    {
3119      my %args = ( string => "", ignoreTags => "", sequence => "", @_ );
3120      $string = $args{string};
3121      $ignoreTags = (length $args{ignoreTags} > 0 ? $args{ignoreTags} : undef);
3122      $sequence = (length $args{sequence} > 0 ? $args{sequence} : undef);
3123
3124      if (!defined $ignoreTags && defined $sequence)
3125      {
3126        my @sequences = split /,/, $sequence;
3127        foreach my $sequence (@sequences)
3128        {
3129          $ignoreTags .= "|" if (length $ignoreTags);  # make sure multiple sequences are | seperated for the regular expression.
3130          $ignoreTags .= $sequences{$sequence};
3131          if (!exists $sequences{$sequence})
3132          {
3133            $self->setError(code => "1017");
3134            $self->displayError(message => "sequence = '$sequence' does not exist!");
3135          }
3136        }
3137      }
3138    }
3139  }
3140
3141  if (length $string > 0)
3142  {
3143    # handle the special cases first.
3144    foreach my $char (qw/&/)
3145    {
3146      $string =~ s/(?<!\\)[$char]/$formUnEncodedCharactersHash{$char}/emg;
3147      # now remove the \ from any chars that were escaped.
3148      $string =~ s/(\\([$char]))/$2/mg;
3149    }
3150    # now handle the rest.
3151    if (defined $ignoreTags)
3152    {
3153      # handle the < case where we encode < if it is not \ escaped and also not one of the ignoreTags values.
3154      $string =~ s/(?<!\\)(<)(?!(\/)?($ignoreTags)(\s+[^>]+)?(\/)?>)/$formUnEncodedCharactersHash{$1}/emg;
3155
3156      # handle the > case where we encode > if it is not \ escaped and does not have one of the ignoreTags before it.
3157      # escape the > tag when it is part of an ignoreTag entry.
3158      $string =~ s/(<(\/)?($ignoreTags)(\s+[^>]+)?(\/)?)(>)/$1\\$6/mg;
3159      # convert all non-escaped >'s.
3160      $string =~ s/(?<!\\)(>)/$formUnEncodedCharactersHash{$1}/emg;
3161
3162      # now remove the \ from any chars that were escaped.
3163      $string =~ s/(\\([$formUnEncodedCharacters]))/$2/mg;
3164    }
3165    else
3166    {
3167      $string =~ s/(?<!\\)([$formUnEncodedCharacters])/$formUnEncodedCharactersHash{$1}/emg;
3168      # now remove the \ from any chars that were escaped.
3169      $string =~ s/(\\([$formUnEncodedCharacters]))/$2/mg;
3170    }
3171    # now fixup \n to \\n, as long as it is not already \ escaped.
3172    $string =~ s/(?<!\\)(\n)/\\n/mg;
3173  }
3174
3175  return $string;
3176}
3177
3178sub AUTOLOAD  # Read only access to data objects.
3179{
3180  my $self = shift;
3181  my $type = ref($self) || DBIWrapper::myDie("$self is not an object.\n");
3182  my $name = $AUTOLOAD;
3183  $name =~ s/.*://; # strip fully-qualified portion
3184  unless (exists $self->{$name})
3185  {
3186    die "Can't access `$name' field in object of class $type";
3187  }
3188  return $self->{$name};
3189}
3190
3191sub myDie
3192{
3193  my $message = shift;
3194  my @callerArgs = caller(2);
3195  (my $subName = $callerArgs[3]) =~ s/^(.+)(::)([^:]+)$/$1->$3/;
3196  die "$subName: $message";
3197}
3198
3199sub DESTROY
3200{
3201  my $self = shift;
3202
3203  if (ref $self->{dbh} eq "DBI::db")
3204  {
3205    $self->close;
3206    if ($self->error)
3207    {
3208      die "DBIWrapper->DESTROY: " . $self->errorMessage;
3209    }
3210  }
3211}
3212
3213=item bool sybaseErrorHandler()
3214
3215returns 0 if the "error" is an informational error from sybase that we
3216can safely ignore.  Currently ignores the Text conversion errors that
3217are causing the connect to fail.
3218
3219returns 1 for all other errors to cause DBI to process them.
3220
3221=cut
3222sub sybaseErrorHandler
3223{
3224  my ($err, $sev, $state, $line, $server, $proc, $msg, $sql, $errType) = @_;
3225
3226  # ignore the "errors" that are informational.
3227  if (($err == 2401 && $sev == 11 && $state == 2)
3228   || ($err == 2411 && $sev == 10 && $state == 1)
3229   || ($err == 2409 && $sev == 11 && $state == 2))
3230  {
3231    return 0;
3232  }
3233  if ($err == 1205)
3234  {
3235    if ($deadlockEncountered)
3236    {
3237      print "ERROR:  deadlockEncountered is already set and we just encountered another deadlock!\n";
3238      return 1;
3239    }
3240    $deadlockEncountered = 1;
3241    return 0;
3242  }
3243  elsif ($deadlockEncountered)
3244  {
3245    print "WARNING:  Temporarily ignoring err=$err, severity=$sev, state=$state, line=$line, server=$server, proc=$proc, msg=$msg, sql='$sql' due to deadlock being encountered.\n";
3246    return 0;
3247  }
3248  #print "err=$err, sev=$sev, state=$state, line=$line, server=$server, proc=$proc, msg=$msg, sql='$sql', errType=$errType\n";
3249  return 1;
3250}
3251
3252=back
3253
3254=cut
3255
32561;
3257__END__
3258
3259=head1 NOTE:
3260
3261 All data fields are accessible by specifying the object and
3262 variable as follows:
3263 Ex.  $value = $obj->variable;
3264
3265 Any methods where it is possible to specify just a single
3266 argument and still have it be valid, you can now specify
3267 the argument without having to name it first.
3268
3269 Ex:  calling read() without using the substitute or plug
3270 options can be done as $dbi->read("SELECT * from test");
3271
3272 Methods updated to support this:
3273 setError, read, readXML, write, setLogLevel
3274
3275=head1 AUTHOR
3276
3277James A. Pattie, james at pcxperience dot com
3278
3279=head1 SEE ALSO
3280
3281perl(1), DBI(3), DBIWrapper::XMLParser(3), DBIWrapper::ResultSet(3)
3282
3283=cut
3284