Passing and using a List<string> as Oracle Binding Parameter - c#

I have a query ending with :
AND U2.USER_ID IN (:ToUserIDs)
My ToUserIDs is List<string> being created from entries in a TextBox.:
ToUserIDs = new List<string>(ToUserIDsTextBox.Text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries));
My OracleParameter is set up as follow:
OracleParameter oracleParameter3 = oracleDataAdapter.SelectCommand.Parameters.Add(":ToUserIDs", OracleDbType.NVarchar2);
oracleParameter3.Direction = ParameterDirection.Input;
oracleParameter3.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
oracleParameter3.Value = (List<string>)args["ToUserIDs"];
Here I am running into deep water as I am not sure who to handle Lists/Arrays in an OracleParameter.
I am getting this error:
Unable to cast object of type 'System.Collections.Generic.List`1[System.String]'
to type 'System.String[]

This wont work. The IN in clause only supports:
fixed length of scalars, like:
AND U2.USER_ID IN (:P1, :P2, :P3)
a subquery
AND U2.USER_ID IN (select x from y)
You can rewrite your query using TABLE cast keyword
AND U2.USER_ID IN (select * from TABLE(:P1))
Where P1 is parameter of type: array of varhar2

Used a post by #saminpa (Oracle Parameters with IN statement?) as solution.
AND U2.USER_ID IN (SELECT IDS.ToUserID FROM XMLTABLE('/ToUserIDs/ToUserID' PASSING XMLTYPE (:ToUserIDsElement) COLUMNS ToUserID VARCHAR(100) PATH '/ToUserID/.') IDS)

Related

ODP.Net - Calling stored procedure with custom type parameter throws ORA-06550/PLS-00306

I'm trying to call a stored procedure that takes a SYS_REFCURSOR output parameter and an input parameter with a custom type. Right now when I try to call the procedure from my application I get this error:
ORA-06550: line 1, column 7:
PLS-00306: wrong number or types of arguments in call to 'SP_TEST_01'
ORA-06550: line 1, column 7:
PL/SQL: Statement ignored
This is the PL/SQL script that creates the custom type and the stored procedure I'm trying to call:
CREATE OR REPLACE TYPE t_string_list AS TABLE OF VARCHAR2(4000);
/
CREATE OR REPLACE PACKAGE TEST_PACKAGE_01
AS
PROCEDURE SP_TEST_01(in_list IN t_string_list, out_cursor OUT SYS_REFCURSOR);
END TEST_PACKAGE_01;
/
CREATE OR REPLACE PACKAGE BODY TEST_PACKAGE_01
AS
PROCEDURE SP_TEST_01(in_list IN t_string_list, out_cursor OUT SYS_REFCURSOR)
IS
BEGIN
OPEN out_cursor FOR
SELECT st.*
FROM TABLE(in_list) t
JOIN SOME_TABLE st ON st.SOME_COLUMN = t.COLUMN_VALUE;
END SP_TEST_01;
END TEST_PACKAGE_01;
/
I've messed around with a variety of approaches and iterations on the C# side of things, but this is what I've come up with so far:
using (var context = new SomeDbContext())
{
using (var conn = new OracleConnection(context.Database.Connection.ConnectionString))
{
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = "TEST_PACKAGE_01.SP_TEST_01";
cmd.CommandType = CommandType.StoredProcedure;
cmd.ArrayBindCount = values.Count; // values is a List<string>
cmd.Parameters.Add(new OracleParameter
{
OracleDbType = OracleDbType.Varchar2,
Direction = ParameterDirection.Input,
CollectionType = OracleCollectionType.PLSQLAssociativeArray,
Value = values.ToArray(),
Size = values.Count
});
cmd.Parameters.Add(new OracleParameter()
{
OracleDbType = OracleDbType.RefCursor,
Direction = ParameterDirection.Output
});
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
// do stuff here
}
}
}
}
I was able to reproduce the error and I discovered that the [main] problem with passing the array parameter is in the type declaration of t_string_list. You need to make it an indexed array rather than an associative one by adding INDEX BY BINARY_INTEGER. In order to do that, you have to move the type definition into the package because that clause doesn't seem to be supported outside of a package.
CREATE OR REPLACE PACKAGE TEST_PACKAGE_01
AS
TYPE t_string_list IS TABLE OF VARCHAR2(4000) INDEX BY BINARY_INTEGER;
PROCEDURE SP_TEST_01(in_list IN t_string_list, out_cursor OUT SYS_REFCURSOR);
END TEST_PACKAGE_01;
This at least gets the parameter passed. Once executing the procedure, however, I got another error in querying the list as a table using the TABLE() operator.
ORA-21700: object does not exist or is marked for delete
The solution here says that assigning the list to a temporary variable somehow causes it to work. Why? Oracle, I guess. In all seriousness, though, it may be delay-loaded and assigning it to a temporary causes it to fully read the parameter data. That's just a working theory, though, and a better solution (with explanation) would be welcomed.
Regardless of why that workaround helped, this worked:
CREATE OR REPLACE PACKAGE BODY TEST_PACKAGE_01
AS
PROCEDURE SP_TEST_01(in_list IN t_string_list, out_cursor OUT SYS_REFCURSOR)
IS
temp_list t_string_list := in_list;
BEGIN
OPEN out_cursor FOR
SELECT t.COLUMN_VALUE
FROM TABLE(temp_list) t;
-- took out the join for testing
END SP_TEST_01;
END TEST_PACKAGE_01;
On the C# side, cmd.ArrayBindCount isn't what's advertised, apparently. I got a nasty error when I assigned it:
ORA-03137: malformed TTC packet from client rejected: ...
So I got rid of that before digging into the type and procedure definitions and that got me to the error you reported above. So as far as the parameteter itself, what you had was right.
cmd.Parameters.Add(new OracleParameter()
{
OracleDbType = OracleDbType.Varchar2,
Direction = ParameterDirection.Input,
CollectionType = OracleCollectionType.PLSQLAssociativeArray,
Value = values.ToArray()
});
The Count property is optional but if you assign it a value less than the number of elements you want to pass, it will only pass what you specify. Best to leave it unassigned.
For an output array, however, I'm guessing you would need to set the Count property to tell it the maximum number of elements on output and ArrayBindSize to specify the maximum size of each element.
Simply selecting the array elements into the cursor, as in my even-simpler version of your procedure, I was able to observe each element of the array in reader[0] within the while loop.

c# Oracle Table Output Parameter with returning statement

It looks like it's possible to have an Oracle command in C# which has an output parameter, however if it is I'm not sure how to wire it up.
The command:
declare
type new_rows is table of Table1%rowtype;
newRows new_rows;
type newKeys_rec is record (col1 number, col2 number);
type newKeys_type is table of newKeys_rec;
newKeys newKeys_type;
begin
select *
bulk collect into newRows
from Table2;
forall idx in 1..newRows.count()
insert into Table1
values newRows(idx)
returning Table1.col1, Table1.col2 bulk collect into newKeys;
end;
The command parameter in sql:
Parameters.Add(new OracleParameter
{
ParameterName = "newKeys",
ObjectTypeName = "newKeys_type",
OracleDbType = OracleDbType.Object,
Direction = ParameterDirection.Output
});
The error:
OCI-22303: type ""."NEWKEYS_TYPE" not found
UPDATE: Following upon the answers below:
1) Declare the type on the schema:
Create type Schema.newKeys_object as object (col1 number, Col2 number)
Create type Schema.newKeys_type as table of Schema.type1_object
2) In the OracleParameter:
Parameters.Add(new OracleParameter
{
ParameterName = "newKeys",
ObjectTypeName = "newKeys_type",
OracleDbType = OracleDbType.Table,
Direction = ParameterDirection.ReturnValue
});
In order for the PL/SQL types to be accessible from C# you need to define them as database types using the CREATE TYPE statement. See this Web page for more details on that DDL statement. Note also that a database type belongs to a schema and has access permissions just like a database table has, so when accessing the database type from C# code, you may need to prepend the schema name to the type name, as in...
SCOTT.NEWKEYS_TYPE

Add collection as SQL parameter to SqlCommand without using (Table-Valued-Parameter) type

I'm trying to use a table as a parameter in my SQL query, but I cannot get it to work without using the name of an existing type (Table Valued Parameter).
In MSSQL I can do this:
declare #mytable table (column1 int, column2 nvarchar(10))
insert into #mytable select 1, "test" UNION ALL select 2, "test2" UNION ALL [...]
select * from sometable inner join #mytable on sometable.id = #mytable.column1
To accomplish this in C#, I can (roughly) do the following:
SqlCommand cmd ...
var p = cmd.CreateParameter();
p.TypeName = "MyType"; // <-- I dont't want to do this
p.SqlDbType = SqlDbType.Structured;
p.Value = myDataTable;
p.ParameterName = "table";
cmd.Parameters.Add(p);
cmd.CommandText = "select * from sometable inner join #mytable on ... ";
For this to work, I have to create the type "MyType" in the database: CREATE TYPE [MyType] AS TABLE (...);
My problem is that I need to explicitly specify the type of the table, even though in MSSQL I can do it inline (see previous example). Secondly, I need to explicitly define the type in the database for each possible collection type.
Is there any way to add a collection as a parameter to the SQL command without needing to declare its type in the database and using that type as the type name of the parameter?
I ended up using Json (as proposed by #DanGuzman), tested it, and it performed more than well with a relatively simple dataset of about 500 items (my use case).
Simple implementation example in C#:
var col = new object[] { new { Column1 = "Test", Column2 = 1 }, new { Column1 = "2nd row", Column2 = 2 }, ... };
IDbDataParameter p = command.CreateParameter();
p.DbType = DbType.String;
p.ParameterName = "mytable";
p.Value = JsonConvert.SerializeObject(col); // NewtonSoft
command.Parameters.Add(p);
command.CommandText = "SELECT * FROM OPENJSON(#mytable) WITH (Column1 nvarchar(max), Column2 int)";
References:
OPENJSON
JsonConvert.SerializeObject(..)

Passing in Oracle Parameter to SQL string

I'm having a problem where I don't know how I'm supposed to pass in an Oracle parameter where the C# type is a string and the Oracle type is a Varchar2.
Currently I'm passing in this string as CMS','ABC thinking that Oracle will add in the '' that surround this string making it a varchar2 that looks like 'CMS','ABC'.
This works for a single string like CMS but when the value is something longer, like something typically in a IN (list) command the parameter won't be passed in correctly.
This is the code I'm referring too.
string sql = 'SELECT name FROM Pers p WHERE p.FirstName IN (:names)';
The below works when the value of :names being passed in is CML without any quotes.
OracleParameter param = new OracleParameter(":names", OracleDbType.Varchar2, "CML", ParameterDirection.Input);
Below doesn't work when the value of :names being passed in is CML','ABC with quotes on the inside.
OracleParameter param = new OracleParameter(":names", OracleDbType.Varchar2, "CML','ABC", ParameterDirection.Input);
Why is that?
Does Oracle add in single quotes around the parameter when it's passed into the sql statement? Why doesn't it add quotes around the second case?
ODP.NET parameters do not work with multiple, comma separated values. Each parameter is treated as a single value, whatever kind of quotes it contains.
Oracle does not add quotes around parameter values when passed to a query. Quotes are just a way to write a VARCHAR value in a query, but when using parameters, Oracle doesn't "replace your parameter with its value then execute the query", as this would allow SQL injection.
If that was the case, imagine your parameter value was: "CML', 'ABC');DROP DATABASE Test;--". Oracle would then execute SELECT name FROM Pers p WHERE p.FirstName IN ('CML', 'ABC');DROP DATABASE Test;--'!
See this question for ideas on how to solve your problem: Oracle Parameters with IN statement?
From your comments/answers I was able to come up with this solution. I hope it helps others who come.
To get around ODT.NET parameters not working with multiple comma separated values you can divide each value into its own parameter. Like the following.
string allParams = "CML, ABC, DEF";
string formattedParams = allParams.Replace(" ", string.Empty); // Or a custom format
string [] splitParams = formattedParams.Split(',');
List<OracleParamter> parameters = new List<OracleParameter>();
string sql = #"SELECT * FROM FooTable WHERE FooValue IN (";
for(int i = 0; i < splitParams.Length; i++)
{
sql += #":FooParam" + i + ",";
parameters.Add(new OracleParameter(":FooParam" + i, OracleDbType.Varchar2, splitParams[i], ParameterDirection.Input));
{
sql = sql.Substring(0, (sql.Length - 1));
sql += ')';
The string sql will now have this as it's value: SELECT * FROM FooTable WHERE FooValue IN (:FooParam0,:fooParam1, etc...)
This will solve the problem.
Another approach would be to add in a bunch of OR clauses for each parameter. The above example is better since you don't write a bunch of OR clauses though.

Get parameters for a ADO.NET CommandText

Is there any way (maybe even in ADO.NET) to get a list of the parameters of a CommandText?
That is to say, given the following query:
INSERT INTO TABLE test VALUES(#value1, #value2)
Is there any way to know that the query parameters are value1, value2? Or should I have to parse the query on my own to find them (of course '#' syntax may change from one db engine to other...)?
Clarification:
I want to know the parameters name and the order in the query before having to fill the command parameters dictionary.
You can use SqlParameterCollection.Count property to get that information.
So you can do like
SqlCommand cmd = new SqlCommand("insert into table test values(#value1, #value2)", con);
cmd.Parameters.Add("#value1", SqlDbType.VarChar).Value = "test";
cmd.Parameters.Add("#value2", SqlDbType.VarChar).Value = "test";
int count = cmd.Parameters.Count; // you get the parameter count
You as well get the name of parameter using the parameter collection like
string p1 = cmd.Parameters[0].ParameterName;
string p2 = cmd.Parameters[1].ParameterName;
Not exactly sure what you mean by parameters order in the query
If the query is a stored procedure, you can use SqlCommandBuilder.DeriveParameters (see How can I retrieve a list of parameters from a stored procedure in SQL Server). However that unfortunately only works for CommandType=StoredProcedure
In SQL Server 2012 (and above) you can use sp_describe_undeclared_parameters (see https://msdn.microsoft.com/en-us/library/ff878260.aspx)
Doesn't seem like there is any alternative for lower versions other than parsing the SQL query text directly (regex, or possibly using the SQLDOM that ships with SSDT (or via nuget): https://blogs.msdn.microsoft.com/arvindsh/2013/04/04/using-the-transactsql-scriptdom-parser-to-get-statement-counts/ )
Edit: simple F# example of finding parameters using the Microsoft.SqlServer.TransactSql.ScriptDom nuget:
#r "Microsoft.SqlServer.TransactSql.ScriptDom"
open System.IO
open System.Collections.Generic
open Microsoft.SqlServer.TransactSql.ScriptDom
let query = #"select * from dbo.Customers where FirstName=#FirstName"
let parser = new TSql120Parser(true)
let errors : ref<IList<ParseError>> = ref(null)
let frag = parser.Parse(new StringReader(query), errors) // the actual work
frag.ScriptTokenStream
|> Seq.filter (fun f -> f.TokenType = TSqlTokenType.Variable)
|> Seq.iter (fun t -> (printfn "Type = %A; Name = %s" t.TokenType t.Text))
outputs
Type = Variable; Name = #FirstName
...however if the script involves temporary variables you can easily get false positives, eg:
declare #temp nvarchar(50) = #FirstName
select * from dbo.Customers where FirstName=#temp
...finds #temp twice, and #FirstName

Categories