So I've finally decided to get around to learning how to use stored procedures, and although I do have them working, I'm unsure if I'm doing it correctly - aka. the best way. So here's what I've got.
Three procedures: TryAddTag, CheckTagExists, and AddTag.
TryAddTag is the procedure that is my intermediary between other code (eg. PHP, etc...) and the other two procedures, so this is the one that gets called.
TryAddTag
DELIMITER //
CREATE PROCEDURE TryAddTag(
IN tagName VARCHAR(255)
)
BEGIN
-- Check if tag already exists
CALL CheckTagExists(tagName, #doesTagExist);
-- If it does not exist, add it
IF #doesTagExist = FALSE THEN
CALL AddTag(tagName);
END IF;
END //
DELIMITER ;
AddTag
DELIMITER //
CREATE PROCEDURE AddTag(
IN tagName VARCHAR(255)
)
BEGIN
INSERT INTO
tags
VALUES(
NULL,
tagName
);
END //
DELIMITER ;
CheckTagExists
DELIMITER //
CREATE PROCEDURE CheckTagExists(
IN
tagName VARCHAR(255),
OUT
doesTagExist BOOL
)
BEGIN
-- Check if tag exists
SELECT
EXISTS(
SELECT
*
FROM
tags
WHERE
tags.NAME = tagName
)
INTO
doesTagExist;
END //
DELIMITER ;
My problems stem from this and use of #doesTagExist.
-- Check if tag already exists
CALL CheckTagExists(tagName, #doesTagExist);
Is the the correct way to use one of these variables? And/or, how can I use a DECLARE'd variable to store the result of CheckTagExists within TryAddTag? I expected something along the lines of
...
DECLARE doesTagExist BOOL;
SET doesTagExist = CheckTagExist('str');
...
or something like that...
your stored procedure is a little over-engineered for my liking - keep it simple :)
MySQL
drop table if exists tags;
create table tags
(
tag_id int unsigned not null auto_increment primary key,
name varchar(255) unique not null
)
engine=innodb;
drop procedure if exists insert_tag;
delimiter #
create procedure insert_tag
(
in p_name varchar(255)
)
proc_main:begin
declare v_tag_id int unsigned default 0;
if exists (select 1 from tags where name = p_name) then
select -1 as tag_id, 'duplicate name' as msg; -- could use multiple out variables...i prefer this
leave proc_main;
end if;
insert into tags (name) values (p_name);
set v_tag_id = last_insert_id();
-- do stuff with v_tag_id...
-- return success
select v_tag_id as tag_id, 'OK' as msg;
end proc_main #
delimiter ;
PHP
<?php
ob_start();
try{
$conn = new mysqli("localhost", "foo_dbo", "pass", "foo_db", 3306);
$conn->autocommit(FALSE); // start transaction
// create the tag
$name = 'f00';
$sql = sprintf("call insert_tag('%s')", $conn->real_escape_string($name));
$result = $conn->query($sql);
$row = $result->fetch_array();
$result->close();
$conn->next_result();
$tagID = $row["tag_id"]; // new tag_id returned by sproc
if($tagID < 0) throw new exception($row["msg"]);
$conn->commit();
echo sprintf("tag %d created<br/>refresh me...", $tagID);
}
catch(exception $ex){
ob_clean();
//handle errors and rollback
$conn->rollback();
echo sprintf("oops error - %s<br/>", $ex->getMessage());
}
// finally
$conn->close();
ob_end_flush();
?>
Stored PROCEDURES can return a resultset. The last thing you SELECT in a stored procedure is available as a resultset to the calling environment.. Stored FUNCTIONS can return only a single result primitive.
You may also mark your parameters as INOUT parameters.
If you want this:
DECLARE doesTagExist BOOL;
SET doesTagExist = CheckTagExist('str');
then you should use functions:
DELIMITER //
CREATE FUNCTION CheckTagExists(
tagName VARCHAR(255)
)
BEGIN
DECLARE doesTagExist BOOL;
-- Check if tag exists
SELECT
EXISTS(
SELECT
*
FROM
tags
WHERE
tags.NAME = tagName
)
INTO
doesTagExist;
RETURN doesTagExist;
END //
DELIMITER ;
DECLARE doesTagExist BOOL;
SET CheckTagExist('str',doesTagExist);
is the correct way of doing it with just store procedures. There are no 'regular' return values.
Related
Is it possible to create a select trigger? I know there are update insert or delete triggers. But select trigger is what I need for a little url shortener application. Everytime a shorturl is hit I'd like to update counter and last access date. A select trigger would be perfect.
select url where code = :code
whould be the one to trigger the trigger.
Ended up doing this:
create function `get_the_url`(c char(7))
returns varchar(255)
begin
declare result varchar(255);
update code
set
hits = hits + 1,
last_used=current_timestamp()
where code = c;
return (
select url
from code
where code = c
);
end
saving this for prosperity and me.
for this table:
create table `code` (
`code` char(5) not null,
`url` varchar(240) default null,
`last_used` datetime not null default '0000-00-00 00:00:00',
`hits` int(10) unsigned not null default 0,
primary key (`code`) using hash,
key `url` (`url`) using hash
) engine=myisam
For those few php folks interested, this is how I populated the table:
<?php
declare(strict_types=1);
$charset ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNAOPQRSTUVWXYZ1234567890-_.!~*'()";
$size = 8;
function getCode(): string {
global $size;
global $charset;
$length = strlen($charset)-1;
$result =
$charset[rand(0,$length)]
.$charset[rand(0,$length)]
.$charset[rand(0,$length)]
.$charset[rand(0,$length)]
.$charset[rand(0,$length)];
return $result;
}
$db = new \PDO("mysql:dbname=qr","qr","qr");
$insert = $db-> prepare("insert into code(code,url,last_used,hits)values(:code,null,default,default)");
$code = getCode();
$insert->bindParam(':code',$code);
for($i=1000; $i>0; $i--) {
$code = getCode();
$insert-> execute() or die($insert->errorInfo()[2]);
}
and yes, populating this upfront probably makes sense as storage space is cheap and computing power is not.
I created a stored procedure that sets the certain variable value and executes merge using this variable. Currently, it just returns a hardcoded message "Successfully executed.".
Is it possible to return the same result as the original merge query returns, like
number of rows inserted
number of rows updated
?
CREATE OR REPLACE PROCEDURE ALERTS_MERGE_PROCEDURE ()
RETURNS STRING NOT NULL
LANGUAGE JAVASCRIPT
AS
$$
var sql_command = '
MERGE INTO tablename
..
WHEN MATCHED THEN
UPDATE SET ...
WHEN NOT MATCHED THEN
INSERT ...
);
'
snowflake.execute(
{
sqlText: sql_command
});
return "Successfully executed.";
$$;
You can iterate over the columns of the first row of the returned object from the execution:
create or replace temp table tablename as
select 1::int id, 'a'::string tx;
create or replace temp table tablesource as
select 1::int id, 'b'::string tx
union select 2, 'c';
CREATE OR REPLACE PROCEDURE ALERTS_MERGE_PROCEDURE ()
RETURNS STRING NOT NULL
LANGUAGE JAVASCRIPT
AS
$$
var sql_command = `
merge into tablename a
using tablesource b
on a.id = b.id
when matched then update set tx=b.tx
when not matched then insert (id, tx) values (b.id, b.tx);
`;
var x = snowflake.execute({sqlText: sql_command});
x.next();
var result = '';
for (i=1; i<=x.getColumnCount(); i++) {
result += x.getColumnName(i) + ': ' + x.getColumnValue(i) + '\n';
}
return result;
$$;
call alerts_merge_procedure();
Returns:
number of rows inserted: 1
number of rows updated: 1
Yes, look at the methods of the statement object. This is all documented here: https://docs.snowflake.com/en/sql-reference/stored-procedures-api.html#object-statement
Say I call a stored procedure like this:
call SP_TEST('CAT','LION');
Now post successful run I get the query id using the command:
select last_query_id();
This returns the query id as 01a07606-0b02-362d-0001-1d6602361072
Say I want this query id to be written to a variable during runtime - such as :
create or replace procedure test_proc111
("SRC" VARCHAR(30), "TGT" VARCHAR(30))
returns varchar
language javascript
execute as owner
as '
{
var TABLE_VALUE = "";
var code1 = "select last_query_id();"
var code1_excecute = snowflake.execute({sqlText: code1});
while(code1_excecute.next()) {
var TABLE_VALUE = code1_excecute.getColumnValue(1);
}
return TABLE_VALUE
}
';
Now I call this stored procedure like this:
call test_proc1111('TEST','TARGET')
But I get this error:
Execution error in stored procedure TEST_PROC1111: Statement NULL not found At Snowflake. execute, line 8 position 31
How can we achieve this use case?
In the stored procedure in the question, you'll only get a query id if it's executed as caller:
create or replace procedure test_proc111
("SRC" VARCHAR(30), "TGT" VARCHAR(30))
returns varchar
language javascript
execute as caller
as '
var TABLE_VALUE;
var code1 = "select last_query_id(-1);"
var code1_excecute = snowflake.execute({sqlText: code1});
while(code1_excecute.next()){
TABLE_VALUE = code1_excecute.getColumnValue(1);
}
return TABLE_VALUE
'
;
This because if it's defined at execute as owner, it will only have a previous query id if there is another query executed within the procedure:
create or replace procedure test_proc111
("SRC" VARCHAR(30), "TGT" VARCHAR(30))
returns varchar
language javascript
execute as owner
as '
var TABLE_VALUE;
var code1 = "select last_query_id(-1);"
var code1_excecute = snowflake.execute({sqlText: code1});
var code1 = "select last_query_id(-1);"
var code1_excecute = snowflake.execute({sqlText: code1});
while(code1_excecute.next()){
TABLE_VALUE = code1_excecute.getColumnValue(1);
}
return TABLE_VALUE
'
;
My project is using .NET Core 3.1 and I have my stored procedures executing in my repository class. I want to insert and return the scope identity(the id of the record that just inserted UserNumber) so I can use it for another stored proc within this same method. The problem I have here is that parameters[1].Value value is returning zero.
Here is an abbreviation of my stored proc:
ALTER PROCEDURE [dbo].[InsertUser]
#iUserNumber int OUTPUT,
As
INSERT dbo.tblUser (
CreatedBy
)
VALUES (#LoginUserId)
IF ##ERROR <> 0 GOTO ERRHANDLER
SET #UserNumber = SCOPE_IDENTITY() /** this is the primary Key **/
RETURN(#UserNumber)
Here is a sample of my repository
public int InsertUsers(int LoginUserId, int UserNumber)
{
SqlParameter[] parameters = new List<SqlParameter>()
{
_dbContext.CreateSqlParameter(StoredProcedureConstants.LoginUserId,SqlDbType.Int,LoginUserId.ToSafeInt()),
_dbContext.CreateSqlParameter(StoredProcedureConstants.UserNumber,SqlDbType.Int,UserNumber.ToSafeInt())
}.ToArray();
var intResult = _dbContext.ExecuteNonQuery(StoredProcedureConstants.InsertUsers, parameters);
var result2 = parameters[1].Value; //This comes back as zero
How do I assign the scope identity to result2?
Should be something like:
CREATE OR ALTER PROCEDURE [dbo].[InsertUser]
#LoginUserId int,
#iUserNumber int OUTPUT
As
INSERT dbo.tblUser (CreatedBy)
VALUES (#LoginUserId)
SET #iUserNumber = SCOPE_IDENTITY() /** this is the primary Key **/
and
SqlParameter[] parameters = new List<SqlParameter>()
{
_dbContext.CreateSqlParameter("#LoginuserId",SqlDbType.Int,LoginUserId),
_dbContext.CreateSqlParameter("#iUserNumber",SqlDbType.Int)
}.ToArray();
parameters[1].Direction = ParameterDirection.Output;
_dbContext.ExecuteNonQuery(StoredProcedureConstants.InsertUsers, parameters);
var result2 = parameters[1].Value;
Here is my stored procedure
ALTER PROCEDURE Delete
#ID nvarchar(64),
#value int = 0 output
AS
BEGIN
IF(EXISTS(SELECT * FROM A where Ap = #ID))
BEGIN
set #value = 1
END
ELSE IF(EXISTS(SELECT * FROM B where Bp = #ID))
BEGIN
set #value = 2
END
ELSE
BEGIN
select *
from Table_D
END
END
RETURN #value
Problem is that when I execute it, this does not return any value
There are multiple ways of returning status information from a stored procedure to an application. Each has its pros and cons; no single technique can definitely be said to be the right one in all circumstances. Even so, I'll start off with:
TL;DR: recommendation
Use RAISERROR if your stored procedure runs into trouble and cannot return the data it normally returns. Use OUTPUT parameters for information the client isn't free to ignore, but which isn't logically part of your result. Use the return value if you have an informational status code that the client is free to ignore. Use additional result sets only if you know what you're doing.
RAISERROR
If your stored procedure encounters an error and can't return any data, you can use RAISERROR to terminate execution and cause an exception to be raised on the client side.
CREATE PROCEDURE [Delete]
#ID nvarchar(64)
AS BEGIN
IF(EXISTS(SELECT * FROM A where Ap = #ID))
BEGIN
RAISERROR('Wrong. Try again.', 11, 1);
RETURN;
END
ELSE IF(EXISTS(SELECT * FROM B where Bp = #ID))
BEGIN
RAISERROR('Wrong in a different way. Try again.', 11, 2);
RETURN;
END
ELSE
BEGIN
select *
from Table_D
END
END
The second parameter (severity) must be set to at least 11 to make the error propagate as an exception, otherwise it's just an informational message. Those can be captured too, but that's out of the scope of this answer. The third parameter (state) can be whatever you like and could be used to pass the code of the error, if you need to localize it, for example. User-generated message always have SQL error code 50000, so that can't be used to distinguish different errors, and parsing the message is brittle.
The C# code to process the result:
try {
using (var reader = command.ExecuteReader()) {
while (reader.Read()) {
...
}
}
} catch (SqlException e) {
Console.WriteLine(
"Database error executing [Delete] (code {0}): {1}", e.State, e.Message
);
}
This is a natural fit for errors because the code to actually process the data stays what it is, and you can handle the exception at the right location (rather than propagating a status code everywhere). But this method is not appropriate if the stored procedure is expected to return a status that is informational and not an error, as you would be catching exceptions all the time even though nothing's wrong.
Output parameter
A stored procedure can set parameter values as well as receive them, by declaring them OUTPUT:
CREATE PROCEDURE [Delete]
#ID nvarchar(64),
#StatusCode INT OUTPUT
AS BEGIN
IF(EXISTS(SELECT * FROM A where Ap = #ID))
BEGIN
SET #StatusCode = 1;
END
ELSE IF(EXISTS(SELECT * FROM B where Bp = #ID))
BEGIN
SET #StatusCode = 2;
END
ELSE
BEGIN
SET #StatusCode = 0;
select *
from Table_D
END
END
From C#, this is captured in a parameter marked as an output parameter:
SqlParameter statusCodeParameter = command.Parameters.Add(
new SqlParameter {
ParameterName = "#StatusCode",
SqlDbType = SqlDbType.Int,
Direction = ParameterDirection.Output
}
);
using (var reader = command.ExecuteReader()) {
int statusCode = (int) statusCodeParameter.Value;
if (statusCode != 0) {
// show alert
return;
}
while (reader.Read()) {
...
}
}
The benefits here are that the client cannot forget to declare the parameter (it must be supplied), you're not restricted to a single INT, and you can use the value of the parameter to decide what you want to do with the resul set. Returning structured data is cumbersome this way (lots of OUTPUT parameters), but you could capture this in a single XML parameter.
Return value
Every stored procedure has a return value, which is a single INT. If you don't explicitly set it using RETURN, it stays at 0.
CREATE PROCEDURE [Delete]
#ID nvarchar(64)
AS BEGIN
IF(EXISTS(SELECT * FROM A where Ap = #ID))
BEGIN
RETURN 1
END
ELSE IF(EXISTS(SELECT * FROM B where Bp = #ID))
BEGIN
RETURN 2
END
ELSE
BEGIN
select *
from Table_D
END
END
From C#, the return value has to be captured in a single special parameter marked as the return value:
SqlParameter returnValueParameter = command.Parameters.Add(
new SqlParameter { Direction = ParameterDirection.ReturnValue }
);
using (var reader = command.ExecuteReader()) {
// this could be empty
while (reader.Read()) {
...
}
}
int returnValue = (int) returnValueParameter.Value;
It's important to note that the return value will not be available until you've processed all other result sets that the stored procedure generates (if any), so if you're using it for a status code that indicates there are no rows, you must still process the empty result set first before you have the status code. You cannot return anything other than an INT. Frameworks/OR mappers often have no support for the return value. Finally, note that the client is not required to do anything with the return value, so you have to carefully document its intended use.
Result set
The stored procedure can simply return what it wants as the result set, just like it's returning the other data. A stored procedure is allowed to return multiple result sets, so even if your status is logically separate from the other data, you can return it as a row.
CREATE PROCEDURE [Delete]
#ID nvarchar(64)
AS BEGIN
DECLARE #StatusCode INT = 0;
IF(EXISTS(SELECT * FROM A where Ap = #ID))
BEGIN
SET #StatusCode = 1;
END
ELSE IF(EXISTS(SELECT * FROM B where Bp = #ID))
BEGIN
SET #StatusCode = 2;
END
SELECT #StatusCode AS StatusCode;
IF #StatusCode = 0
BEGIN
select *
from Table_D
END
END
To process this with C#, we need SqlDataReader.NextResult:
using (var reader = command.ExecuteReader()) {
if (!reader.Read()) throw new MyException("Expected result from stored procedure.");
statusCode = reader.GetInt32(reader.GetOrdinal("StatusCode"));
if (statusCode != 0) {
// show alert
return;
}
reader.NextResult();
while (reader.Read()) {
// use the actual result set
}
}
The main drawback here is that it's not intuitive for a stored procedure to return a variable number of result sets, and very few data frameworks/OR mappers support it, so you'll nearly always end up writing manual code like this. Returning multiple result sets is not really a good fit for returning a single piece of data like a status code, but it might be an alternative to returning structured data in an XML output parameter (especially if there's lots).
The return seems to be out of scope of the procedure. Try:
ALTER PROCEDURE Delete
#ID nvarchar(64),
#value int=0 output
AS
BEGIN
IF(EXISTS(SELECT * FROM A where Ap=#ID))
BEGIN
set #value=1
END
ELSE IF(EXISTS(SELECT * FROM B where Bp=#ID))
BEGIN
set #value=2
END
ELSE
BEGIN
set #value=5
end --end if
RETURN #value
end --end procedure
This is where using tabbing properly makes the code a lot more readable, and these problems more obvious
Don't use the output parameter. Rather, use this:
ALTER PROCEDURE Delete
#ID nvarchar(64)
AS
BEGIN
DECLARE #value int
SET #value = 0
IF(EXISTS(SELECT 1 FROM A where Ap=#ID))
BEGIN
set #value=1
END
ELSE IF(EXISTS(SELECT 1 FROM B where Bp=#ID))
BEGIN
set #value=2
END
ELSE
BEGIN
set #value=5
end
Select #value as Value, * from Table_D
end
Can you try running the SP as the script below?
declare #pID as nvarchar(64)
declare #pValue as int
set #pID = 1 -- Your ID filter
exec Delete #pID, #pValue OUTPUT
select #pValue