How to capture the newly inserted row's ID value from C#? - tsql

Here's my stored procedure
ALTER PROCEDURE dbo.spInsertNewTask
#ModuleID int,
#Task varchar(50),
#StartDate date,
#PlannedEndDate date,
#EstimatedEndDate date,
#Status int,
#Comments varchar(500),
#Started bit
AS
INSERT INTO DTasks (ModuleID, Task, StartDate, PlannedEndDate, EstimatedEndDate, Status, Comments, Started, LastUpdated)
VALUES (#ModuleID, #Task, #StartDate, #PlannedEndDate, #EstimatedEndDate, #Status, #Comments, #Started, GETDATE())
RETURN SCOPE_IDENTITY()
When I try to capture the newly created ID, all I'm getting is always 1. How to capture the New row's ID?
newID = cmd.ExecuteNonQuery();
In fact I need this new ID for further processing.
Thanks for helping.

Instead of ExecuteNonQuery, which returns the number of effected rows, you should use ExecuteScalar, casting the returned object to the correct type:
newID = (int)cmd.ExecuteScalar();
For the above to work, you also need to change the SQL from returning a return value to select the value:
SELECT SCOPE_IDENTITY()
The code example on the page is pretty much what you are looking to do.
If you want to keep the return type, you will need to add a return type parameter to the command, execute as non-query as you currently do, then read the value of the return parameter, as described in Getting return value from stored procedure in C#.

You need to pass [ID] assuming that it is you identity column as output parameter:
In C# you need to add new parameter which is output parameter and at the end of execution to retrieve :
Initialize:
SqlParameter param = new SqlParameter("#ID", 0);
param.Direction = ParameterDirection.Output;
param.DbType = DbType.Int32;
Retrieve after execution:
int id = Convert.ToInt32(sqlcommand.Parameters["#ID"].Value.ToString());
Stored procedure:
ALTER PROCEDURE dbo.spInsertNewTask
#ID INT = NULL OUTPUT,
#ModuleID int,
#Task varchar(50),
#StartDate date,
#PlannedEndDate date,
#EstimatedEndDate date,
#Status int,
#Comments varchar(500),
#Started bit
AS
BEGIN
INSERT INTO DTasks (ModuleID, Task, StartDate, PlannedEndDate, EstimatedEndDate, Status, Comments, Started, LastUpdated)
VALUES (#ModuleID, #Task, #StartDate, #PlannedEndDate, #EstimatedEndDate, #Status, #Comments, #Started, GETDATE())
SET #ID = SCOPE_IDENTITY()
END
GO

Related

Run a stored procedure using select columns as input parameters?

I have a select query that returns a dataset with "n" records in one column. I would like to use this column as the parameter in a stored procedure. Below a reduced example of my case.
The query:
SELECT code FROM rawproducts
The dataset:
CODE
1
2
3
The stored procedure:
ALTER PROCEDURE [dbo].[MyInsertSP]
(#code INT)
AS
BEGIN
INSERT INTO PRODUCTS description, price, stock
SELECT description, price, stock
FROM INVENTORY I
WHERE I.icode = #code
END
I already have the actual query and stored procedure done; I just am not sure how to put them both together.
I would appreciate any assistance here! Thank you!
PS: of course the stored procedure is not as simple as above. I just choose to use a very silly example to keep things small here. :)
Here's two methods for you, one using a loop without a cursor:
DECLARE #code_list TABLE (code INT);
INSERT INTO #code_list SELECT code, ROW_NUMBER() OVER (ORDER BY code) AS row_id FROM rawproducts;
DECLARE #count INT;
SELECT #count = COUNT(*) FROM #code_list;
WHILE #count > 0
BEGIN
DECLARE #code INT;
SELECT #code = code FROM #code_list WHERE row_id = #count;
EXEC MyInsertSP #code;
DELETE FROM #code_list WHERE row_id = #count;
SELECT #count = COUNT(*) FROM #code_list;
END;
This works by putting the codes into a table variable, and assigning a number from 1..n to each row. Then we loop through them, one at a time, deleting them as they are processed, until there is nothing left in the table variable.
But here's what I would consider a better method:
CREATE TYPE dbo.code_list AS TABLE (code INT);
GO
CREATE PROCEDURE MyInsertSP (
#code_list dbo.code_list)
AS
BEGIN
INSERT INTO PRODUCTS (
[description],
price,
stock)
SELECT
i.[description],
i.price,
i.stock
FROM
INVENTORY i
INNER JOIN #code_list cl ON cl.code = i.code;
END;
GO
DECLARE #code_list dbo.code_list;
INSERT INTO #code_list SELECT code FROM rawproducts;
EXEC MyInsertSP #code_list = #code_list;
To get this to work I create a user-defined table type, then use this to pass a list of codes into the stored procedure. It means slightly rewriting your stored procedure, but the actual code to do the work is much smaller.
(how to) Run a stored procedure using select columns as input
parameters?
What you are looking for is APPLY; APPLY is how you use columns as input parameters. The only thing unclear is how/where the input column is populated. Let's start with sample data:
IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL DROP TABLE dbo.Products;
IF OBJECT_ID('dbo.Inventory','U') IS NOT NULL DROP TABLE dbo.Inventory;
IF OBJECT_ID('dbo.Code','U') IS NOT NULL DROP TABLE dbo.Code;
CREATE TABLE dbo.Products
(
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Inventory
(
icode INT NOT NULL,
[description] VARCHAR(1000) NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
);
CREATE TABLE dbo.Code(icode INT NOT NULL);
INSERT dbo.Inventory
VALUES (10,'',20.10,3),(11,'',40.10,3),(11,'',25.23,3),(11,'',55.23,3),(12,'',50.23,3),
(15,'',33.10,3),(15,'',19.16,5),(18,'',75.00,3),(21,'',88.00,3),(21,'',100.99,3);
CREATE CLUSTERED INDEX uq_inventory ON dbo.Inventory(icode);
The function:
CREATE FUNCTION dbo.fnInventory(#code INT)
RETURNS TABLE AS RETURN
SELECT i.[description], i.price, i.stock
FROM dbo.Inventory I
WHERE I.icode = #code;
USE:
DECLARE #code TABLE (icode INT);
INSERT #code VALUES (10),(11);
SELECT f.[description], f.price, f.stock
FROM #code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
Results:
description price stock
-------------- -------- -----------
20.10 3
40.10 3
Updated Proc (note my comments):
ALTER PROC dbo.MyInsertSP -- (1) Lose the input param
AS
-- (2) Code that populates the "code" table
INSERT dbo.Code VALUES (10),(11);
-- (3) Use CROSS APPLY to pass the values from dbo.code to your function
INSERT dbo.Products ([description], price, stock)
SELECT f.[description], f.price, f.stock
FROM dbo.code AS c
CROSS APPLY dbo.fnInventory(c.icode) AS f;
This ^^^ is how it's done.

How to use openxml within a user defined function in SQL Server

I have an XML structure that I parse using OPENXML within a stored procedure to retrieve parameters used to perform a query. This procedure was a base procedure that a different stored procedure (procedure 2) is calling. Procedure 2 uses an insert-exec construct to get the data from the base procedure. This works great as long as we only call Procedure 2 or the base procedure.
My first problem is that I have a different procedure (procedure 3) that now needs to get the result from procedure 2 (I need the business rules that this procedure enforces), but cannot due to the message:
An INSERT EXEC statement cannot be nested.
I then tried to take the base procedure and make it a table valued function, but when I execute it, I receive the message:
Only functions and some extended stored procedures can be executed from within a function.
How do I get around one or both of these issues?
EDIT 1
I am including a code snippet to show the base procedure (Procedure 1) and the procedure implementing business requirements on the results of that procedure (Procedure 2). If there is a 3rd procedure that needs the results with the business rules applied, we run into problems.
create procedure dbo.p_Proc
#Xml xml
as
begin
set nocount on;
declare #l_idoc int
, #InfoId int
, #InfoTypeId int
, #Id int
, #Name varchar(50)
, #StatusId int
, #RoleId int
, #XmlBase xml
, #l_path varchar(100);
declare #T_TABLE table(
InfoId int
, InfoTypeId int
);
declare #T_RESULT table
(
Field1 int
, Field2 varchar(50)
, Field3 int
);
EXEC sp_xml_preparedocument #l_idoc OUTPUT, #Xml;
set #l_path = '/xml/Info';
insert into #T_TABLE(InfoId, InfoTypeId)
select InfoId, InfoTypeId
from OPENXML (#l_idoc, #l_path, 1)
with (
InfoId int './#InfoId'
, InfoTypeId int './#InfoTypeId'
);
select #InfoId = InfoId
, #InfoTypeId = InfoTypeId
from #T_TABLE;
-- create the XML to call the base widgets
select #XmlBase =
(
select *
from
(
select t.Id, t.Name, t.StatusId, t.RoleId
from #T_TABLE w
inner join dbo.T_TABLE2 t
on t.InfoId = w.InfoId
and t.InfoTypeId = w.InfoTypeId
) b
for xml raw('Widget'), root('Xml')
);
-- retrieve widgets from base security
insert into #T_RESULT(Field1, Field2, Field3)
exec dbo.p_ProcBase #Xml = #XmlBase;
-- apply business logic here
select w.Field1, w.Field2, w.Field3
from #T_RESULT w;
end;
go
create procedure dbo.p_ProcBase
#Xml xml = null
as
begin
set nocount on;
declare #l_idoc int
, #Id int
, #Name varchar(50)
, #StatusId int
, #RoleId int
, #l_path varchar(100);
declare #T_Table table(
Id int
, Name varchar(50)
, StatusId int
, RoleId int
);
EXEC sp_xml_preparedocument #l_idoc OUTPUT, #Xml;
set #l_path = '/Xml/Widget';
insert into #T_Table(Id, Name, StatusId, RoleId)
select Id, Name, StatusId, RoleId
from OPENXML (#l_idoc, #l_path, 1)
with (
ProjectId int './#Id'
, WidgetTypeName varchar(50) './#Name'
, WorkflowStatusId int './#StatusId'
, UserRoleId bigint './#RoleId'
);
select #Id = w.Id
, #Name = w.Name
, #StatusId = w.StatusId
, #RoleId = w.RoleId
from #T_Table w;
-- retrieve enabled widgets for which the user has a role in the current workflow state
select t.Field1, t.Field2, t.Field3
from dbo.T_TABLE t
where t.StatusId = #StatusId
and t.RoleId = #RoleId;
end;
In order to send a data set (table) between procs, you must use a Table type, store the output of proc2 in a variable of table type and add a readonly only table type parameter to proc3
First you must create a table type to map your output from proc2:
CREATE TYPE T_RESULT AS TABLE
(
Field1 int
, Field2 varchar(50)
, Field3 int
);
In dbo.p_Proc change #T_RESULT to:
declare #T_RESULT T_RESULT
Then create proc3:
CREATE PROCEDURE dbo.proc3
#T_RESULT T_RESULT READONLY
AS
BEGIN
SET NOCOUNT ON
INSERT INTO T3(...)
SELECT ... FROM #T_RESULT
END
Don't forget to add READONLY after a table type parameter in a proc.

Column name as parameter and do sum on that in T Sql

My requirement is to send Columnname as Parameter to Stored procedure and do SUM on that Column.
I have written a small stored procedure to accept the column name as a parameter and do sum on that but I am getting an error with it.
CREATE PROCEDURE dbo.testCol
-- Add the parameters for the stored procedure here
#type as nvarchar(20),
#beginDate as smalldatetime,
#endDate as smalldatetime
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
select dbo.mytable.date, sum(#type) as quantity
from dbo.mytable
where dbo.mytable.Date between #beginDate AND #endDate
group by dbo.mytable.date,dbo.mytable.day
order by dbo.mytable.date
END
GO
I am getting the error as "Operand data type nvarchar is invalid for sum operator." while executing this stored procedure.
Any help much appreciated.
Thanks
You'll need to use dynamic sql to get the passed column name to be used this way in the query.
Use sp_executesql and include the column name in the string, and pass #beginDate and #endDate as parameters.
If you know the column names that are valid values, you can use CASE to avoid the evils of SQL Injection:
select dbo.mytable.date,
sum(case #type when 'QuantityColumn1' then QuantityColumn1
else QuantityColumn2 end ) as quantity
from dbo.mytable
where dbo.mytable.Date between #beginDate AND #endDate
group by dbo.mytable.date,dbo.mytable.day
order by dbo.mytable.date
If you must resort to dynamic SQL, validate your input so it can be trusted.
Yes, there is an easier way, Coolcake. Just cast the variable first like so:
CREATE TABLE FOO
(N VARCHAR(20))
SELECT * FROM DBO.FOO
INSERT INTO FOO VALUES('10')
INSERT INTO FOO VALUES('20')
INSERT INTO FOO VALUES('30')
INSERT INTO FOO VALUES('40')
INSERT INTO FOO VALUES('50')
SELECT SUM(CAST(N AS INT)) FROM FOO

How do I TryParse in SQL 2000?

I have a stored procedure in an old SQL 2000 database that takes a comment column that is formatted as a varchar and exports it out as a money object. At the time this table structure was setup, it was assumed this would be the only data going into this field. The current procedure functions simply this this:
SELECT CAST(dbo.member_category_assign.coment AS money)
FROM dbo.member_category_assign
WHERE member_id = #intMemberId
AND
dbo.member_category_assign.eff_date <= #dtmEndDate
AND
(
dbo.member_category_assign.term_date >= #dtmBeginDate
OR
dbo.member_category_assign.term_date Is Null
)
However, data is now being inserted into this column that is not parsable to a money object and is causing the procedure to crash. I am unable to remove the "bad" data (since this is a third party product), but need to update the stored procedure to test for a money parsable entry and return that.
How can I update this procedure so that it will only return the value that is parsable as a money object? Do I create a temporary table and iterate through every item, or is there a more clever way to do this? I'm stuck with legacy SQL 2000 (version 6.0) so using any of the newer functions unfortunately is not available.
Checking for IsNumeric may help you - you can simply return a Zero value. If you want to return a 'N/a' or some other string value
I created the sample below with the columns from your query.
The first query just returns all rows.
The second query returns a MONEY value.
The third one returns a String value with N/A in place of the non-integer value.
set nocount on
drop table #MoneyTest
create table #MoneyTest
(
MoneyTestId Int Identity (1, 1),
coment varchar (100),
member_id int,
eff_date datetime,
term_date datetime
)
insert into #MoneyTest (coment, member_id, eff_date, term_date)
values
(104, 1, '1/1/2008', '1/1/2009'),
(200, 1, '1/1/2008', '1/1/2009'),
(322, 1, '1/1/2008', '1/1/2009'),
(120, 1, '1/1/2008', '1/1/2009')
insert into #MoneyTest (coment, member_id, eff_date, term_date)
values ('XX', 1, '1/1/2008', '1/1/2009')
Select *
FROM #MoneyTest
declare #intMemberId int = 1
declare #dtmBeginDate DateTime = '1/1/2008'
declare #dtmEndDate DateTime = '1/1/2009'
SELECT
CASE WHEN ISNUMERIC (Coment)=1 THEN CAST(#MoneyTest.coment AS money) ELSE cast (0 as money) END MoneyValue
FROM #MoneyTest
WHERE member_id = #intMemberId
AND #MoneyTest.eff_date <= #dtmEndDate
AND
(
#MoneyTest.term_date >= #dtmBeginDate
OR
#MoneyTest.term_date Is Null
)
SELECT
CASE WHEN ISNUMERIC (Coment)=1 THEN CAST (CAST(#MoneyTest.coment AS money) AS VARCHAR) ELSE 'N/a' END StringValue
FROM #MoneyTest
WHERE member_id = #intMemberId
AND #MoneyTest.eff_date <= #dtmEndDate
AND
(
#MoneyTest.term_date >= #dtmBeginDate
OR
#MoneyTest.term_date Is Null
)
Apologies for making a new answer, where a comment would suffice, but I lack the required permissions to do so. Onto the answer to your question, I would only like to add that you should use the above ISNUMERIC carefully. While it works much as expected, it also parses things like '1.3E-2' as a value numeric, which strangely enough you cannot cast into a numeric or money without generating an exception. I generally end up using:
SELECT
CASE WHEN ISNUMERIC( some_value ) = 1 AND CHARINDEX( 'E', Upper( some_value ) ) = 0
THEN Cast( some_value as money )
ELSE Cast( 0 as money )
END as money_value

sql missing rows when grouped by DAY, MONTH, YEAR

If I select from a table group by the month, day, year,
it only returns rows with records and leaves out combinations without any records, making it appear at a glance that every day or month has activity, you have to look at the date column actively for gaps. How can I get a row for every day/month/year, even when no data is present, in T-SQL?
Create a calendar table and outer join on that table
My developer got back to me with this code, underscores converted to dashes because StackOverflow was mangling underscores -- no numbers table required. Our example is complicated a bit by a join to another table, but maybe the code example will help someone someday.
declare #career-fair-id int
select #career-fair-id = 125
create table #data ([date] datetime null, [cumulative] int null)
declare #event-date datetime, #current-process-date datetime, #day-count int
select #event-date = (select careerfairdate from tbl-career-fair where careerfairid = #career-fair-id)
select #current-process-date = dateadd(day, -90, #event-date)
while #event-date <> #current-process-date
begin
select #current-process-date = dateadd(day, 1, #current-process-date)
select #day-count = (select count(*) from tbl-career-fair-junction where attendanceregister <= #current-process-date and careerfairid = #career-fair-id)
if #current-process-date <= getdate()
insert into #data ([date], [cumulative]) values(#current-process-date, #day-count)
end
select * from #data
drop table #data
Look into using a numbers table. While it can be hackish, it's the best method I've come by to quickly query missing data, or show all dates, or anything where you want to examine values within a range, regardless of whether all values in that range are used.
Building on what SQLMenace said, you can use a CROSS JOIN to quickly populate the table or efficiently create it in memory.
http://www.sitepoint.com/forums/showthread.php?t=562806
The task calls for a complete set of dates to be left-joined onto your data, such as
DECLARE #StartInt int
DECLARE #Increment int
DECLARE #Iterations int
SET #StartInt = 0
SET #Increment = 1
SET #Iterations = 365
SELECT
tCompleteDateSet.[Date]
,AggregatedMeasure = SUM(ISNULL(t.Data, 0))
FROM
(
SELECT
[Date] = dateadd(dd,GeneratedInt, #StartDate)
FROM
[dbo].[tvfUtilGenerateIntegerList] (
#StartInt,
,#Increment,
,#Iterations
)
) tCompleteDateSet
LEFT JOIN tblData t
ON (t.[Date] = tCompleteDateSet.[Date])
GROUP BY
tCompleteDateSet.[Date]
where the table-valued function tvfUtilGenerateIntegerList is defined as
-- Example Inputs
-- DECLARE #StartInt int
-- DECLARE #Increment int
-- DECLARE #Iterations int
-- SET #StartInt = 56200
-- SET #Increment = 1
-- SET #Iterations = 400
-- DECLARE #tblResults TABLE
-- (
-- IterationId int identity(1,1),
-- GeneratedInt int
-- )
-- =============================================
-- Author: 6eorge Jetson
-- Create date: 11/22/3333
-- Description: Generates and returns the desired list of integers as a table
-- =============================================
CREATE FUNCTION [dbo].[tvfUtilGenerateIntegerList]
(
#StartInt int,
#Increment int,
#Iterations int
)
RETURNS
#tblResults TABLE
(
IterationId int identity(1,1),
GeneratedInt int
)
AS
BEGIN
DECLARE #counter int
SET #counter= 0
WHILE (#counter < #Iterations)
BEGIN
INSERT #tblResults(GeneratedInt) VALUES(#StartInt + #counter*#Increment)
SET #counter = #counter + 1
END
RETURN
END
--Debug
--SELECT * FROM #tblResults