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.

Reply via email to