Here's an updated version of what I'm thinking of with respect
to $sth->more_results (many thanks for the valuable feedback).
This is quite long because I've tried to consider all the issues.
=head1 Multiple result sets and the $sth->more_results method
There are 3 kinds of batches of SQL statements:
o Arrays of parameters - where an array of values is bound to
placeholders in a single statement.
[This kind of batch is supported via the bind_param_array() and
execute_array() methods and won't be discussed further here.]
o Explicit Batches - where the $sql passed to prepare() contains
multiple statements separated by semicolons.
o Procedures - where a procedure that contains multiple statements
is executed AND the database is one that returns the results
from those embedded statements (e.g. Sybase, MS SQL).
In the text below I'll refer to the individual statements within
a batch as sub-statements.
When a batch is first executed the statement handle appears exactly
as if the batch only contained the first sub-statement. (The only
exception is that the $sth->{NUM_OF_PARAMS} attribute equals the
total number of placeholders in the entire statement.)
The $sth->more_results method is used to move to the results of the
next sub-statement in the batch. If more_results() returns true then
the statement handle appears exactly as if the batch only contained
the next sub-statement. And so on. The $sth->more_results() method
I<appears> to have the effect of preparing and executing the next
sub-statement within the batch.
The more_results() method returns false in these situations:
If there is another sub-statement and that sub-statement fails
then
The error is recorded (which will trigger RaiseError if enabled).
more_results() returns 0;
else
# No more result sets. Either all succeeded or an error has caused
# execution of later statement to be skipped
more_results() returns undef;
end
Because the statement handle always appears as if only one statement
has been executed it follows that:
$sth->err can be used to tell if there was an error, and
$sth->{NUM_OF_FIELDS} > 0 if the sub-statement is a SELECT, and
$sth->rows can be used to get the row count if it isn't
(but see SQL_BATCH_ROW_COUNT below)
Whether the entire batch of statements is executed before any results
are available, or whether calling more_results() actually triggers
the execution of the next sub-statement in the batch, is undefined.
The state of statement handle attributes after more_results() returns
undef is undefined.
Placeholders work as if the entire statement string was a single statement.
If execute() is called with parameter values then the number of values must
match the total number of placeholders in the entire statement string.
The $h->{BatchOnlySelect} attribute can be used to skip results
from non-SELECT sub-statements.
=head2 Driver Metadata via get_info()
Three get_info() types are relevant here: SQL_MULT_RESULT_SETS,
SQL_BATCH_SUPPORT and SQL_BATCH_ROW_COUNT. I'll use the symbolic
names available via the DBI::Const::GetInfoType module:
use DBI::Const::GetInfoType;
=head3 SQL_MULT_RESULT_SETS
my $multresults_support = $dbh->get_info($GetInfoType{SQL_MULT_RESULT_SETS});
returns 'Y' is the driver supports multiple result sets and 'N' otherwise.
The likes of MS SQL Server, Sybase and DB2 return 'Y'.
=head3 SQL_BATCH_SUPPORT
my $batch_support = $dbh->get_info($GetInfoType{SQL_BATCH_SUPPORT});
This returns a bitmask indicating the drivers support for batches:
o SQL_BS_SELECT_EXPLICIT: The driver supports Explicit Batches that can
have result-set generating statements.
o SQL_BS_ROW_COUNT_EXPLICIT The driver supports Explicit Batches that can
have row-count generating statements.
o SQL_BS_SELECT_PROC As above but for Procedures
o SQL_BS_ROW_COUNT_PROC As above but for Procedures
The likes of MS SQL Server sets all bits, DB2 sets SELECT_EXPLICT,
ROW_COUNT_EXPLICIT and SELECT_PROC but not COUNT_PROC.
=head3 SQL_BATCH_ROW_COUNT
my $batch_row_count = $dbh->get_info($GetInfoType{SQL_BATCH_ROW_COUNT});
This returns a bitmask indicating how row counts from consecutive
INSERT, DELETE, or UPDATE statements within the batch are made
available:
o SQL_BRC_ROLLED_UP
Row counts for consecutive INSERT, DELETE, or UPDATE
statements are rolled up into one. If this bit is not set,
then row counts are available for each individual statement.
o SQL_BRC_PROCEDURES
Row counts, if any, are available when a batch is executed
in a stored procedure. If row counts are available, they
can be rolled up or individually available, depending on
the SQL_BRC_ROLLED_UP bit.
o SQL_BRC_EXPLICIT
Row counts, if any, are available when a batch is executed
directly by calling SQLExecute or SQLExecDirect. If row
counts are available, they can be rolled up or individually
available, depending on the SQL_BRC_ROLLED_UP bit.
=head2 Example Implementation via DBI Subclass
Another way to think about the behaviour of more_results() is to
imagine a DBI subclass that did something like this:
sub prepare {
my ($dbh, $Statement) = @_;
my @statements = split /;/, $Statement; # split statements
my $sth = $dbh->SUPER::prepare(shift @statements); # prepare the first
$sth->{statements} = [EMAIL PROTECTED] # store the rest
if @statements;
return $sth;
}
sub execute {
my $sth = shift;
if ($sth->{statements}) { # this is a batch
$sth->{parameters} = [ @_ ]; # store the parameters
return $sth->_more_results_execute();
}
return $sth->SUPER::execute(@_);
}
sub more_results {
my ($sth) = @_;
$sth->_more_results_prepare() or return;
return $sth->_more_results_execute();
}
sub _more_results_prepare {
my ($sth) = @_;
my $statements = $sth->{statements}
or return undef; # doesn't have multiple statements
next_results:
my $Statement = shift @$statements
or return undef; # no more statements left
$sth->prepare($Statement)
or return 0; # prepare failure
return $sth;
}
sub _more_results_execute {
my ($sth) = @_;
execute_next:
my @params = splice @{ $sth->{parameters} }, 0, $sth->{NUM_OF_PARAMS};
my $rv = $sth->execute(@params)
or return 0; # execute failure
if ($sth->{BatchOnlySelect} && $sth->{NUM_OF_FIELDS} == 0) {
# skip this non-select sub-statement
$sth->_more_results_prepare or return;
goto execute_next;
}
return $rv; # must be true but is otherwise undefined
}
sub finish {
my ($sth) = @_;
$sth->SUPER::finish; # discard any unfetched from current sub-statement
$sth->{statements} = undef; # discard any unexecuted sub-statements
$sth->{parameters} = undef; # discard any unused parameters
return 1;
}
Note that SELECT statements which return no rows are never skipped.
Only non-SELECT statements are skipped by $sth->{BatchOnlySelect}.
We can discuss the name and default setting of that attribute later (not now, please).
If a batch of statements update the values of placeholders which
are bound using bind_param_inout() the output values are only
guaranteed to be available after all results have been processed
(ie when more_results() returns undef). Though some drivers may be
able to update the values sooner, or update them more than once.
[In practice a driver may have to call the 'more_results' function
of the underlying database API at the end of each result set
in order to correctly implement the desired behaviour.]
=head2 Application Examples
The generic loop to fetch multiple result sets is like this:
$sth->execute;
do {
if ($sth->err) {
...handle sub-statement error... (if RaiseError not set)
}
elsif ($sth->{NUM_OF_FIELDS}) {
while (@data = $sth->fetchrow_array) {
...
}
}
else {
printf "Non-SELECT: %d rows\n", $sth->rows;
}
} while (defined $sth->more_results);
If the application is happy to exit the loop if any sub-statement
trigers an error then the first C<if> and the C<defined> can be removed.
Then the code reduces to:
$sth->execute or ...;
do {
if ($sth->{NUM_OF_FIELDS}) {
while (@data = $sth->fetchrow_array) {
...
}
}
else {
printf "Non-SELECT: %d rows\n", $sth->rows;
}
} while ($sth->more_results);
and if $sth->{BatchOnlySelect} is set then that simplifies down to:
$sth->execute or ...;
do {
while (@data = $sth->fetchrow_array) {
...
}
} while ($sth->more_results};
or even, for the do-loop-phobic, just:
$sth->execute or ...;
while ($data = $sth->fetchrow_arrayref
or $sth->more_results && $data = $sth->fetchrow_arrayref
) {
...
}
=head2 Related Changes
The selectall_* methods could perhaps be extended to suport an
optional attribute which would make them more_results() and loop.
Naturally that would only be used by applications in cases where
it was useful.
=head2 Issues
Active: the principles above mean that $sth->{Active} goes false
at the end of each set of results. That goes against the definition
of what Active means but I think that's a small issue compared with
the greater consistency elsewhere. A new attribute could be added
to fill the gap if need be.
finish: $sth->finish will discard all pending result sets.
($sth->more_results automatically discards any unfetched
rows from the current sub-statement.)
row errors: row fetch errors (where a row has an error, like a
truncation, but more rows can still be fetched) could be distinguished
previously by the fetch*() method returning false but Active still
being true. That will still be true.
err: Obviously some errors will be detected and reported by the
original prepare() and execute() but others will not be detected
till later. As far as possible more_results() is the place to
report any errors that would have been detected and reported by a
corresponding prepare() and execute() of the individual sub-statement.
(Even if the driver has to call it's own databases' more_results()
function at the end of the previous result set, any error should
be stored internally and not reported till the $sth->more_results()
method is called.)
BatchOnlySelect: How to behave if $sth->{BatchOnlySelect}
is true and a non-select statement generates an error?
We could:
a) ignore the error.
b) report the error as if BatchOnlySelect wasn't set.
c) report the error unless $sth->{BatchOnlySelect} >= 2 (my preference)
BatchOnlySelect must work even if a non-select is the first
sub-statement, i.e., the initial execute() must also ignore an
initial non-select.
So BatchOnlySelect must only apply to batches (hence prepare() or
execute() must be aware that they're working with a batch, either
and explicit one or a stored procedure).
If BatchOnlySelect is set and the batch only contains non-select
statements then the initial execute() would return false and not "0E0".
Comments?
Tim.
p.s. Many thanks to Michael Peppler, Jeff Urlwin and Martin Evans for their
valuable research and assistance.