# DBIWrapper.pm - The DataBase Wrapper Class that provides the DBI database # connection and core functions for working with DBI databases. # Created by James Pattie, 11/02/2000. # Copyright (c) 2000-2005 Xperience, Inc. http://www.pcxperience.com/ # All rights reserved. This program is free software; you can redistribute it # and/or modify it under the same terms as Perl itself. package DBIWrapper; use strict; use DBI; use vars qw($AUTOLOAD $VERSION @ISA @EXPORT @EXPORT_OK); require Exporter; @ISA = qw(Exporter AutoLoader); @EXPORT = qw(); $VERSION = '0.23'; use vars qw($formUnEncodedCharacters %formUnEncodedCharactersHash); $formUnEncodedCharacters = '<>"'; %formUnEncodedCharactersHash = ( '<' => '<', '>' => '>', '"' => '"', '&' => '&' ); =head1 NAME DBIWrapper - Perl extension for generic DBI database access. =head1 SYNOPSIS use DBIWrapper; my $db = DBIWrapper->new(dbType => "Pg", dbName => "test_db", dbHost => "localhost", dbUser => "nobody", dbPasswd => "", dbPort => "5432", predefinedDSN => "", printError => 1, raiseError => 1, autoCommit => 0); if ($db->error()) { die $db->errorMessage(); } my $sth = $db->read("SELECT * FROM test_tb"); my $result = $db->write(sql => "INSERT INTO test_tb (name, value) VALUES (?, ?)", plug => [ $name, $value ]); # this used DBI's substitution features to plugin the name and value. $db->close(); # close down the database connection. Any read()'s # or write()'s will no longer be valid with this object until a new() is # issued again. =head1 DESCRIPTION DBIWrapper is the generic database Object for accessing the DBI database interface. It provides the lowest level of functionality needed by any program wanting to access databases via the DBI. Currently, DBIWrapper is only aware of Pg (PostgreSQL), mysql (MySQL), Sybase and ODBC DBD modules and how to work with them correctly. Support for transactions on MySQL is now checked for and if found to be available, the AutoCommit flag is turned off so that transactions will be used. The substitution array (if used) will cause each ##?1##, ##?2##, etc. string in the sql string to be substituted for the corresponding value in the substitution array. It must start at ?1. It is up to the user to pass in the correct number of elements for both the plug and substitution arrays. The plug array is used to pass in the values for DBI to replace in the sql string of ? which is standard DBI notation. =head1 Sybase NOTES The getDataArray(), getDataArrayHeader(), getDataHash(), getDataHashHeader(), readXML(), readHTML() methods all properly handle multiple result sets being returned from Sybase. This could be the result of multiple select statements or a compute clause. In the case of the Header() methods, the header row is based on the first returned select statement, which may not be correct for the following statements or compute blocks. =head1 Exported FUNCTIONS B: I = 1(true), 0(false) =over 4 =item scalar new(dbType, dbName, dbHost, dbUser, dbPasswd, dbPort, printError, raiseError, autoCommit, predefinedDSN, setDateStyle, logLevel, server, interfaces) Creates a new instance of the DBIWrapper object and opens a connection to the specified database. If predefinedDSN is specified then it is used instead of the dbName, dbHost, dbPort values. This is mainly to support ODBC easier. If setDateStyle is 1 (default) and dbType = Pg, then the datestyle for PostgreSQL is set to US (MM/DD/YYYY). logLevel defaults to 0. There are 4 levels 0, 1, 2 and 3 which log the following items when an error occurs: 0) Nothing is output 1) dbType, dbHost, dbName, printError, raiseError, autoCommit, setDateStyle, supportsTransactions, transactionType, server, interfaces 2) all of 1 plus dbUser, dbPort, predefinedDSN 3) all of 2 plus dbPasswd Sybase specific: server allows you to specify the database server to connect to by name and must be defined in your interfaces file. interfaces allows you to specify the Sybase interfaces file needed to properly connect to the Sybase database. If you do not specify server and interfaces, then dbHost and dbPort will be used. =cut sub new { my $that = shift; my $class = ref($that) || $that; my $self = bless {}, $class; my %args = ( dbType => 'Pg', dbHost => 'localhost', dbUser => 'nobody', dbPasswd => '', dbPort => '5432', predefinedDSN => "", printError => 1, raiseError => 1, autoCommit => 0, setDateStyle => 1, logLevel => 0, interfaces => "", server => "", @_ ); my ($dbType, $dbName, $dbHost, $dbUser, $dbPasswd, $dbPort, $predefinedDSN, $printError, $raiseError, $autoCommit, $setDateStyle, $logLevel, $interfaces, $server); $dbType = $args{dbType}; $dbHost = $args{dbHost}; $dbName = $args{dbName}; $dbUser = $args{dbUser}; $dbPasswd = $args{dbPasswd}; $dbPort = $args{dbPort}; $predefinedDSN = $args{predefinedDSN}; $printError = $args{printError}; $raiseError = $args{raiseError}; $autoCommit = $args{autoCommit}; $setDateStyle = $args{setDateStyle}; $logLevel = $args{logLevel}; $interfaces = $args{interfaces}; $server = $args{server}; $self->{supportsTransactions} = 1; # by default all Databases support Transactions, except for MySQL. $self->{transactionType} = ""; # This is only set by MySQL so we know what type of transaction support is available. if ($dbType eq "mysql") { $autoCommit = 1; # it may not do transactions yet. $self->{supportsTransactions} = 0; } $self->{error} = 0; # nothing wrong yet. $self->{errorString} = ""; $self->{errorPhrase} = "() - Error!
\n"; $self->{dbType} = $dbType; $self->{dbHost} = $dbHost; $self->{dbName} = $dbName; $self->{dbUser} = $dbUser; $self->{dbPasswd} = $dbPasswd; $self->{dbPort} = $dbPort; $self->{predefinedDSN} = $predefinedDSN; $self->{printError} = $printError; $self->{raiseError} = $raiseError; $self->{autoCommit} = $autoCommit; $self->{setDateStyle} = $setDateStyle; $self->{logLevel} = $logLevel; $self->{interfaces} = (length $interfaces > 0 ? $interfaces : undef); $self->{server} = (length $server > 0 ? $server : undef); $self->{dbh} = undef; # set this explicitly now so that we have something to check if an error occurs later. if (!$self->isValid(new => 1)) { $self->error(errorString => "Error!
\n" . $self->errorMessage); return $self; } my $dbh; my $dsn; my %errorHandlers = (); $dsn = "dbi:$dbType:"; if ($dbType =~ /^(Pg|mysql)$/) { if (length $predefinedDSN > 0) { $dsn .= $predefinedDSN; } else { $dsn .= "dbname=$dbName;host=$dbHost;port=$dbPort"; } } elsif ($dbType eq "Sybase") { if (length $predefinedDSN > 0) { $dsn .= $predefinedDSN; } else { $dsn .= "database=$dbName"; if (defined $interfaces) { $dsn .= ";server=$server;interfaces=$interfaces"; } else { # use host and port $dsn .= ";host=$dbHost;port=$dbPort"; } } $errorHandlers{syb_err_handler} = \&sybaseErrorHandler; } elsif ($dbType eq "ODBC") { if (length $predefinedDSN > 0) { $dsn .= $predefinedDSN; } else { $self->error(errorString => "Error!
\nYou must specify the 'predefinedDSN' for dbType = '$dbType'!
\n" . $self->debugMessage); return $self; } } eval { $dbh = DBI->connect($dsn, $dbUser, $dbPasswd, { PrintError => $printError, RaiseError => $raiseError, AutoCommit => $autoCommit, %errorHandlers }); }; if ($@) { $self->{dbh} = undef; $self->error(errorString => "Eval of connect failed!
\nError = '$@'.
\nDBIError = '" . $DBI::errstr . "'.
\n" . $self->debugMessage); } else { if ($dbh) { if (!$DBI::err) { $self->{dbh} = $dbh; if ($dbType eq "Pg" && $setDateStyle) { $self->write(sql => "SET datestyle TO 'POSTGRES,US'"); } if ($dbType eq "mysql") { # check for Transaction support, and if present disable the AutoCommit flag. if ($self->mysqlHasTransactions) { $self->{supportsTransactions} = 1; $self->{dbh}->{AutoCommit} = 0; } if ($self->error) { $self->{dbh} = undef; $self->error(errorString => "Checking for Transactions with MySQL failed!
\n" . $DBI::errstr . "
\n" . $self->debugMessage); } } } else { $self->{dbh} = undef; $self->error(errorString => "connect failed!
\n" . $DBI::errstr . "
\n" . $self->debugMessage); } } else { $self->{dbh} = undef; $self->error(errorString => "connect failed!
\n" . $DBI::errstr . "
\n" . $self->debugMessage); } } return $self; } =item bool isValid() Returns 1 if the DBI object is valid, else 0 if invalid. =cut sub isValid { my $self = shift; my %args = ( new => 0, @_ ); my $new = $args{new}; my $valid = 1; my $errorString = ""; if (!$new) { if (!defined $self->{dbh}) { $errorString .= "dbh is not defined!
\n"; $valid = 0; } } if ($self->{dbType} !~ /^(Pg|mysql|ODBC|Sybase)$/) { $errorString .= "dbType = '$self->{dbType}' is invalid!
\n"; $valid = 0; } if (length $self->{predefinedDSN} == 0) # only check the dbHost, dbName and dbPort values if not using predefinedDSN { if (length $self->{dbName} == 0) { $errorString .= "dbName = '$self->{dbName}' is invalid!
\n"; $valid = 0; } if ($self->{dbType} eq "Sybase") { if (defined $self->{server} && !defined $self->{interfaces}) { $errorString .= "server = '$self->{server}'. interfaces must be specified!
\n"; $valid = 0; } if (defined $self->{interfaces} && !defined $self->{server}) { $errorString .= "interfaces = '$self->{interfaces}'. server must be specified!
\n"; $valid = 0; } } if ($self->{dbType} ne "Sybase" || ($self->{dbType} eq "Sybase" && (!defined $self->{server} && !defined $self->{interfaces}))) { if (length $self->{dbHost} == 0) { $errorString .= "dbHost = '$self->{dbHost}' is invalid!
\n"; $valid = 0; } if ($self->{dbPort} !~ /^(\d+)$/) { $errorString .= "dbPort = '$self->{dbPort}' is invalid!
\n"; $valid = 0; } } } if (length $self->{dbUser} == 0) { $errorString .= "dbUser = '$self->{dbUser}' is invalid!
\n"; $valid = 0; } if (length $self->{dbPasswd} == 0) { $errorString .= "dbPasswd = '$self->{dbPasswd}' is invalid!
\n"; $valid = 0; } if ($self->{autoCommit} !~ /^(0|1)$/) { $errorString .= "autoCommit = '$self->{autoCommit}' is invalid!
\n"; $valid = 0; } if ($self->{printError} !~ /^(0|1)$/) { $errorString .= "printError = '$self->{printError}' is invalid!
\n"; $valid = 0; } if ($self->{raiseError} !~ /^(0|1)$/) { $errorString .= "raiseError = '$self->{raiseError}' is invalid!
\n"; $valid = 0; } if ($self->{setDateStyle} !~ /^(0|1)$/) { $errorString .= "setDateStyle = '$self->{setDateStyle}' is invalid!
\n"; $valid = 0; } if ($self->{logLevel} !~ /^(0|1|2|3)$/) { $errorString .= "logLevel = '$self->{logLevel}' is invalid!
\n"; $valid = 0; } if (!$valid) { $self->error(errorString => "$errorString" . $self->debugMessage); } return $valid; } =item void close() Closes the connection to the database. =cut sub close { my $self = shift; if (defined $self->{dbh} && ref $self->{dbh} eq "DBI::db") { my $result; eval { $result = $self->{dbh}->disconnect; }; if ($@) { $self->error(errorString => "Eval of disconnect failed!
\nError = '$@'.
\n" . $self->debugMessage); return; } else { eval { if (!$result || $self->{dbh}->err) { $self->error(errorString => "disconnect failed!
\n" . $self->{dbh}->errstr . "
\n" . $self->debugMessage); return; } }; if ($@) { $self->error(errorString => "Eval of result check failed!
\nError = '$@'.
\n" . $self->debugMessage); return; } } $self->{dbh} = undef; # signal it is no longer valid! } } =item bool error(errorString) This method will set the error condition if an argument is specified. The current error state is returned, regardless of if we are setting an error or not. A \n is appended to the errorString so you don't have to provide it. errorString is prefixed with the caller's full method name followed by the errorPhrase string. You can either specify the errorString value by name: $self->error(errorString => "This is an error!"); or by value: $self->error("This is an error!"); If you specify multiple arguments (in pass by value mode), then we check to see if the first argument contains %'s that are not \ escaped and are not %%. If this is the case, then the incoming arguments will be passed through sprintf() for formatting, else we just join them with a space ' ' and append them to the current errorString. To see if an error happened: if ($self->error) { die "Error: " . $self->errorMessage; } =cut sub error { my $self = shift; my @callerArgs = caller(1); (my $subName = $callerArgs[3]) =~ s/^(.+)(::)([^:]+)$/$1->$3/; my $callerErrStr = "$subName$self->{errorPhrase}"; if (scalar @_ > 0) { # we are setting an error condition. if (scalar @_ == 1) { $self->{errorString} .= $callerErrStr . @_[0]; } else { if (@_[0] eq "errorString") { my %args = ( @_ ); if (!exists $args{errorString}) # make sure we get the errorString argument! { $self->error($callerErrStr . "errorString is missing!
\n"); return; } else { $self->{errorString} .= $callerErrStr . $args{errorString}; } } else { # handle the sprintf case. if (@_[0] =~ /(?{errorString} .= $callerErrStr; eval "\$self->{errorString} .= sprintf($str);"; if ($@) { $self->error($callerErrStr . $@); return; } } else { $self->{errorString} .= $callerErrStr . join(" ", @_); } } } $self->{errorString} .= "\n"; $self->{error} = 1; } return $self->{error}; } =item void setError(errorString) DEPRECATED: see error() optional: errorString returns: nothing Sets error = 1 and errorString = string passed in. The errorString is prefixed with the caller's full method name followed by the errorPhrase string. You can either call as setError(errorString => $string) or setError($string) If you do not specify anything, we blow an error telling you to specify errorString. \n is appended to the contents of the errorString passed in. =cut sub setError { my $self = shift; my @callerArgs = caller(1); (my $subName = $callerArgs[3]) =~ s/^(.+)(::)([^:]+)$/$1->$3/; my $callerErrStr = "$subName$self->{errorPhrase}"; my $deprecated = "DEPRECATED call to setError! Convert to using error().
\n"; if (scalar @_ == 1) { $self->{errorString} = $deprecated . $callerErrStr . @_[0]; } else { my %args = ( @_ ); if (!exists $args{errorString}) # make sure we get the errorString argument! { $self->setError($callerErrStr . "errorString is missing!
\n"); return; } else { $self->{errorString} = $deprecated . $callerErrStr . $args{errorString}; } } $self->{errorString} .= "\n"; $self->{error} = 1; } =item void prefixError(errorString) optional: errorString returns: nothing Sets error = 1 and prefixes errorString with string passed in. The errorString is prefixed with the caller's full method name followed by the errorPhrase string. You can either specify the errorString value by name: $self->prefixError(errorString => "This is an error!"); or by value: $self->prefixError("This is an error!"); If you specify multiple arguments (in pass by value mode), then we check to see if the first argument contains %'s that are not \ escaped and are not %%. If this is the case, then the incoming arguments will be passed through sprintf() for formatting, else we just join them with a space ' ' and append them to the current errorString. If you don't specify anything then If you have a previous error, we prefix the caller info to that error message. =cut sub prefixError { my $self = shift; my @callerArgs = caller(1); (my $subName = $callerArgs[3]) =~ s/^(.+)(::)([^:]+)$/$1->$3/; my $callerErrStr = "$subName$self->{errorPhrase}"; if (scalar @_ == 1) { $self->{errorString} = $callerErrStr . @_[0] . $self->{errorString} . "\n"; } else { if (@_[0] eq "errorString") { my %args = ( @_ ); if (!exists $args{errorString}) # make sure we get the errorString argument! { if ($self->{errorString}) { # prefix the old errorString value. $self->{errorString} = $callerErrStr . $self->{errorString}; } else { $self->error($callerErrStr . "errorString is missing!
\n"); return; } } else { $self->{errorString} = $callerErrStr . $args{errorString} . "\n" . $self->{errorString}; } } else { # handle the sprintf case. if (@_[0] =~ /(?{errorString}; $self->{errorString} = $callerErrStr; eval "\$self->{errorString} .= sprintf($str);"; if ($@) { $self->error($callerErrStr . $@); return; } $self->{errorString} .= "\n" . $oldErrorStr; } else { $self->{errorString} = $callerErrStr . join(" ", @_) . "\n" . $self->{errorString}; } } } $self->{error} = 1; } =item scalar didErrorOccur(void) DEPRECATED: see error() Returns the value of error. =cut sub didErrorOccur { my $self = shift; return $self->{error}; } =item scalar errorMessage(void) Returns the value of errorString. =cut sub errorMessage { my $self = shift; return $self->{errorString}; } =item scalar errorStr(void) Returns the value of errorString. Alternative to errorMessage(). =cut sub errorStr { my $self = shift; return $self->{errorString}; } =item void resetError(void) Resets the error condition flag and string. =cut sub resetError { my $self = shift; $self->{error} = 0; $self->{errorString} = ""; } =item void commit() causes the database to commit the current transaction. Only works if AutoCommit is set to 0 and the database supports Transactions. =cut sub commit { my $self = shift; if (!$self->{supportsTransactions}) { return; } eval { $self->{dbh}->commit; }; if ($@) { $self->error(errorString => "commit failed!
\nError = $@" . "
\n" . $self->debugMessage); } elsif ($DBI::err) { $self->error(errorString => "commit failed!
\nError = $DBI::errstr" . "
\n" . $self->debugMessage); } } =item void rollback() causes the database to rollback the current transaction. Only works if AutoCommit is set to 0 and the database supports Transactions. =cut sub rollback { my $self = shift; if (!$self->{supportsTransactions}) { return; } eval { $self->{dbh}->rollback; }; if ($@) { $self->error(errorString => "rollback failed!
\nError = $@" . "
\n" . $self->debugMessage); } elsif ($DBI::err) { $self->error(errorString => "rollback failed!
\nError = $DBI::errstr" . "
\n" . $self->debugMessage); } } =item ref read(sql => "", plug => [], substitute => []) (This function should only be called for SELECT statements). executes the specified sql statement passing in any values in plug to the execute method after doing any substitutions that are in substitute. The resulting sql data is passed back to the user as a reference for them to do with as they please. =cut sub read { my $self = shift; my $sql = ""; my @plug = (); my @substitute = (); if (scalar @_ == 1) { $sql = shift; } else { my %args = ( plug => [], substitute => [], @_ ); @plug = @{$args{'plug'}}; @substitute = @{$args{'substitute'}}; $sql = $args{'sql'}; } # validate we got a sql statement to work with. if (length $sql == 0) { $self->error(errorString => "SQL string not passed in!" . "
\n" . $self->debugMessage); return undef; } # check and see if we need to do any substitutions. if (scalar @substitute > 0) { for (my $i=0; $i < scalar @substitute; $i++) { my $temp_string = "\\#\\#\\?" . ($i+1) . "\\#\\#"; $sql =~ s/$temp_string/$substitute[$i]/g; } } # now prepare the statement my $sth; eval { $sth = $self->{dbh}->prepare($sql); }; if ($@) { $self->error(errorString => "Eval of prepare failed!
\nError = '$@'.
\nsql='$sql'.
\nplug='" . join("', '", @plug) . "'.
\n" . $self->debugMessage); return undef; } elsif (!$sth || $DBI::err) { $self->error(errorString => "Preparing failed!
\n" . $DBI::errstr . "
\nsql='$sql'.
\nplug='" . join("', '", @plug) . "'.
\n" . $self->debugMessage); return undef; } # now execute the sql statement passing in any parameters given via plug my $rc; eval { $rc = $sth->execute(@plug); }; if ($@) { $self->error(errorString => "Eval of execute failed!
\nError = '$@'.
\nsql='$sql'.
\nplug='" . join("', '", @plug) . "'.
\n" . $self->debugMessage); return undef; } elsif (!$rc || $DBI::err) { $self->error(errorString => "Execute failed!
\n" . $DBI::errstr . "
\nsql='$sql'.
\nplug='" . join("', '", @plug) . "'.
\n" . $self->debugMessage); return undef; } return $sth; } =item @ getDataArray(sql, plug, substitute) requires: sql optional: plug, substitute returns: array of arrayrefs as the result of $sth->fetchall_arrayref See read() for argument info. =cut sub getDataArray { my $self = shift; my $sql = ""; my $sth = undef; if (scalar @_ == 1) { $sql = shift; $sth = $self->read($sql); } else { my %args = ( plug => [], substitute => [], @_ ); $sth = $self->read(%args); } my @data = (); if ($self->error) { $self->prefixError(); return @data; } while (my $row = $sth->fetchrow_arrayref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results})) { if (defined $row && ref($row) eq "ARRAY" && scalar @{$row} > 0) { my @row = @{$row}; push @data, \@row; } } $sth->finish; return @data; } =item @ getDataHash(sql, plug, substitute, case) requires: sql optional: plug, substitute, case returns: array of hashrefs where the column names are case preserved if case = 1, or lowercased if case = 0. case defaults to 0 (lowercase). See read() for argument info. =cut sub getDataHash { my $self = shift; my $sql = ""; my $case = 0; my $sth = undef; if (scalar @_ == 1) { $self->{dbh}->{FetchHashKeyName} = 'NAME_lc'; $sql = shift; $sth = $self->read($sql); } else { my %args = ( plug => [], substitute => [], case => 0, @_ ); my $case = $args{case}; $self->{dbh}->{FetchHashKeyName} = ($case == 1 ? 'NAME' : 'NAME_lc'); $sth = $self->read(%args); } my @data = (); if ($self->error) { $self->prefixError(); return @data; } while (my $row = $sth->fetchrow_hashref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results})) { if (defined $row && ref($row) eq "HASH" && scalar keys %{$row} > 0) { my %row = %{$row}; push @data, \%row; } } $sth->finish; return @data; } =item @ getDataArrayHeader(sql, plug, substitute, case) requires: sql optional: plug, substitute, case returns: array of arrayrefs The first row of the array is an array containing the column names in the order returned by the database. The column names are case preserved if case = 1, or lowercased if case = 0. NOTE: If 0 rows were returned, we still return an array with 1 row in it, which is the header row. case defaults to 0 (lowercase). See read() for argument info. =cut sub getDataArrayHeader { my $self = shift; my $sql = ""; my $case = 0; my $sth = undef; if (scalar @_ == 1) { $sql = shift; $sth = $self->read($sql); } else { my %args = ( plug => [], substitute => [], case => 0, @_ ); my $case = $args{case}; $sth = $self->read(%args); } my @data = (); if ($self->error) { $self->prefixError(); return @data; } # get the column NAMES push @data, $sth->{($case == 1 ? 'NAME' : 'NAME_lc')}; while (my $row = $sth->fetchrow_arrayref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results})) { if (defined $row && ref($row) eq "ARRAY" && scalar @{$row} > 0) { my @row = @{$row}; push @data, \@row; } } $sth->finish; return @data; } =item @ getDataHashHeader(sql, plug, substitute, case) requires: sql optional: plug, substitute, case returns: array of hashrefs where the column names are case preserved if case = 1, or lowercased if case = 0. The first row of the array is an array containing the column names in the order returned by the database. The column names respect the case flag. NOTE: If 0 rows were returned, we still return an array with 1 row in it, which is the header row. case defaults to 0 (lowercase). See read() for argument info. =cut sub getDataHashHeader { my $self = shift; my $sql = ""; my $case = 0; my $sth = undef; if (scalar @_ == 1) { $self->{dbh}->{FetchHashKeyName} = 'NAME_lc'; $sql = shift; $sth = $self->read($sql); } else { my %args = ( plug => [], substitute => [], case => 0, @_ ); my $case = $args{case}; $self->{dbh}->{FetchHashKeyName} = ($case == 1 ? 'NAME' : 'NAME_lc'); $sth = $self->read(%args); } my @data = (); if ($self->error) { $self->prefixError(); return @data; } # get the column NAMES push @data, $sth->{($case == 1 ? 'NAME' : 'NAME_lc')}; while (my $row = $sth->fetchrow_hashref || ($self->{dbType} eq "Sybase" && $sth->{syb_more_results})) { if (defined $row && ref($row) eq "HASH" && scalar keys %{$row} > 0) { my %row = %{$row}; push @data, \%row; } } $sth->finish; return @data; } =item scalar readXML(sql, plug, substitute, columns, displayNULLAs, ignoreTags, sequence, displaySQL) requires: sql optional: plug, substitute, columns = 0, displayNULLAs, ignoreTags, sequence, displaySQL = 1 returns: valid XML document describing the data selected from the database. Uses getDataHashHeader() to actually validate the data and execute the SELECT statement. The resulting XML document will either have an error condition set (if read() signaled an error occured) or will be the result of traversing the data returned from getDataHashHeader(). If displaySQL = 0, then we do not output the If the XML document is an error document, then: else In \n) if $displaySQL; $xmlDoc .= <<"END_OF_CODE"; END_OF_CODE } else { # now process the result set returned and generate the XML document. $xmlDoc = <<"END_OF_CODE"; END_OF_CODE $xmlDoc .= qq(