T-SQL Cursor in stored procedure - sql-server

I am using a stored procedure and I want to use cursor for inserting new data (if data exist I want to update)
ALTER Procedure [dbo].[conn]
#ResellerID int,
#GWResellerID int,
#UserName varchar(50),
#Password varchar(50),
#URL varchar(100),
#ServiceType int,
#ServiceDesc varchar(50),
#FeedFrom bit,
#PublicKey varchar(max)
AS
declare gateway cursor for
select *
from reseller_profiles
where main_reseller_ID = #ResellerID
OPEN gateway
FETCH NEXT FROM gateway INTO #ResellerID
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO [dbo].tblGatewayConnection([ResellerID],[GWResellerID], [UserName], [Password], [URL], [ServiceType], [ServiceDesc],[feedFromMain], publicKey)
VALUES (#ResellerID, #GWResellerID, #UserName, #Password, #URL, #ServiceType, #ServiceDesc, #FeedFrom, #PublicKey)
FETCH NEXT FROM gateway INTO #ResellerID
END
CLOSE gateway
DEALLOCATE gateway
My table name is tblGatewayConnection has these columns:
resellerID
gwResellerID
userName
password
url
serviceType
serviceDesc
feedFromMain
publicKey
While I insert data using the stored procedure, I get an exception
Cursorfetch: The number of variables declared in the INTO list must match that of selected columns.
What did I miss ?
Any help will be appreciated.
Thanks.

Why even bother with a cursor?!?!?!?!? I won't tell you what's wrong with your cursor - because instead of fixing the cursor, you should learn to avoid it in the first place!
Seriously - avoid RBAR (row-by-agonizing-row) processing whenever you can, and here, it's really utterly pointless to use a cursor - just use this nice and clean set-based statement:
ALTER PROCEDURE [dbo].[conn] #ResellerID INT,
#GWResellerID INT,
#UserName VARCHAR(50),
#Password VARCHAR(50),
#URL VARCHAR(100),
#ServiceType INT,
#ServiceDesc VARCHAR(50),
#FeedFrom BIT,
#PublicKey VARCHAR(max)
AS
INSERT INTO dbo.tblGatewayConnection
(ResellerID, GWResellerID, UserName, Password,
URL, ServiceType, ServiceDesc, feedFromMain,
publicKey)
SELECT
ResellerID, GWResellerID, UserName, Password,
URL, ServiceType, ServiceDesc, feedFromMain,
publicKey
FROM
dbo.reseller_profiles
WHERE
main_reseller_ID = #ResellerID
and you're done!! No messy cursor, no unnecessary local variables - just a simple INSERT ... SELECT and you've achieved what you want!

I am not sure the error message could be any more self explanatory:
Cursorfetch: The number of variables declared in the INTO list must match that of selected columns.
You are selecting all the columns from reseller_profiles
declare gateway cursor for
select *
from reseller_profiles
where main_reseller_ID = #ResellerID
And trying to put them into a single variable:
FETCH NEXT FROM gateway INTO #ResellerID
The number of columns you select in your cursor must match the number of variables you are inserting to, so you would need something like
declare gateway cursor for
select reseller_id
from reseller_profiles
where main_reseller_ID = #ResellerID
HOWEVER you should not be using a cursor for this, you can use the same thing using INSERT .. SELECT:
INSERT INTO [dbo].tblGatewayConnection
( [ResellerID],[GWResellerID], [UserName], [Password], [URL],
[ServiceType], [ServiceDesc],[feedFromMain], publicKey
)
SELECT Resellerid, #GWResellerID, #UserName, #Password,
#URL, #ServiceType, #ServiceDesc, #FeedFrom, #PublicKey
FROM reseller_profiles
WHERE main_reseller_ID = #ResellerID;
As has been said, you should avoid cursors at all costs, if you absolutely have to use a cursor, declare the most light weight cursor you can. In your case for example, you were only moving forwards within the cursor, only reading data, not modifying it, and only accessing the cursor locally, therefore you would declare the cursor as follows:
DECLARE gateway CURSOR LOCAL STATIC FAST_FORWARD
FOR
SELECT ...
FROM ..
Although cursors perform terribly at the best of times they are given an even worse reputation by lazy declarations.
Finally, You should get out of the habit of using SELECT *

Related

How to systematically get stored procedure (SP) parameter names and their values INSIDE the SP execution

As described in title, I am trying to systematically get stored procedure pararameter names and their corresponding values inside the execution of the proper stored procedure.
First point, which is taking stored procedure parameter names, is easy using table [sys].[all_parameters] and the stored procedure name. However, getting the actual values of these parameters is the difficult part, specially when you are not allowed to use table [sys].[dm_exec_input_buffer] (as a developer, I am not allowed to read this table, since it is a system administrator table).
Here is the code I have so far, which I am sure can serve you as a template:
CREATE PROCEDURE [dbo].[get_proc_params_demo]
(
#number1 int,
#string1 varchar(50),
#calendar datetime,
#number2 int,
#string2 nvarchar(max)
)
AS
BEGIN
DECLARE #sql NVARCHAR(MAX);
DECLARE #ParameterNames NVARCHAR(MAX) = ( SELECT STRING_AGG([Name], ',') FROM [sys].[all_parameters] WHERE OBJECT_ID = OBJECT_ID('[dbo].[get_proc_params_demo]') )
SET #sql = N'SELECT ' + #ParameterNames;
DECLARE GetParameterValues CURSOR FOR
SELECT DISTINCT [Name] FROM [sys].[all_parameters] WHERE OBJECT_ID = OBJECT_ID('[dbo].[get_proc_params_demo]');
OPEN GetParameterValues;
DECLARE #param_values NVARCHAR(MAX) = NULL
DECLARE #StoredProcedureParameter NVARCHAR(MAX)
FETCH NEXT FROM GetParameterValues INTO #StoredProcedureParameter;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #param_values = 'ISNULL('+#param_values+','')'+#StoredProcedureParameter+','
EXEC(#param_values)
FETCH NEXT FROM GetParameterValues INTO #StoredProcedureParameter;
END;
CLOSE GetParameterValues;
DEALLOCATE GetParameterValues;
SET #param_values = LEFT(#param_values, LEN(#param_values) - 1)
EXEC sp_executesql #sql,#ParameterNames,#param_values;
END
EXEC [dbo].[get_proc_params_demo]
#number1=42,
#string1='is the answer',
#calendar='2019-06-19',
#number2=123456789,
#string2='another string'
This is my approach trying to dynamically get parameter actual values inside a cursor, but it does not work, and I am clueless so far. I know it is quite rudimentary, and I am happy to hear other approaches. To be fair, I don't know if this problem is even possible to solve without system tables, but it would be great.
EDIT: This is an attempt to get a generic code that works on any stored procedure. You do not want to hardcode any parameter name. The only input you have is the stored procedure name via OBJECT_NAME(##PROCID)

escape special characters in stored procedure variables

I've made a stored procedure to get practice with cursors, I've a problem with special characters, for instance, if last_name contains a single quote, I've got an error, I need to escape it in some way, how could I do that? I don't know which special characters are contained in these fields, I've tried with QUOTENAME(d.last_name) but it didn't work
CREATE OR alter PROCEDURE list_employees
AS
BEGIN
DECLARE cursore CURSOR FAST_FORWARD FOR SELECT TOP(20) d.id, d.first_name, d.last_name, cd.contact
FROM employees d
JOIN contacts cd ON cd.fk_employee= d.id
ORDER BY d.id;
DECLARE #id_employee VARCHAR(36);
DECLARE #first_name VARCHAR(50);
DECLARE #last_name VARCHAR(50);
DECLARE #contact VARCHAR(255);
DECLARE #insert_statement varchar(1000);
IF OBJECT_ID('dbo.list_employees', 'U') IS NOT NULL
BEGIN
DROP TABLE dbo.list_employees;
END
OPEN cursore;
FETCH NEXT FROM cursore INTO #id_employee , #first_name , #cognome, #contatto ;
if(##FETCH_STATUS = 0)
BEGIN
CREATE TABLE dbo.list_employees(id_employee VARCHAR(36), first_name VARCHAR(50), last_name VARCHAR(50), contact VARCHAR(255))
END
WHILE ##FETCH_STATUS = 0
BEGIN
SET #insert_statement = 'INSERT INTO list_employees SELECT '''+#id_employee +''', '''+#first_name +''', '''+#last_name +''','''+ #contact +''''
exec(#insert_statement )
FETCH NEXT FROM cursore INTO #id_employee , #first_name , #last_name , #contact ;
END
CLOSE cursore;
DEALLOCATE cursore;
END;
Since your code drops an existing table and then recreates it I suspect this procedure is an odd way of getting the "current top 20". Instead of using a cursor and all sorts of hassle this would be massively simplified to use a view. There is no need to constantly drop a table and repopulate it.
Here is what your view might look like.
create or alter view list_employees as
SELECT TOP(20) d.id
, d.first_name
, d.last_name
, cd.contact
FROM employees d
JOIN contacts cd ON cd.fk_employee = d.id
ORDER BY d.id;
Firstly, let's cover off why what you have isn't working; this is because you are injecting values that should be parameters. Specifically these 2 lines are the cause:
SET #insert_statement = 'INSERT INTO list_employees SELECT '''+#id_employee +''', '''+#first_name +''', '''+#last_name +''','''+ #contact +''''
exec(#insert_statement )
There are, in truth, 2 bad habits here:
injection of non-sanitised values (a huge security vulnerability)
Use of EXEC(#SQL) syntax, rather than sys.sp_executesql, meaning you can't parametrise your statement.
If you parametrise your statement, then the problem you have goes away:
SET #insert_statement = 'INSERT INTO dbo.list_employees (id_employee,first_name,last_name,contact) VALUES (#id_employee,#first_name,#last_name,#contact);';
EXEC sys.sp_executesql #insert_statement, N'#id_employee VARCHAR(36),#first_name VARCHAR(50),#last_name VARCHAR(50),#contact VARCHAR(255)', #id_employee,#first_name,#last_name,#contact;
Of course, this poses the question of why use dynamic SQL at all, there's nothing dynamic about the statement. It's not that the table might not exist prior to the execution, as if a table doesn't exist the validation can be deferred by the engine, and you CREATE the table in the same scope. Perhaps the definition of the table is changing? I hope not.
As also mentioned, however, the CURSOR isn't really required here. Although you state you are practicing them, there are very few times that they are ever needed, and changing something like this to use an RBAR CURSOR will be terrible for performance. You really should be using a set based INSERT:
INSERT INTO dbo.list_employees (id_employee,first_name,last_name,contact)
SELECT TOP (20)
e.id,
e.first_name,
e.last_name,
c.contact
FROM dbo.employees e
JOIN dbo.contacts c ON c.fk_employee= e.id
ORDER BY e.id;
Or, better yet, use a VIEW as Sean demonstrates in their answer.

Using dynamic sql in a procedure to insert data into schema tables

I hope that you are all doing well.
I've been working on a project where I need to store data about my college that includes ID numbers, names, contact details etc.
I'm having a bit of difficulty in creating a stored procedure that will be able to insert data into a specified schema.table_name. The procedure must be able to allow the EXEC command to specify which schema you would like the insert data into. The table_name will stay the same for all the 14 schemas. The following code sample is what I have come up with but it doesn't seem to work:
CREATE PROCEDURE AddStudent_proc(#campus varchar(50), #StudentID numeric(4,0), #Name varchar(50), #Surname varchar(50), #ID_numeric numeric(13,0), #Address varchar(100))
AS
BEGIN
DECLARE #dynamic varchar(MAX)
SET #dynamic = 'INSERT INTO ['+quotename(#campus)+'].Student_tbl(
StudentID,
Name,
Surname,
ID_numeric,
Address
)
VALUES('+quotename(#StudentID)+','+quotename(#Name)+','+quotename(#Surname)+','+quotename(#ID_numeric)+','+quotename(#Address)+');'
EXEC (#dynamic);
END
GO
My entire structure can be found
here
I'd appreciate any help on this topic as I am still quite new to SQL as a whole.
Thanks in advance.
You don't need to use quotename for data - as the name of the function implies, it should be used with names (A.K.A identifiers).
Also, when you are using quotename it addeds [ and ] around the value it receives, so no point of adding them again (['+quotename(#campus)+'] in your code).
I would recommend three improvements to the procedure you have now:
Change the data type of #campus to sysname - this is a special data type synonym to nvarchar(128) not null used by SQL Server for all identifiers.
white-list the schema name.
This is a critical change to protect against SQL Injection attacks.
Anything that can't be parameterized needs to be white-listed.
use sp_ExecuteSql instead of EXEC
This will result in a better stored procedure because it eliminates the threat of SQL Injection.
I've written a couple of blog posts that adds some more information and background on this subject: The do’s and don’ts of dynamic SQL for SQL Server and Back to basics: SQL Injection.
Anyway, here's how I would write this procedure:
CREATE PROCEDURE AddStudent_proc(
#campus sysname,
#StudentID numeric(4,0),
#Name varchar(50),
#Surname varchar(50),
#ID_numeric numeric(13,0),
#Address varchar(100)
)
AS
BEGIN
IF EXISTS(
SELECT 1
FROM Sys.Schemas
WHERE name = #campus
)
BEGIN
DECLARE #dynamic nvarchar(4000),
#paramDefinition nvarchar(4000)
SELECT #dynamic = N'INSERT INTO '+ quotename(#campus) + N'.Student_tbl (
StudentID,
Name,
Surname,
ID_numeric,
Address
)
VALUES(#StudentID, #Name, #Surname, #ID_numeric, #Address)',
#paramDefinition =
N'#StudentID numeric(4,0),
#Name varchar(50),
#Surname varchar(50),
#ID_numeric numeric(13,0),
#Address varchar(100)'
EXEC sp_executeSql #dynamic, #paramDefinition, #StudentID, #Name, #Surname, #ID_numeric, #Address;
END
END
GO

Easiest Method for determining Active Directory groups and members in SQL

I need to write a report in SSRS (T-SQL) that shows any current user which reports on the SSRS report server they have read-access to, which is determined by Active Directory at the present. To complicate matters, the Active Directory doesn't have groups set up as group elements - all users in the AD are objectClass=User and objectCategory=Person.
My question is: how can I write a query that will match a user to all their "memberOf" elements without knowing necessarily what the group names are (since they might change, etc.)? From there, I think I can piece together how to match each element to the reports.
EDIT: Here's what I have written so far. It's not creating the procedure because of a syntax error, but I can't spot the error.
USE [ReportServer]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[ActiveDirectoryPermissions]
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Table1 TABLE
(
[GroupName] nvarchar(MAX),
[GroupPath] nvarchar(MAX)
)
INSERT INTO #Table1 ( [GroupName], [GroupPath] )
SELECT sAMAccountName as [GroupName], replace(ADsPath,'LDAP://','') as [GroupPath]
FROM OPENQUERY( ADSI,
'SELECT sAMAccountname, ADsPath
FROM ''LDAP://DC=[REDACTED],DC=COM''
WHERE objectCategory=''group'' AND CN=''*''
ORDER BY CN')
DECLARE #Table2 TABLE
(
[GroupPath] nvarchar(MAX),
[MemberName] nvarchar(MAX)
)
DECLARE table_1_cursor CURSOR FOR
SELECT GroupPath
FROM #Table1 t1
DECLARE #SQL nvarchar(MAX)
DECLARE #temp nvarchar(MAX)
OPEN table_1_cursor
FETCH NEXT FROM table_1_cursor INTO #temp
WHILE ##FETCH_STATUS = 0
BEGIN
SET #SQL = 'SELECT '''+#temp+''' AS GroupPath, cn
FROM OPENQUERY(ADSI,
''SELECT cn
FROM ''''LDAP://DC=[REDACTED],DC=com''''
WHERE
memberOf='''''+#temp+'''''
'')'
INSERT INTO #Table2 ( [GroupPath], [MemberName] )
EXEC sp_executesql #SQL;
FETCH NEXT FROM table_1_cursor INTO #temp
END
CLOSE table_1_cursor
DEALLOCATE table_1_cursor
SELECT *
FROM #Table2 t2
INNER JOIN #Table1 t1 ON (t2.GroupPath=t1.GroupPath)
GO
Comment out the contents of the stored procedure and create it. Then alter the sproc by uncommenting the statements one at a time. You can also try commenting out parts of the select statement. I suspect the problem is where you are building # sql. I would select # temp and # sql at this point. Running the code directly rather than as part of a a procedure. That way you can manually check and test the output. Well done for persevering this far. Those ' would have driven me mad.
You are missing the final END on the stored procedure. Next problem: What is ADSI ? I presume that is the name of a linked server that exposes AD? As in
https://www.mssqltips.com/sqlservertip/2580/querying-active-directory-data-from-sql-server/
There is an alternative method of accessing AD that does not require the linked server.
EXEC master.dbo.sp_QueryAD
'SELECT sAMAccountname, ADsPath
FROM ''LDAP://OU=REDACTED,DC=REDACTED''
WHERE objectCategory=''group'' AND CN=''*''
ORDER BY CN'
There is a small amount of config to enable this, but if you search on the error, it only takes a moment to set up.

sp_msforeachtable performing actions on variables

I am trying to figure out how to use sp_msforeachtable to perform an action on all tables and variables that match variable/table names stored in another table
IE
I have a table that has 3 columns : table, variable, action
and I am trying to use sp_MSforeachtable to see which tables and variables match, and if match, perform that action on the table.
How do you call variable names in the sp_MSforeachtable statement? I know to use ? for the table name, but not sure how I would say if variable name=variable name then do X
Is there another way to do this without using this undocumented SP?
Ill try to explain better:
I am trying to clean personal info from a bunch of tables... I have a table that looks like this (not sure how to format a table, so imagine each entry is a seperate row, so the first row is Name, A, and set to '')
Variable
Name
Phone Number
Name
Table
A
A
B
Action
Set to ''
Set to '555-555-5555'
Set to ''
etc.
I then have a database full of tables....on table A, I would want my code to set all rows of variable 'Name'
to '' (blank)
, and Phone Number to '555-555-5555'
etc.and then move on to table B and do the same and so on
I would use a cursor and dynamic SQL:
--Set up for test:
CREATE TABLE #DataTable (column1 nvarchar(128) NOT NULL, column2 int NOT NULL); --Create global temp table so it can be accessed from dynamic SQL.
CREATE TABLE ##ActionTable ([table] nvarchar(128) NOT NULL, variable nvarchar(MAX) NOT NULL, [action] nvarchar(MAX) NOT NULL);
INSERT INTO ##ActionTable ([table], variable, [action])
VALUES
('#DataTable', '1', 'INSERT INTO #table (column1, column2) VALUES (''#variable_1'', #variable);'),
('#DataTable', '2', 'INSERT INTO #table (column1, column2) VALUES (''#variable_1'', #variable);'),
('#DataTable', '3', 'INSERT INTO #table (column1, column2) VALUES (''#variable_1'', #variable);'),
('#DataTable', '4', 'INSERT INTO #table (column1, column2) VALUES (''#variable_1'', #variable);');
--Code:
DECLARE #action nvarchar(MAX);
DECLARE #table nvarchar(128);
DECLARE #variable nvarchar(MAX);
DECLARE rowCurser CURSOR FOR SELECT [table], variable, [action] FROM ##ActionTable;
OPEN rowCurser;
FETCH rowCurser INTO #table, #variable, #action
WHILE ##FETCH_STATUS = 0
BEGIN
--Execute the code (pick one of the two. Option 2 is safer and can be cached (faster), but it does not work with my example because the parameters are left as variables).
-- Option 1:
SET #action = REPLACE(REPLACE(#action, '#table', #Table), '#variable', #variable);
EXECUTE(#action);
-- Option 2:
EXECUTE sp_executesql #stmt = N'INSERT INTO #DataTable (column1, column2) VALUES (CAST(#variable as nvarchar(128)) + N''_2'', #variable);', #params = N'#variable nvarchar(MAX)', #variable = #variable;
--Setup for next iteration
FETCH rowCurser INTO #table, #variable, #action
END
CLOSE rowCurser;
DEALLOCATE rowCurser;
--Check and cleanup from test
SELECT * FROM #DataTable;
DROP TABLE #DataTable;
DROP TABLE ##ActionTable;
Note: There are security concerns with what you are trying to do, since anyone who can add to your table will have the same access as the account which runs the script. You could reduce these concerns by defining the actions in another table which can only be edited by the administrator, then referencing the action in your existing table.
Note: It is best to have the data types of #action, #table, and #variable match their source columns. The variables an be any data type in your database (as long as it is not a local temp type). You will notice that there are two places in the code above where the types are defined, first where the variables are declared at the top, and second where the arguments for sp_executesql are defined in the string near the bottom.
Note: if #stmt and #params are assigned with a constant instead of a variable, make sure to prefix the constant with N so it will be read as a Unicode string.

Resources