Here's a further update 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 Stored Procedures - where a procedure that contains multiple
   statements is executed AND the database is one that returns
   back to the client the results from those embedded statements.

 o Explicit Batches - where the $sql passed to prepare() contains
   multiple statements. The statements are typically delimited by
   semicolons but database specific and that's not relevant here.
   Some databases, such as Sybase, don't make a distinction between
   Explicit Batches and Stored Procedures.

In the text below I'll refer to the individual statements within
a batch as I<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.

Some errors will obviously be detected and reported by the original
prepare() and execute() but others will not be detected till later.
As far as possible more_results() should report 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, an error relating to the next sub-statement
should be stored internally and not reported till the $sth->more_results()
method is called.

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.


=head2 Skipping Non-SELECT Results

When executing stored procedures, for example, applications are
sometimes only interested in the results of SELECT statements executed
by the procedure and not in any INSERTs, DELETEs, or UPDATEs that
it may also have executed. So it would be helpful to make it easy
to skip those non-SELECT results (as DBD::ODBC currently does).

However, extending the DBI specification to support skipping non-SELECT
results from batches is problematic for several reasons, including
that execution of sub-statements is effectively split between the
initial execute() and subsequent calls to more_results().

A complete implementation of skipping non-SELECT results would require
that execute() should call more_results() whenever NUM_OF_FIELDS was 0.
(It can't be limited to apply only to "batches" because many drivers
can't tell if there are multiple results until they try to fetch them.)

So, at least for now, this feature won't be part of the DBI but,
as always, drivers are free to implement their own mechanisms
under the control of driver-specific attributes.


=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:

  SQL_BS_SELECT_EXPLICIT

The driver supports Explicit Batches that can have I<result-set> generating statements.

  SQL_BS_ROW_COUNT_EXPLICIT

The driver supports Explicit Batches that can have I<row-count> generating statements.

  SQL_BS_SELECT_PROC
  SQL_BS_ROW_COUNT_PROC

As above but for Stored Procedures

The likes of MS SQL Server sets all bits, DB2 sets SQL_BS_SELECT_EXPLICIT,
SQL_BS_ROW_COUNT_EXPLICIT and SQL_BS_SELECT_PROC but not SQL_BS_ROW_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 I<consecutive>
INSERT, DELETE, or UPDATE statements within the batch are made
available:

  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.

  SQL_BRC_PROCEDURES

Row counts, if any, are available when a batch is executed I<within> a
stored procedure. If row counts are available, they can be rolled
up or individually available, depending on the SQL_BRC_ROLLED_UP bit.

  SQL_BRC_EXPLICIT

Row counts, if any, are available when an Explicit Batch is executed.
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

In this section I've provided an I<example> proof-of-concept
implementation (in the form of a DBI subclass) as a way of
expressing the behaviour and exploring the issues.

It is I<not> intended to be a model implementation for drivers to
copy because drivers for databases which support multiple results
should just let the database look after it.

(Drivers for other databases can just ignore this whole thread,
at least until the DBI has a working preparse() method :)

I have left a $sth->{SkipNonSelect} attribute in this example but,
as discussed above, I'm not proposing to include it in the spec.

Also, this example doesn't do-the-right-thing with NUM_OF_PARAMS
because it doesn't set it the the total number of params required.


sub prepare {
    my ($dbh, $Statement) = @_;
    my @statements = split /;/, $Statement; # (obviously dumb example)
    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;
    $sth->{parameters} = [ @_ ];
    my @params = splice @{ $sth->{parameters} }, 0, $sth->{NUM_OF_PARAMS};
    my $rv = $sth->SUPER::execute(@params);
    if ($sth->{SkipNonSelect} && $sth->{NUM_OF_FIELDS} == 0) {
        # Application wants to skip this non-SELECT statement,
        # so call more_results() to see if there any any SELECTS,
        # if so we'll return the result of more_results(), but if not
        # then we'll return the original $rv (so this is transparent)
        my $more_rv = $sth->more_results();
        return $more_rv if defined $more_rv;
    }
    retun $rv;
}

sub more_results {
    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
    my @params = splice @{ $sth->{parameters} }, 0, $sth->{NUM_OF_PARAMS};
    my $rv = $sth->execute(@params)
        or return 0;    # execute failure
    if ($sth->{SkipNonSelect} && $sth->{NUM_OF_FIELDS} == 0) {
        goto next_results;
    }
    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;
}

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. In that case
it would typically just set a flag and still defer the preparing
of the new statement attributes until $sth->more_results is called.]


=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 you've no interest in any non-SELECT results that might be
returned, then that simplifies down to:

  $sth->execute or ...;
  do {
      next unless $sth->{NUM_OF_FIELDS} > 0;
      while (@data = $sth->fetchrow_array) {
          ...
      }
  } while ($sth->more_results};
 
or even, for the do-loop-phobic with no non-SELECT results, just:

  $sth->execute or ...;
  while ($data = $sth->fetchrow_arrayref
     or $sth->more_results && $data = $sth->fetchrow_arrayref
  ) {
      ...
  }
 
=head2 Active

The interaction between the Active attribute, the finish method,
and row fetch errors needs to be reconsidered in relation to batches.

The finish() method will discard I<all> pending result sets.
The more_results() method automatically discards any unfetched
rows from the current sub-statement.

The statement handle Active attribute is used to indicate that there
is active state on the database server associated with this handle.
In other words that there is more data to be fetched, and that
finish() should be called before the statement handle is destroyed.

The principles described at the start mean that $sth->{Active}
I<should> go false at the end of I<each> set of SELECT results.
However, that goes against the definition of what Active means
because in a batch there I<is> still active state on the server.

So Active will remain true while there is active state on the server.
This looser definition gives more flexibility to the driver and
should discourage applications from making inappropriate assumptions.

The original principle still applies: Active should automatically
go false after results from a statement handle are fully consumed,
and $sth->finish can be used to force it to false if required.

This means:

  Active should be false after execute() of a single non-SELECT
  statement, or a stored procedure that returns a single non-SELECT
  statement row count. Applications shouldn't need to call finish()
  in these cases.

  Active can be true for a non-SELECT sub-statement (within a batch),
  which isn't the case currently. But no code should be relying on that.

=head2 Non-terminal Row Errors

A non-terminal row fetch error occurs where a row has an error,
like a truncation or divide by zero, I<but more rows can still be fetched>.

Currently that can be distinguished from a terminal error (where
more rows I<can't> be fetched) because Active remains true. But now
that Active will stay true till the end of a batch that can't be used.

This isn't a big issue because (a) very few applications need to
distinguish recover from non-terminal row fetch errors, and
(b) it's always possible to just try to fetch the next row.
So I'm inclined to ignore it, at least for now.

=cut


Comments?

Tim.

p.s. Many thanks to Michael Peppler, Jeff Urlwin and Martin Evans for their
valuable research and assistance.

Reply via email to