5

I need help understanding if the way I'm trying to use a Ref Cursor as a ReturnValue Parameter for multiple records/values, with the PL/SQL just being the CommandText of an OracleCommand object and not in a Stored Procedure or Function, is even possible.

If that is not possible, what I'm trying to do is find a way to issue a PL/SQL statement that will Update an unknown number of records (depends on how many match the WHERE clause), and return the Ids of all the records Updated in an OracleDataReader, using a single round-trip to the database, without the use of a Stored Procedure or Function.

I'm working with Oracle 11g using ODP.NET for communication with an existing C# .NET 4.0 code-base that uses the SQL connection to retrieve/modify data. The simplified test table definition I'm using looks like so:

CREATE TABLE WorkerStatus
(
    Id                  NUMERIC(38)         NOT NULL
    ,StateId            NUMERIC(38)         NOT NULL
    ,StateReasonId      NUMERIC(38)         NOT NULL
    ,CONSTRAINT PK_WorkerStatus PRIMARY KEY ( Id )
)

I pre-populate the table with three test values like so:

BEGIN
    EXECUTE IMMEDIATE 'INSERT INTO WorkerStatus (Id, StateId, StateReasonId)
                        VALUES (1, 0, 0)';
    EXECUTE IMMEDIATE 'INSERT INTO WorkerStatus (Id, StateId, StateReasonId)
                        VALUES (2, 0, 0)';
    EXECUTE IMMEDIATE 'INSERT INTO WorkerStatus (Id, StateId, StateReasonId)
                        VALUES (3, 0, 0)';
END;

The existing SQL statement, loaded from a script file named Oracle_UpdateWorkerStatus2, and contained in the OracleCommand.CommandText looks like so:

DECLARE
    TYPE id_array IS TABLE OF WorkerStatus.Id%TYPE INDEX BY PLS_INTEGER;    

    t_ids   id_array;
BEGIN
    UPDATE WorkerStatus
    SET
         StateId = :StateId
        ,StateReasonId = :StateReasonId
    WHERE
        StateId = :CurrentStateId
    RETURNING Id BULK COLLECT INTO t_Ids;
    SELECT Id FROM t_Ids;
END;

I've created a small C# test program to attempt to isolate where I'm getting an ORA-01036 "illegal variable name/number" error that has a main body that looks like so:

using System;
using System.Configuration;
using System.Data;
using System.Text;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;
namespace OracleDbTest
{
  class Program
  {
    static void Main(string[] args)
    {
        // Load the SQL command from the script file.
        StringBuilder sql = new StringBuilder();
        sql.Append(Properties.Resources.Oracle_UpdateWorkerStatus2);

        // Build and excute the command.
        OracleConnection cn = new OracleConnection(ConfigurationManager.ConnectionStrings["OracleSystemConnection"].ConnectionString);
        using (OracleCommand cmd = new OracleCommand(sql.ToString(), cn))
        {
            cmd.BindByName = true;
            cn.Open();

            OracleParameter UpdatedRecords  = new OracleParameter();
            UpdatedRecords.OracleDbType     = OracleDbType.RefCursor;
            UpdatedRecords.Direction        = ParameterDirection.ReturnValue;
            UpdatedRecords.ParameterName    = "rcursor";

            OracleParameter StateId         = new OracleParameter();
            StateId.OracleDbType            = OracleDbType.Int32;
            StateId.Value                   = 1;
            StateId.ParameterName           = "StateId";

            OracleParameter StateReasonId   = new OracleParameter();
            StateReasonId.OracleDbType      = OracleDbType.Int32;
            StateReasonId.Value             = 1;
            StateReasonId.ParameterName     = "StateReasonId";

            OracleParameter CurrentStateId  = new OracleParameter();
            CurrentStateId.OracleDbType     = OracleDbType.Int32;
            CurrentStateId.Value            = 0;
            CurrentStateId.ParameterName    = "CurrentStateId";

            cmd.Parameters.Add(UpdatedRecords);
            cmd.Parameters.Add(StateId);
            cmd.Parameters.Add(StateReasonId);
            cmd.Parameters.Add(CurrentStateId);

            try
            {
                cmd.ExecuteNonQuery();
                OracleDataReader dr = ((OracleRefCursor)UpdatedRecords.Value).GetDataReader();
                while (dr.Read())
                {
                    Console.WriteLine("{0} affected.", dr.GetValue(0));
                }
                dr.Close();
            }
            catch (OracleException e)
            {
                foreach (OracleError err in e.Errors)
                {
                    Console.WriteLine("Message:\n{0}\nSource:\n{1}\n", err.Message, err.Source);
                    System.Diagnostics.Debug.WriteLine("Message:\n{0}\nSource:\n{1}\n", err.Message, err.Source);
                }
            }
            cn.Close();
        }
        Console.WriteLine("Press Any Key To Exit.\n");
        Console.ReadKey(false);
    }
  }
}

I've tried changing the parameter names, naming and not-naming the UpdatedRecords parameter, changing the order so the UpdatedRecords is first or last. The closest thing I've found so far is the following StackOverflow question (How to call an Oracle function with a Ref Cursor as Out-parameter from C#?), but that still uses a Stored Function as far as I can tell.

Running the Oracle_UpdateWorkerStatus2 PL/SQL script from SQL Developer, it opens the "Enter Binds" dialog where I enter the values for CurentStateId, StateId and StateReasonId as in the code above, but it gives the following error report:

Error report:
ORA-06550: line 13, column 17:
PL/SQL: ORA-00942: table or view does not exist
ORA-06550: line 13, column 2:
PL/SQL: SQL Statement ignored
06550. 00000 -  "line %s, column %s:\n%s"
*Cause:    Usually a PL/SQL compilation error.
*Action:

I don't really understand why it's telling me the table doesn't exist, when I've defined the WorkerStatus table, and declared the t_Ids variable, of type id_array, to be a table as well. Any help here is greatly appreciated.

Community
  • 1
  • 1
Paul
  • 387
  • 1
  • 6
  • 11
  • Could this be a privilege/visibility issue? When you manually connect to the oracle account you were executing your script "Oracle_UpdateWorkerStatus2" in, can you select from the table workerstatus? – Juergen Hartelt Jun 30 '12 at 13:39
  • I am able to select from the WorkerStatus table, as well as drop and create it. I'm fairly new to Oracle, so I don't know if there are any special privileges I would need to be able to use ref cursors, but our I.T./DBA technician set me up with all the privileges normally used by our developers, so I don't believe this is a privilege issue. I will ask him on Monday though. – Paul Jun 30 '12 at 16:34
  • Checked with our DBA, and he is not aware of any special privileges required to do what I am trying. Also, he took a quick scan of what I've got here, and doesn't see any reason why it is not working. Any thoughts on what I might be doing wrong, or another way to achieve the goals? – Paul Jul 02 '12 at 18:13
  • Sorry Paul, I was mislead by the C# code. So you were already executing your script from SQL-Developer, not calling it from some application code. Looking at that script, I would now expect the last "select" to cause an exception, because it does not contain an "into" clause. Inside PL/SQL you cannot just do a "select". The result set needs to be placed into a variable. You either need to use a stored function to pass the data via its return value, or you need to execute pure SQL. Wouldn't the update statement be exactly what you want? "UPDATE WorkerStatus ... RETURNING Id;". No "into" clause. – Juergen Hartelt Jul 02 '12 at 18:48
  • Thank you Juergen. I tried running the SQL, via ODP.NET, from a C# Console Application first, when that failed, I tried running just the SQL from SQL Developer. As far as I know, with ODP.NET, the only way to return multiple records from an Update to an application are via a Ref Cursor or Array. With the Array, it seems you have to know the record count up front, which won't work for me. I don't know of a way to direct the Returning clause to return to a Ref Cursor. So I don't believe just utilizing the Returning clause can work with the application. – Paul Jul 02 '12 at 19:04
  • Hi Juergen, Based on the info you gave about PL/SQL and Select Into, I had a thought, maybe I can just Select into a Ref Cursor output bind variable/parameter. I'll give that a go, and report back, when I get back to my desk. – Paul Jul 02 '12 at 19:06
  • I tried changing the last line of the SQL to: 'OPEN :ReturnValue FOR SELECT Id FROM t_Ids;' but that didn't work. So I switched it to using a TestTable that I created (the TestTable only has one column, Id), and changed the last two lines of the SQL to: 'RETURNING Id BULK COLLECT INTO TestTable; OPEN :ReturnValue FOR SELECT Id FROM TestTable;' but that didn't work either. For now, I'm going to have to go to either a quick and dirty solution that will suffice for the moment, so I can move on, or a two-pass solution that may allow two concurrent operations to create a conflict in the data. – Paul Jul 02 '12 at 23:28

1 Answers1

5

I will try an answer instead of another comment.

As I said in one comment, a pure/simple select-statement does not work in PL/SQL. But I was wrong in stating, that you need a stored function to return a ref cursor.

But first things first: The type "id_array" you declare in your PL/SQL-block is a PL/SQL type. It cannot be used in a ref cursor select statement. Instead you will need a SQL type:

create type id_array as table of number;

This needs to be executed only once, just like a "create table".

Your PL/SQL-block could then look like this:

DECLARE
    t_ids   id_array;
BEGIN
    UPDATE WorkerStatus
    SET
         StateId = :StateId
        ,StateReasonId = :StateReasonId
    WHERE
        StateId = :CurrentStateId
    RETURNING Id BULK COLLECT INTO t_Ids;

    OPEN :rcursor FOR SELECT * FROM TABLE(cast(t_Ids as id_array));    
END;

PS:
While assembling this post, I realized where the ORA-00942 might come from. The array t_ids was based on a PL/SQL type, which is not known/available on the SQL side.

Juergen Hartelt
  • 664
  • 4
  • 5
  • Btw, for anyone who looks at this later, I've edited the C# code above to work with :rcursor, instead of :ReturnValue. To get the example working place the C# code into a new Console Application, replace my PL/SQL from the Oracle_UpdateWorkerStatus2 script file with Juergen's PL/SQL block. Execute the CREATE TYPE once as Juergen mentions. Then run the C# Console Application, and it should list the 3 ids of the records as affected. – Paul Jul 03 '12 at 17:26