Postgres Stored Procedure from C# - c#

I have a Postgres database with a stored procedure that returns JSON documents, based on the article here: http://www.sqlines.com/postgresql/npgsql_cs_result_sets
The procedure is represented like this:
-- Procedure that returns a single result set (cursor)
CREATE OR REPLACE FUNCTION get_data_test() RETURNS refcursor AS $$
DECLARE
ref refcursor; -- Declare a cursor variable
BEGIN
OPEN ref FOR -- Open a cursor
SELECT row_to_json(r) AS data
FROM
(
SELECT *
FROM data AS d
) r;
RETURN ref; -- Return the cursor to the caller
END;
$$ LANGUAGE plpgsql;
I am then running the following code from a .net console app:
// Making connection with Npgsql provider
using (NpgsqlConnection conn = new NpgsqlConnection(connstring))
{
conn.Open();
var trans = conn.BeginTransaction();
var cmd = new NpgsqlCommand("get_data_test", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Transaction = trans;
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
Trace.WriteLine(reader[0]);
}
}
The output is a single instance wit the name of the refcursor, rather than the actual data:
< unnamed portal 1 >
If I run the select query directly in the command text, the result set is returned as expected. I've also tried explicitly calling the proc via text using "SELECT get_data_test()" but this also fails with just the cursor name.
I don't believe I am missing a step and this refcursor should be returned unbundled. What am I doing wrong?

As it happens, the tutorial was wrong:
Nice GitHub bug report: https://github.com/npgsql/npgsql/issues/1777
Correct answer:
-- Procedure that returns a single result set (cursor)
CREATE OR REPLACE FUNCTION get_data_test() RETURNS TABLE (data JSON) AS $$
BEGIN
RETURN query
SELECT row_to_json(r) AS data
FROM
(
SELECT *
FROM data AS d
) r;
END;
$$ LANGUAGE plpgsql;

Related

Npgsql fetching records from Stored Procedures

I have tried to execute the codes below to search for a username
using (var cmd = new NpgsqlCommand("search_users", conn))
{
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#username", NpgsqlTypes.NpgsqlDbType.Text, "xxx");
using (var reader = cmd.ExecuteReader())
while (reader.Read())
x.Add(reader["loginname"].ToString());
}
On the line of cmd.ExecuteReader() I'm getting this error
Npgsql.PostgresException: '42601: a column definition list is
required for functions returning "record"'
Am I doing the right way to read the returned records ? This is the stored procedure
CREATE FUNCTION search_users(username TEXT) RETURNS RECORD AS $$
DECLARE
ret RECORD;
BEGIN
SELECT * from public."Users" as t1
WHERE t1."LoginName" = username INTO ret;
RETURN ret;
END;$$ LANGUAGE plpgsql;
Any help is greatly appreciated.
If you want to return a record that matches the layout of public."Users" then I think all you need to do is specify that in the return type:
CREATE FUNCTION search_users(username TEXT) RETURNS public."Users" AS $$
DECLARE
ret RECORD;
BEGIN
SELECT * from public."Users" as t1
WHERE t1."LoginName" = username INTO ret;
RETURN ret;
END;$$ LANGUAGE plpgsql;
As far as the C# code... I see what you are trying to do, and I think if this were Oracle that approach would work perfectly. In PostgreSQL, the line between a function and a stored procedure is a little blurred (in my opinion), and the easiest way to execute the function and return the results is with a simple select statement. As such, your C# would look as follows:
using (var cmd = new NpgsqlCommand("select * from search_users(:USER_NAME)", conn))
{
cmd.Parameters.AddWithValue("USER_NAME", "xxx");
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
x.Add(reader["loginname"].ToString());
}
}
The use of the # versus the : totally doesn't matter... I only switched to the colon for personal style preferences.

PL/SQL - Oracle: Stored procedure returning dataset from various tables

What I need:
In PLS/SQL on an Oracle DB, create a stored procedure or function with parameters, which given a declared table of , where is a ROW of a table (with all the fields), returns the resultset following the conditions given in the parameters. After, I need to call them from Microsoft Entity Framework with edmx file.
Basically the need is to being able to provide a quick report of the table contents into a pdf, matching some filters, with an oracle db.
The mantainer must be able, provided a script I give, to create and add new reports, so this needs to be dynamic.
Here's what I've got so far:
CREATE OR REPLACE type THETABLEIWANTTYPE as table of THETABLEIWANT%TYPE
create function
SCHEMA.THETABLEIWANT_FUNCTION(PARAM_GR in number default 1)
return THETABLEIWANTTYPE
PIPELINED
as
result_table THETABLEIWANTTYPE
begin
SELECT S.id, S.idg, S.sta, S.tab
Bulk collect into result_table
from SCHEMA.THETABLEIWANT S
WHERE IDGR = PARAM_GR
IF result_table.count > 0 THEN
for i in result_table.FIRST .. result_table.LAST loop
pipe row (result_table(i))
end loop
end if
return
end;
But it's not working. It gives errors.
Running CREATE TYPE I get:
Compilation errors for TYPE SCHEMA.THETABLEIWANT
Error: PLS-00329: schema-level type has illegal reference to
SCHEMA.THETABLEIWANT
The mantainer will launch the script creating a TYPE of the row of the table I need, then the function should return a table with the records.
Then calling it from Entity Framework I should be able to execute it like I'm calling a normal select from my table, IE:
``_dbContext.THETABLEIWANT.Where(x => x.IDGR = Param_gr).ToList();
The problem is that mantainers should be able to generate new kind of reports with any select inside without the need of my intervention on the software code.
Any hint?
It's ok also to bulk all the select result into a temp table but it has to be dynamic as column will be changing
I ended up to write a PLS/SQL procedure that returns a cursor and managing it from C# code with Oracle.ManagedDataAccess Library.
Here's the procedure, for anyone interested:
CREATE OR REPLACE PROCEDURE SCHEMA.PROC_NAME(
PARAM_1 VARCHAR2,
RESULT OUT SYS_REFCURSOR)
IS
BEGIN
OPEN RESULT FOR
SELECT A, V, C AS MY_ALIAS from SCHEMA.TABLE WHERE FIELD = PARAM_1 AND FIELD_2 = 'X';
END;
And here's the C# code for calling and getting the result:
OracleConnection conn = new OracleConnection("CONNECTIONSTRING");
try
{
if (conn.State != ConnectionState.Open)
conn.Open();
List<OracleParameter> parametri = new List<OracleParameter>()
{
new OracleParameter
{
ParameterName = nameof(filter.PARAM_1),
Direction = ParameterDirection.Input,
OracleDbType = OracleDbType.NVarchar2,
Value = filter.PARAM_1
}
};
OracleCommand cmd = conn.CreateCommand();
cmd.Parameters.AddRange(parametri.ToArray());
OracleParameter cursor = cmd.Parameters.Add(
new OracleParameter
{
ParameterName = "RESULT",
Direction = ParameterDirection.Output,
OracleDbType = OracleDbType.RefCursor
}
);
cmd.CommandText = procedureName;
cmd.CommandType = CommandType.StoredProcedure;
cmd.ExecuteNonQuery();
using (OracleDataReader reader = ((OracleRefCursor)cursor.Value).GetDataReader())
{
if (reader.HasRows)
while (reader.Read())
{
//Iterate the result set
}
}
}
catch(Exception ex)
{
//Manage exception
}

One function for multiple stored procedures C#

I have a function that executes stored procedures. The thing is, I wish to use this one function for multiple stored procedures that take different arguments.
Of course, if I do so, I will get an error saying that
Procedure or function has too many arguments specified
What I want to do is to create the function such that when it executes a stored procedure, that stored procedure only takes the parameters that it needs and ignore the others. Is this possible?
Here is what I have so far:
try
{
using (SqlConnection con = new SqlConnection(consr))
{
SqlCommand cmd = new SqlCommand();
SqlDataReader reader;
cmd.CommandText = stp;
cmd.Parameters.Add(new SqlParameter("#proc1", cmb1.SelectedItem.ToString()));
cmd.Parameters.Add(new SqlParameter("#proc2", cmb2.SelectedItem.ToString()));
cmd.Parameters.Add(new SqlParameter("#proc3", cmb3.SelectedItem.ToString()));
cmd.CommandType = CommandType.StoredProcedure;
cmd.Connection = con;
con.Open();
reader = cmd.ExecuteReader();
con.Close();
}
}
catch (SqlException exp)
{
throw new InvalidOperationException(exp.Message);
}
Here are two of the procedures:
ALTER PROCEDURE [dbo].[test1]
#proc1 varchar(20)
AS
Begin
select * from tab where name=#proc1
END
and
ALTER PROCEDURE [dbo].[test1]
#proc2 varchar(20)
AS
Begin
select * from tab where name=#proc2
END
I want to use the same function to execute both
You can set the params you don't want to use to null, then check for which values are available and respond accordingly
ALTER PROCEDURE [dbo].[test1]
#proc1 varchar(20) = null
#proc2 varchar(20) = null
#proc3 varchar(20) = null
AS
Begin
IF #proc1 IS NOT NULL BEGIN
select * from tab where name=#proc1
END
END
Make your function take two arguments, the name of the SP and a 2d array of SP argument names and their corresponding values. Then set up your sql command by iterating over the 2d array and adding the parameters in the loop.
If you need to return several result sets, see here: How do I return multiple result sets with SqlCommand? and modify the function and args so that they take an array of SP names and an array of 2d arrays, containing the SP arg names and values for each SP.
I think you should use this structure :
CREATE PROCEDURE MyProcName
#Parameter1 INT = 1,
#Parameter2 VARCHAR (100) = 'StringValue',
#Parameter3 VARCHAR (100) = NULL
AS
/* check for the NULL / default value (indicating nothing was passed */
if (#Parameter3 IS NULL)
BEGIN
/* whatever code you desire for a missing parameter*/
INSERT INTO ........
END
/* and use it in the query as so*/
SELECT *
FROM Table
WHERE Column = #Parameter
Can you please share the stored procedure code? I think you need to use if else or case statement in your function and call appropriate stored procedure with required parameters inside your function.

How to do ExecuteScalar Function with Npgsql and PostgreSQL?

I am learning Npgsql and PostgrSQL. I am unable to get this simple test to work. Here is my function:
CREATE OR REPLACE FUNCTION count_customers(_customerid integer DEFAULT NULL::integer)
RETURNS void AS
$BODY$
BEGIN
SELECT COUNT(*) FROM Customers
WHERE CustomerId = _customerid or _customerid is null;
END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Here is my C# code:
[Test]
public void ExecuteScalarTest()
{
NpgsqlConnection conn = new NpgsqlConnection("Host=localhost; Database=postgres; User ID=postgres; Password=password");
conn.Open();
IDbCommand command = conn.CreateCommand();
command.CommandText = "count_customers";
command.CommandType = CommandType.StoredProcedure;
object result = command.ExecuteScalar();
conn.Close();
Console.WriteLine(result);
}
I keep getting the error below.
Npgsql.NpgsqlException : ERROR: 42601: query has no destination for result data
This is nothing to do with nPgSQL. Your problem is in your stored function.
You've written a trivial wrapper in PL/PgSQL, but you haven't used RETURN. You can't use SELECT in PL/PgSQL except when its output goes to a variable (via SELECT INTO or as a subquery like x := (SELECT ...) or to the RETURN QUERY statement.
You should write:
BEGIN
RETURN QUERY
SELECT COUNT(*) FROM Customers
WHERE CustomerId = _customerid
OR _customerid is null;
END
and define your procedure as RETURNS bigint, since obviously you cannot get a value from the function if it returns void. Also, this function is STABLE not VOLATILE. If you aren't sure, say nothing. The same is true for COST - unless you have a good reason, leave it out.
This is still overcomplicated though. You can use a simple sql function for calls like this, e.g.
CREATE OR REPLACE FUNCTION count_customers(_customerid integer DEFAULT NULL::integer)
RETURNS bigint LANGUAGE sql STABLE AS
$BODY$
SELECT COUNT(*) FROM Customers
WHERE CustomerId = $1 OR $1 is null;
$BODY$;

Returning a recordset from Oracle to .Net using an Oracle function that takes arguments

I currently have the following funciton in an oracle database that returns a concatenated string seperated by pipes. It's a legacy application that is being updated to use .net 3.5. The exiisting application concatenates the returned result set into a VARCHAR2 data type. What I want to do is return the entire result set back to my .net client. The MS SQL equivalent of what I'm trying to accomplish is a simple "SELECT * FROM TBL WHERE id = #id" I'm not use to some of the concepts Oracle uses. I't seems like e blend of OOP and SQL querying. I've read multiple examples on this but can't seem to find exactly what I'm looking for. Can you please help?
CREATE OR REPLACE FUNCTION DOCSADMIN.GET_DOCS (
RECID IN NUMBER) -- RECORD ID
RETURN VARCHAR2 -- CONCATENATED STRING WITH PIPES
IS
RETVAL VARCHAR2(5000) :='';
DOCSTRING VARCHAR2(5000) :='';
DOCNAME VARCHAR2(5000) :='';
DOCNUMBER NUMBER;
STATUS VARCHAR2(5000) :='';
DOCTYPE VARCHAR2(5000) :='';
EDITDATE DATE :='';
/******************************************************************************
NAME: GET_DOCS
PURPOSE: Pulls associated docs from profile table
******************************************************************************/
CURSOR GETDOCINFO IS SELECT DOCNUMBER, DOCNAME, CUSTOM_STATUS, DOCUMENTTYPES.DESCRIPTION, LAST_EDIT_TIME
FROM PROFILE, DOCUMENTTYPES, FORMS WHERE NAD_APID = IN_APID AND PROFILE.FORM = FORMS.SYSTEM_ID AND
DOCUMENTTYPE = DOCUMENTTYPES.SYSTEM_ID AND FORM_NAME = 'DOCS_PROFILE' ORDER BY DOCNUMBER;
BEGIN
OPEN GETDOCINFO;
--GET THE FIRST RECORD
FETCH GETDOCINFO INTO DOCNUMBER, DOCNAME, STATUS, DOCTYPE, EDITDATE;
--LOOP THROUGH ALL ASSOCIATED DOCS AND GRAB INFO
WHILE GETDOCINFO%FOUND LOOP
BEGIN
DOCSTRING := DOCNUMBER || '|~|' || DOCNAME || '|~|' || STATUS || '|~|' || DOCTYPE || '|~|' || WS_EDITDATE;
RETVAL := RETVAL || DOCSTRING || '|^|';
GOTO STARTOVER;
END;
<<STARTOVER>>
FETCH GETDOCINFO INTO DOCNUMBER, DOCNAME, STATUS, DOCTYPE, EDITDATE;
END LOOP;
CLOSE GETDOCINFO;
RETURN RETVAL;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
WHEN OTHERS THEN
-- Consider logging
the error and then re-raise
RAISE;
END GET_DOCS;
/
Well, you could convert the function in to a procedure and have an OUT parameter of the SYS_REFCURSOR type. With Oracle and .Net you can pass back a cursor and iterate through that as a reader.
Sample Oracle procedure:
CREATE OR REPLACE PROCEDURE TEST_SCHEMA.TEST_PROCEDURE (
out_DATA OUT SYS_REFCURSOR;
) AS
BEGIN
OPEN out_DATA FOR
SELECT col1,
col2
FROM TEST_SCHEMA.TEST_TABLE;
END test_procedure;
Sample .Net end:
using (OracleConnection connection = new OracleConnection("connstring"))
using (OracleCommand command = connection.CreateCommand()) {
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "TEST_SCHEMA.TEST_PROCEDURE";
command.Parameters.Add("out_DATA", OracleType.Cursor)
.Direction = ParameterDirection.Output;
connection.Open();
command.ExecuteNonQuery();
OracleDataReader reader =
command.Parameters["out_DATA"].Value as OracleDataReader;
if (reader != null) {
using (reader) {
while(reader.Read()) {
string col1 = reader["col1"] as string;
string col2 = reader["col2"] as string;
}
}
}
}
Be sure to close the cursor after you're done using it (accomplished above by the using (reader) statement).
So in your case, you could probably create a procedure that outputs the original cursor in your function, then just iterate over the cursor in .Net as listed above. Just a note, the column names from the Oracle side are important and will match what you're using in .Net.
What I have so far compiles fine.
CREATE OR REPLACE PROCEDURE DOCSADMIN.GET_DOCS_SP ( IN_APID IN NUMBER, out_DATA OUT SYS_REFCURSOR )
AS
BEGIN
OPEN out_DATA FOR
SELECT DOCNUMBER, DOCNAME, CUSTOM_STATUS, DOCUMENTTYPES.DESCRIPTION, LAST_EDIT_TIME
FROM PROFILE, DOCUMENTTYPES, FORMS WHERE APID = IN_APID AND PROFILE.FORM = FORMS.SYSTEM_ID AND
DOCUMENTTYPE = DOCUMENTTYPES.SYSTEM_ID AND FORM_NAME = 'PROFILE' ORDER BY DOCNUMBER;
END GET_DOCS_SP;
/
However, I've run into another situation and would appreciate your input. If I wanted to call the following from a sql database using OPENQUERY how would I do so? The legacy version that was returning the concatenated string looked like the following.
SELECT * FROM OPENQUERY (TESTSERVER, 'SELECT DOCSADMIN.GET_DOCS_SP (26) AS DOCINFO FROM DUAL')
Do I just remove the as DOCINFO FROM DUAL clause?
Thanks

Categories