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 = ( '<' => '<', '>' => '>', '"' => '"', '&' => '&' ); 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 <, > 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 &, <, >. 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