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$;
Related
I'm building a WinForms project in C# using a PostgreSQL database and the Npgsql framework.
For inserting a record, I need to return the ID of the new record. This SO question says to add SELECT SCOPE_IDENTITY() to the query string passed to cmd. So my query string looks like this:
string insertString = "INSERT INTO sometable (company_name, category, old_value, old_desc, new_value, new_desc, reference1, reference2) VALUES (#comp, #cat, #oldValue, #oldDesc, #newValue, #newDesc, #ref1, #ref2); SELECT SCOPE_IDENTITY();";
and then get the ID with something like
int modified = cmd.ExecuteNonQuery();
But that's likely SQL Server-specific. If I use that method, I get an exception at the above line saying, "fuction scope_identity() does not exist".
I wasn't able to find anything that seemed to address this on the Npgsql documentation.
Per the linked SO question and Denis' suggestions I've tried adding both
RETURNING id;
and
CURRVAL(pg_get_serial_sequence('my_tbl_name','id_col_name'))
to the query string, replacing SELECT SCOPE_IDENTITY(); with those statements in the code above. In both cases they work as intended in DBeaver on an insert, but in my C# code in my WinForm project, modified was set to "1".
NOTE: I re-titled the question and added more information about what I've done.
Add "returning idcolumn" to the end of the sql query, then run the command with the ExecuteScalar() method instead of ExecuteNonQuery(). It should return with an int.
string insert = "insert into table1 (col1) values (something) returning idcol";
int id = cmd.ExecuteScalar();
All the comments above were almost nearly spot on and got me to a solution but didn't exactly wrap it in a bow -- so I thought i'd post my implementation that works (with silly fake example tables of course).
private int? InsertNameIntoNamesTable(string name)
{
int? id = null;
using (var dbcon = new NpgsqlConnection(_connectionString))
{
dbcon.Open();
StringBuilder sb = new StringBuilder();
var sql = $#"
insert into names_table
(name)
values
({name})
returning id;
";
sb.Append(sql);
using (var cmd = new NpgsqlCommand(sql, dbcon))
{
id = (int)cmd.ExecuteScalar();
}
dbcon.Close();
}
return id;
}
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;
I'm getting an error "Invalid operation on null data" when my C# code is calling Oracle Function. This happens only if no data is found. If data is found and function returns a value, then everything works ok. I'm a little confused, as - to my understanding at least - function should return 100 if no data found (see function exception).
Oracle Function:
create or replace FUNCTION F_SCO_DPD
(
p_tip IN NUMBER,
p_dav IN VARCHAR2
)
RETURN NUMBER
IS
sco NUMBER;
BEGIN
SELECT max(score) keep(dense_rank first order by vrednost)
INTO sco
FROM sco_sif_score
WHERE sif_kat = 11
AND tip_pod = p_tip
AND vrednost >= (SELECT a.dpd
FROM sco_dpd a
WHERE a.par_davcna = p_dav);
RETURN sco;
EXCEPTION
WHEN NO_DATA_FOUND
THEN
RETURN 100;
END F_SCO_DPD;
C# Code:
using (OracleCommand cmd = new OracleCommand())
{
cmd.Connection = conn;
cmd.CommandText = "F_SCO_DPD";
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new OracleParameter("p_tip", Podjetje.TipSub));
cmd.Parameters.Add(new OracleParameter("p_dav", Podjetje.Davcna));
cmd.Parameters.Add(new OracleParameter("sco", OracleDbType.Decimal, ParameterDirection.ReturnValue));
cmd.BindByName = true;
cmd.ExecuteScalar();
Score.ScoDpd = (int)(OracleDecimal)cmd.Parameters["sco"].Value;
}
You are running an aggregation function. max(score) keep (dense_rank first order by vrednost) is as much an aggregation function as max(score).
That means that your query is an aggregation query with no GROUP BY. All such queries return exactly 1 row. If no rows match the WHERE clause, then the value is NULL.
So, the exception is never triggered. Instead, check if the returned value is NULL.
The resulting code is:
create or replace FUNCTION F_SCO_DPD
(
p_tip IN NUMBER,
p_dav IN VARCHAR2
)
RETURN NUMBER
IS
v_sco NUMBER;
BEGIN
SELECT max(score) keep (dense_rank first order by vrednost)
INTO v_sco
FROM sco_sif_score
WHERE sif_kat = 11 AND
tip_pod = p_tip AND
vrednost >= (SELECT a.dpd
FROM sco_dpd a
WHERE a.par_davcna = p_dav
);
RETURN COALESCE(v_sco, 100);
END F_SCO_DPD;
I'm experimenting with SEQUENCE objects in SQL Server, and getting the next value with C# by specifying the sequence name. Ranges are simple, because there is a stored procedure for them, and you can pass the sequence name;
public static T Reserve<T>(string name, int count, SqlConnection sqlConn)
{
using (var sqlCmd = new SqlCommand("sp_sequence_get_range", sqlConn))
{
sqlCmd.CommandType = CommandType.StoredProcedure;
var firstValueParam = new SqlParameter("#range_first_value", SqlDbType.Variant) { Direction = ParameterDirection.Output };
sqlCmd.Parameters.AddWithValue("#sequence_name", name);
sqlCmd.Parameters.AddWithValue("#range_size", count);
sqlCmd.Parameters.Add(firstValueParam);
sqlCmd.ExecuteNonQuery();
return (T)firstValueParam.Value;
}
}
But what about single values? It seems to me that I can either call the above with a count of '1', or I can construct the SQL dynamically. i.e.
var sqlCmdStr = string.Format("SELECT NEXT VALUE FOR {0}", name);
Which I know to generally be bad practice (i.e. SQL injection).
What would anyone suggest?
Which I know to generally be bad practice (i.e. SQL injection).
Not every dynamic SQL is evil.
Whether you are open to SQL injection depends on where the value (that gets inserted in SQL text) comes from. If it comes from a place under a tight control of your code (e.g. a switch statement that chooses from a set of string constants) then SQL injection is not an issue.
Or, you could simply have a separate query for each sequence (assuming you don't have very many of them).
My suggestion is a combination of both #Gserg's answer and your current solution. Write a stored procedure that takes a VARCHAR parameter #Name. Build the sql string in the stored procedure, using QUOTENAME as suggested by #GSerg. Use EXEC or sp_executesql to run the script.
Something like this (freehand):
CREATE PROCEDURE [GetNext]
#Name VARCHAR(50)
AS
BEGIN
DECLARE #sql VARCHAR(200);
SET #Name = QUOTENAME(#Name, '[');
SET #sql = 'SELECT NEXT VALUE FOR ' + #Name;
EXEC (#sql);
END
Another version of Paul's solution, which will return formatted alphanumeric Key from SQL Sequence
CREATE PROCEDURE [sp_GetNextKey]
#Name NVARCHAR(50),
#FormatText NVARCHAR(50)
AS
--DECLARE #Name NVARCHAR(50)='CustomerKeySequence'
--DECLARE #FormatText NVARCHAR(50) = 'CUS0000#'
DECLARE #sql NVARCHAR(200) = 'SELECT FORMAT((NEXT VALUE FOR ' + QUOTENAME(#Name, '"') + '),'+QUOTENAME(#FormatText, '''')+')';
EXEC (#sql)
/*
RETURNS i.e CUS00184
*/
When I need to do a similar thing, I do this:
string sanitized_name;
using (var sqlCmd = new SqlCommand("select quotename(#unsafe_name, '[');", sqlConn))
{
sqlCmd.Parameters.AddWithValue("#unsafe_name", name);
sanitized_name = (string)sqlCmd.ExecuteScalar();
}
using (var sqlCmd = new SqlCommand(string.Format("select next value for {0};", sanitized_name), sqlConn))
{
...
}
Or create a server-side procedure that does the same.
I am trying to create a parameterized query in C# against a SQL server database.
Code:
query = new StringBuilder( "SELECT #fields FROM #tables");
using(SqlConnection connection = new SqlConnection(connection))
{
SqlCommand command = new SqlCommand(query.ToString(), connection);
command.Parameters.AddWithValue("#fields", fields.ToString());
command.Parameters.AddWithValue("#tables", tables.ToString());
try
{
connection.Open();
Int32 rowsAffected = command.ExecuteNonQuery();
Console.WriteLine("RowsAffected: {0}", rowsAffected);
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
}
The strange part is this fails with the message "Must declare the table variable \"#tables\". However as you can see, it's clearly been defined.
So my question is:
Can you pass a parameter to define
the table list in the FROM
statement?
If you can, why isn't
this working?
SQL doesn't support the FROM clause to be parameterized. So you have to use either dynamic SQL, or create/concatenate the query string prior to submitting it to the database.
No unfortunately you cant use a parameter in the FROM clause.
I think this is not the way SQL command and its parameters should look like. It should look like
SELECT fieldName1, fieldName2
FROM TableName
WHERE fieldName = #paramName
You cannot use parameters as definition of fields to be selected or the target table. If you need to define fields to be selected, simply compose the command string in StringBuilder before you call it - as you need. Parameters are used for filtering purposes. In your case you don't need any paramters, just build your command and execute.
If you're confident that your table and column names are ok, then you can do some safety checks in the database before building your dynamic SQL.
This is just for illustration - for real life, obviously you'd need to make it a lot cleaner:
declare #TABLE_NAME nvarchar(128)
set #TABLE_NAME = 'Robert'');DROP TABLE Students;--' -- This line will raise an error
set #TABLE_NAME = 'BOOK' -- This line will go through properly
declare #sql varchar(max)
set #sql = 'SELECT * FROM '
if exists (select 1 from sys.objects where type = 'U' and name = #TABLE_NAME)
begin
set #sql = #sql + #TABLE_NAME
exec (#sql)
end
else
begin
raiserror ('ERROR ERROR ERROR', 0, 0)
return
end