Oracle supports the use of select list expressions of type REF CURSOR.
These may be explicit cursor expressions - C<CURSOR(SELECT ...)>, or
calls to PL/SQL functions which return REF CURSOR values. The values
of these expressions are known as nested cursors.

DBD::Oracle-1.16 does not support queries using nested cursors.
It produces the following reasonable warning message at (pre-)execute
time before dumping core at fetch time:

 Field [...] has an Oracle type (116) which is not explicitly supported

The patch below adds support for these queries to DBD::Oracle.

This patch is against the 1.16 distribution. If it is deemed
acceptable in principle, I will be happy to re-work it against
the latest development version.

--
Charles Jardine - Computing Service, University of Cambridge
[EMAIL PROTECTED]    Tel: +44 1223 334506, Fax: +44 1223 334679


diff -r -u -N DBD-Oracle-1.16/Oracle.pm DBD-Oracle-1.16-nested/Oracle.pm --- DBD-Oracle-1.16/Oracle.pm 2004-10-21 20:07:53.000000000 +0100 +++ DBD-Oracle-1.16-nested/Oracle.pm 2005-05-02 12:51:52.138587000 +0100 @@ -2447,6 +2447,78 @@ See the C<curref.pl> script in the Oracle.ex directory in the DBD::Oracle source distribution for a complete working example.

+=head1 Nested Cursors
+
+Oracle supports the use of select list expressions of type REF CURSOR.
+These may be explicit cursor expressions - C<CURSOR(SELECT ...)>, or
+calls to PL/SQL functions which return REF CURSOR values. The values
+of these expressions are known as nested cursors.
+
+The value returned to a Perl program when a nested cursor is fetched
+is a statement handle. This statement handle is ready to be fetched from.
+It should not (indeed, must not) be executed.
+
+Oracle imposes a restriction on the order of fetching when nested
+cursors are used. Suppose C<$sth1> is a handle for a select statement
+involving nested cursors, and C<$sth2> is a nested cursor handle fetched
+from C<$sth1>. C<$sth2> can only be fetched from while C<$sth1> is
+still active, and the row containing C<$sth2> is still current in C<$sth1>.
+Any attempt to fetch another row from C<$sth1> renders all nested cursor
+handles previously fetched from C<$sth1> defunct.
+
+Fetching from such a defunct handle results in an error with the message
+C<ERROR nested cursor is defunct (parent row is no longer current)>.
+
+This means that the C<fetchall...> or C<selectall...> methods are not useful
+for queries returning nested cursors. By the time such a method returns,
+all the nested cursor handles it has fetched will be defunct.
+
+It is necessary to use an explicit fetch loop, and to do all the
+fetching from nested cursors within the loop, as the following example
+shows:
+
+    use DBI;
+    my $dbh = DBI->connect(...);
+    my $sth = $dbh->prepare(q{
+        SELECT ename, CURSOR(
+            SELECT ename FROM emp WHERE mgr = mgr.empno
+          ) FROM emp mgr
+        });
+    $sth->execute;
+    while ( my ($mgr, $nested) = $sth->fetchrow_array ) {
+        print "$mgr\n";
+        while ( my ($emp) = $nested->fetchrow_array ) {
+            print "        $emp\n";
+        }
+    }
+
+The cursor returned by the function C<sp_ListEmp> defined in the
+previous section can be fetched as a nested cursor as follows:
+
+    my $sth = $dbh->prepare(q{SELECT sp_ListEmp FROM dual});
+    $sth->execute;
+    my ($nested) = $sth->fetchrow_array;
+    while ( my @row = $nested->fetchrow_array ) { ... }
+
+=head2 Pre-fetching Nested Cursors
+
+By default, DBD::Oracle pre-fetches rows in order to reduce the number of
+round trips to the server. For queries which do not involve nested cursors,
+the number of pre-fetched rows is controlled by the DBI database handle
+attribute C<RowCacheSize> (q.v.).
+
+In Oracle, server side open cursors are a controlled resource, limited in
+number, on a per session basis, to the value of the initialization
+parameter C<OPEN_CURSORS>. Nested cursors count towards ths limit.
+Each nested cursor in the current row counts 1, as does
+each nested cursor in a pre-fetched row. Defunct nested cursors do not count.
+
+An Oracle specific database handle attribute, C<ora_max_nested_cursors>,
+further controls pre-fetching for queries involving nested cursors. For
+each statement handle, the total number of nested cursors in pre-fetched
+rows is limited to the value of this parameter. The default value
+is 0, which disables pre-fetching for queries involving nested cursors.
+
 =head1 Returning A Value from an INSERT

 Oracle supports an extended SQL insert syntax which will return one
diff -r -u -N DBD-Oracle-1.16/dbdimp.c DBD-Oracle-1.16-nested/dbdimp.c
--- DBD-Oracle-1.16/dbdimp.c    2004-10-18 09:25:11.000000000 +0100
+++ DBD-Oracle-1.16-nested/dbdimp.c     2005-04-27 11:19:41.249418000 +0100
@@ -766,6 +766,9 @@
     else if (kl==12 && strEQ(key, "RowCacheSize")) {
        imp_dbh->RowCacheSize = SvIV(valuesv);
     }
+    else if (kl==22 && strEQ(key, "ora_max_nested_cursors")) {
+       imp_dbh->max_nested_cursors = SvIV(valuesv);
+    }
     else if (kl==11 && strEQ(key, "ora_ph_type")) {
         if (SvIV(valuesv)!=1 && SvIV(valuesv)!=5 && SvIV(valuesv)!=96 && 
SvIV(valuesv)!=97)
            warn("ora_ph_type must be 1 (VARCHAR2), 5 (STRING), 96 (CHAR), or 97 
(CHARZ)");
@@ -804,6 +807,9 @@
     else if (kl==12 && strEQ(key, "RowCacheSize")) {
        retsv = newSViv(imp_dbh->RowCacheSize);
     }
+    else if (kl==22 && strEQ(key, "ora_max_nested_cursors")) {
+       retsv = newSViv(imp_dbh->max_nested_cursors);
+    }
     else if (kl==11 && strEQ(key, "ora_ph_type")) {
        retsv = newSViv(imp_dbh->ph_type);
     }
@@ -1610,6 +1616,15 @@
        PerlIO_printf(DBILOGFP, "    dbd_st_execute %s (out%d, lob%d)...\n",
            oci_stmt_type_name(imp_sth->stmt_type), outparams, 
imp_sth->has_lobs);

+    /* Don't attempt execute for nested cursor. It would be meaningless,
+       and Oracle code has been seen to core dump */
+    if (imp_sth->nested_cursor) {
+       oci_error(sth, NULL, OCI_ERROR,
+           "explicit execute forbidden for nested cursor");
+       return -2;
+    }
+
+
     if (outparams) {   /* check validity of bind_param_inout SV's      */
        int i = outparams;
        while(--i >= 0) {
@@ -1798,6 +1813,9 @@
     dTHR;
     D_imp_dbh_from_sth;
     sword status;
+    int num_fields = DBIc_NUM_FIELDS(imp_sth);
+    int i;
+

     if (DBIc_DBISTATE(imp_sth)->debug >= 6)
         PerlIO_printf(DBIc_LOGPIO(imp_sth), "    dbd_st_finish\n");
@@ -1812,8 +1830,10 @@
     /* Turn off ACTIVE here regardless of errors below.                */
     DBIc_ACTIVE_off(imp_sth);

-    if (imp_sth->disable_finish)    /* see ref cursors      */
-       return 1;
+    for(i=0; i < num_fields; ++i) {
+       imp_fbh_t *fbh = &imp_sth->fbh[i];
+       if (fbh->fetch_cleanup) fbh->fetch_cleanup(sth, fbh);
+    }

     if (dirty)                 /* don't walk on the wild side  */
        return 1;
@@ -1884,14 +1904,23 @@
     sword status;
     dTHX ;

+    /* Don't free the OCI statement handle for a nested cursor. It will
+       be reused by Oracle on the next fetch. Indeed, we never
+       free these handles. Experiment shows that Oracle frees them
+       when they are no longer needed.
+    */
+
     if (DBIc_DBISTATE(imp_sth)->debug >= 6)
        PerlIO_printf(DBIc_LOGPIO(imp_sth), "    dbd_st_destroy %s\n",
-        (dirty) ? "(OCIHandleFree skipped during global destruction)" : "");
+        (dirty) ? "(OCIHandleFree skipped during global destruction)" :
+        (imp_sth->nested_cursor) ?"(OCIHandleFree skipped for nested cursor)" : 
"");

     if (!dirty) { /* XXX not ideal, leak may be a problem in some cases */
-       OCIHandleFree_log_stat(imp_sth->stmhp, OCI_HTYPE_STMT, status);
-       if (status != OCI_SUCCESS)
-           oci_error(sth, imp_sth->errhp, status, "OCIHandleFree");
+       if (!imp_sth->nested_cursor) {
+           OCIHandleFree_log_stat(imp_sth->stmhp, OCI_HTYPE_STMT, status);
+           if (status != OCI_SUCCESS)
+               oci_error(sth, imp_sth->errhp, status, "OCIHandleFree");
+       }
     }

     /* Free off contents of imp_sth    */
diff -r -u -N DBD-Oracle-1.16/dbdimp.h DBD-Oracle-1.16-nested/dbdimp.h
--- DBD-Oracle-1.16/dbdimp.h    2004-10-19 22:43:24.000000000 +0100
+++ DBD-Oracle-1.16-nested/dbdimp.h     2005-04-27 11:20:22.489833000 +0100
@@ -88,6 +88,7 @@
     int ph_type;               /* default oratype for placeholders */
     ub1 ph_csform;             /* default charset for placeholders */
     int parse_error_offset;    /* position in statement of last error */
+    int max_nested_cursors;     /* limit on cached nested cursors per stmt */
 };

 #define DBH_DUP_OFF sizeof(dbih_dbc_t)
@@ -110,7 +111,7 @@
     U16                auto_lob;
     int        has_lobs;
     lob_refetch_t *lob_refetch;
-    int        disable_finish; /* fetched cursors can core dump in finish */
+    int        nested_cursor;  /* cursors fetched from SELECTs */

     /* Input Details   */
     char      *statement;      /* sql (see sth_scan)           */
@@ -157,6 +158,7 @@
     void *desc_h;      /* descriptor if needed (LOBs etc)      */
     ub4   desc_t;      /* OCI type of descriptorh              */
     int  (*fetch_func) _((SV *sth, imp_fbh_t *fbh, SV *dest_sv));
+    void (*fetch_cleanup) _((SV *sth, imp_fbh_t *fbh));

     ub2  dbtype;       /* actual type of field (see ftype)     */
     ub2  dbsize;
diff -r -u -N DBD-Oracle-1.16/oci8.c DBD-Oracle-1.16-nested/oci8.c
--- DBD-Oracle-1.16/oci8.c      2004-10-21 10:17:51.000000000 +0100
+++ DBD-Oracle-1.16-nested/oci8.c       2005-05-02 14:03:56.355251000 +0100
@@ -575,6 +575,81 @@

 /* ------ */

+static void
+fetch_cleanup_rset(SV *sth, imp_fbh_t *fbh)
+{
+    SV *sth_nested = (SV *)fbh->special;
+    fbh->special = NULL;
+
+    if (sth_nested) {
+       dTHR;
+       D_impdata(imp_sth_nested, imp_sth_t, sth_nested);
+        int fields = DBIc_NUM_FIELDS(imp_sth_nested);
+       int i;
+       for(i=0; i < fields; ++i) {
+           imp_fbh_t *fbh_nested = &imp_sth_nested->fbh[i];
+           if (fbh_nested->fetch_cleanup)
+               fbh_nested->fetch_cleanup(sth_nested, fbh_nested);
+       }
+       DBIc_ACTIVE_off(imp_sth_nested);
+       SvREFCNT_dec(sth_nested);
+    }
+}
+
+static int
+fetch_func_rset(SV *sth, imp_fbh_t *fbh, SV *dest_sv)
+{
+    OCIStmt *stmhp_nested = ((OCIStmt **)fbh->fb_ary->abuf)[0];
+
+    D_imp_sth(sth);
+    D_imp_dbh_from_sth;
+    dSP;
+    HV *init_attr = newHV();
+    int count;
+
+    ENTER; SAVETMPS; PUSHMARK(SP);
+    XPUSHs(sv_2mortal(newRV((SV*)DBIc_MY_H(imp_dbh))));
+    XPUSHs(sv_2mortal(newRV((SV*)init_attr)));
+    PUTBACK;
+    count = perl_call_pv("DBI::_new_sth", G_ARRAY);
+    SPAGAIN;
+    if (count != 2)
+        croak("panic: DBI::_new_sth returned %d values instead of 2", count);
+    POPs;
+    sv_setsv(dest_sv, POPs);
+    SvREFCNT_dec(init_attr);
+    PUTBACK; FREETMPS; LEAVE;
+
+    if (DBIS->debug >= 3)
+        PerlIO_printf(DBILOGFP,
+               "       fetch_func_rset - allocated %s for nested cursor\n",
+                               neatsvpv(dest_sv, 0));
+
+    fbh->special = (void *)newSVsv(dest_sv);
+
+    {
+       D_impdata(imp_sth_nested, imp_sth_t, dest_sv);
+       imp_sth_nested->envhp = imp_sth->envhp;
+       imp_sth_nested->errhp = imp_sth->errhp;
+       imp_sth_nested->srvhp = imp_sth->srvhp;
+       imp_sth_nested->svchp = imp_sth->svchp;
+
+       imp_sth_nested->stmhp = stmhp_nested;
+       imp_sth_nested->nested_cursor = 1;
+
+        imp_sth_nested->stmt_type = OCI_STMT_SELECT;
+
+       DBIc_IMPSET_on(imp_sth_nested);
+
+       DBIc_ACTIVE_on(imp_sth_nested);  /* So describe won't do an execute */
+
+        if (!dbd_describe(dest_sv, imp_sth_nested)) return 0;
+    }
+
+    return 1;
+}
+/* ------ */
+

 int
 dbd_rebind_ph_rset(SV *sth, imp_sth_t *imp_sth, phs_t *phs)
@@ -1032,6 +1107,7 @@
     int num_errors = 0;
     int has_longs = 0;
     int est_width = 0;         /* estimated avg row width (for cache)  */
+    int nested_cursors = 0;
     ub4 i = 0;
     sword status;

@@ -1205,6 +1281,14 @@
                break;
 #endif

+       case 116:                               /* RSET         */
+               fbh->ftype  = fbh->dbtype;
+               fbh->disize = sizeof(OCIStmt *);
+               fbh->fetch_func = fetch_func_rset;
+               fbh->fetch_cleanup = fetch_cleanup_rset;
+               nested_cursors++;
+               break;
+
        case 182:                  /* INTERVAL YEAR TO MONTH */
        case 183:                  /* INTERVAL DAY TO SECOND */
        case 187:                  /* TIMESTAMP */
@@ -1256,6 +1340,10 @@
        ub4 cache_mem  = 0; /* so memory isn't the limit */
        ub4 cache_rows = calc_cache_rows((int)num_fields,
                                est_width, imp_sth->cache_rows, has_longs);
+       if (nested_cursors) {
+           int row_limit = imp_dbh->max_nested_cursors / nested_cursors;
+           if (cache_rows > row_limit) cache_rows = row_limit;
+        }
        imp_sth->cache_rows = cache_rows;    /* record updated value */
        OCIAttrSet_log_stat(imp_sth->stmhp, OCI_HTYPE_STMT,
            &cache_mem,  sizeof(cache_mem), OCI_ATTR_PREFETCH_MEMORY,
@@ -1271,6 +1359,10 @@
     else {                             /* set cache size by memory     */
        ub4 cache_mem  = -imp_sth->cache_rows; /* cache_mem always +ve here */
        ub4 cache_rows = 100000;        /* set high so memory is the limit */
+       if (nested_cursors) {
+           int row_limit = imp_dbh->max_nested_cursors / nested_cursors;
+           if (cache_rows > row_limit) cache_rows = row_limit;
+        }
        OCIAttrSet_log_stat(imp_sth->stmhp, OCI_HTYPE_STMT,
            &cache_rows, sizeof(cache_rows), OCI_ATTR_PREFETCH_ROWS,
            imp_sth->errhp, status);
@@ -1299,6 +1391,12 @@
        fbh->fb_ary = fb_ary_alloc(define_len, 1);
        fb_ary = fbh->fb_ary;

+       if (fbh->ftype == 116) { /* RSET */
+           OCIHandleAlloc_ok(imp_sth->envhp,
+               (dvoid*)&((OCIStmt **)fb_ary->abuf)[0],
+                OCI_HTYPE_STMT, status);
+       }
+
        OCIDefineByPos_log_stat(imp_sth->stmhp, &fbh->defnp,
            imp_sth->errhp, (ub4) i,
            (fbh->desc_h) ? (dvoid*)&fbh->desc_h : (dvoid*)fb_ary->abuf,
@@ -1360,11 +1458,17 @@
     /* that dbd_describe() executed sucessfuly so the memory buffers   */
     /* are allocated and bound.                                                
*/
     if ( !DBIc_ACTIVE(imp_sth) ) {
-       oci_error(sth, NULL, OCI_ERROR,
+       oci_error(sth, NULL, OCI_ERROR, imp_sth->nested_cursor ?
+           "nested cursor is defunct (parent row is no longer current)" :
            "no statement executing (perhaps you need to call execute first)");
        return Nullav;
     }

+    for(i=0; i < num_fields; ++i) {
+       imp_fbh_t *fbh = &imp_sth->fbh[i];
+       if (fbh->fetch_cleanup) fbh->fetch_cleanup(sth, fbh);
+    }
+
     if (ora_fetchtest && DBIc_ROW_COUNT(imp_sth)>0) {
        --ora_fetchtest; /* trick for testing performance */
        status = OCI_SUCCESS;
diff -r -u -N DBD-Oracle-1.16/t/55nested.t DBD-Oracle-1.16-nested/t/55nested.t
--- DBD-Oracle-1.16/t/55nested.t        1970-01-01 01:00:00.000000000 +0100
+++ DBD-Oracle-1.16-nested/t/55nested.t 2005-05-02 13:35:09.336523000 +0100
@@ -0,0 +1,61 @@
+#!perl -w
+
+sub ok ($$;$) {
+    my($n, $ok, $warn) = @_;
+    ++$t;
+    die "sequence error, expected $n but actually $t"
+    if $n and $n != $t;
+    ($ok) ? print "ok $t\n"
+         : print "# failed test $t at line ".(caller)[2]."\nnot ok $t\n";
+       if (!$ok && $warn) {
+               $warn = $DBI::errstr || "(DBI::errstr undefined)" if $warn eq 
'1';
+               warn "$warn\n";
+       }
+}
+
+use DBI;
+use DBD::Oracle qw(ORA_RSET);
+use strict;
+
+$| = 1;
+
+my $dbuser = $ENV{ORACLE_USERID} || 'scott/tiger';
+my $dbh = DBI->connect('dbi:Oracle:', $dbuser, '', { PrintError => 0 });
+
+unless ($dbh) {
+       warn "Unable to connect to Oracle as $dbuser ($DBI::errstr)\nTests 
skipped.\n";
+       print "1..0\n";
+       exit 0;
+}
+
+my $tests = 16;
+
+print "1..$tests\n";
+
+ok( 1,
+  my $outer = $dbh->prepare(q{
+    SELECT object_name, CURSOR(SELECT object_name FROM dual)
+    FROM all_objects WHERE rownum <= 5
+  })
+);
+ok( 2, $outer->{ora_types}[1] == ORA_RSET);
+ok( 3, $outer->execute);
+ok( 4, my @row1 = $outer->fetchrow_array);
+my $inner1 = $row1[1];
+ok( 5, ref $inner1 eq 'DBI::st');
+ok( 6, $inner1->{Active});
+ok( 7, my @row1_1 = $inner1->fetchrow_array);
+ok( 8, $row1[0] eq $row1_1[0]);
+ok( 9, $inner1->{Active});
+ok(10, my @row2 = $outer->fetchrow_array);
+ok(11, !$inner1->{Active});
+ok(12, !$inner1->fetch);
+ok(13, $dbh->err == -1);
+ok(14, $dbh->errstr =~ / defunct /);
+ok(15, $outer->finish);
+ok(16, $dbh->{ActiveKids} == 0);
+
+$dbh->disconnect;
+
+exit 0;
+



Reply via email to