Get value depending of current iteration on while loop - sql

Currently I'm using SQL Server Management Studio v18, and I want to iterate a Table Type using a while loop, so I do:
CREATE TYPE [VarcharIdTableType] AS TABLE
(
[FileName] VARCHAR(MAX)
)
Then I use a while loop as:
DECLARE #FileTableType [VarcharIdTableType]
INSERT INTO #FileTableType
VALUES ('test1'),('test2'),('test3')
DECLARE #FileCount INT = (SELECT COUNT(*) FROM #FileTableType), #RowCount INT = 0
WHILE #RowCount <= #FileCount
BEGIN
DECLARE #Content VARCHAR(555)= CONCAT('File name ', #filename)
SET #RowCount = #RowCount + 1;
END
So I want to get each FileName depending of iteration number, how can I achieve that?

IMPORTANT: Do you actually need a loop? As you haven't shown us what your desired final outcome is we are unable to comment. But in many cases a set based approach will accomplish what you need and if so it should be used.
Making a few assumptions, such as no ID column in your table, and being able to delete rows from your table variable, I would do the following using exists rather than counting rows.
DECLARE #FileTableType TABLE ([Name] VARCHAR(MAX));
INSERT INTO #FileTableType([Name]) VALUES ('test1'), ('test2'), ('test3');
DECLARE #Content VARCHAR(MAX) = '', #Filename VARCHAR(MAX);
WHILE EXISTS (SELECT 1 FROM #FileTableType)
BEGIN
SELECT TOP 1 #Filename = [Name] FROM #FileTableType;
SET #Content = CONCAT('File number ', #Filename);
DELETE FROM #FileTableType WHERE [Name] = #Filename;
END;
However having a known unique identifier on the values in the table variable would be better because if you have duplicate filenames the first code will have issues.
DECLARE #FileTableType TABLE (id INT IDENTITY(1,1), [Name] VARCHAR(MAX));
INSERT INTO #FileTableType([Name]) VALUES ('test1'), ('test2'), ('test3');
DECLARE #Content VARCHAR(MAX) = '', #Filename VARCHAR(555), #FileId INT;
WHILE EXISTS (SELECT 1 FROM #FileTableType)
BEGIN
SELECT TOP 1 #FileId = id, #Filename = [Name] FROM #FileTableType;
SET #Content = CONCAT('File number ', #Filename);
DELETE FROM #FileTableType WHERE id = #FileId;
END;
Note the best practices:
Terminating all statements with a semi-colon
Listing all columns when inserting
Using VARCHAR(MAX) for #Content since the filename is.

Related

How to fetch the code comments from a stored procedure / function and populate to a table?

How to fetch the code comments from a stored procedure / function and populate to a table?
/*
Author : Test
Comment : Test
*/
I am working on a user defined function by passing either a stored procedure or function as input parameter to read the code history comments and store it in a table. Having the detail in a table to maintain the version notes for the input.
Check this, there are different ways to get the definition, I prefer sp_helptext because it's already splitted in lines
DECLARE #Objects TABLE(name varchar(100))
DECLARE #Lines TABLE(id int identity, line varchar(maX))
INSERT #Objects
SELECT name FROM sys.objects WHERE Type in ('FN', 'IF', 'P', 'TR', 'TF')
DECLARE #ObjectName VARCHAR(100)
WHILE EXISTS (SELECT 1 FROM #Objects)
BEGIN
SELECT TOP 1 #ObjectName = name FROM #Objects
DELETE #Lines
INSERT #Lines (line)
exec sp_helptext #ObjectName
DECLARE #Linestart INT, #LineEnd INT
WHILE EXISTS(SELECT 1 FROM #Lines WHERE charindex('/*', line) > 0)
BEGIN
SELECT TOP 1 #Linestart = id
FROM #Lines WHERE charindex('/*', line) > 0
ORDER BY id
SELECT TOP 1 #LineEnd = id
FROM #Lines WHERE charindex('*/', line) > 0
ORDER BY id
DECLARE #comment VARCHAR(MAX) = ''
SELECT #Coment = #coment + char(13) + char(10) + line
FROM #Lines
WHERE id between #LineStart and #lineEnd
INSERT INTO yourtable (#objectName, #Comment)
DELETE #Lines WHERE id between #LineStart and #lineEnd
END
DELETE #Objects WHERE name = #ObjectName
END
You can create a function/stored procedure to achieve this:
CREATE FUNCTION InsertCommentIntoTable
(
#Param1 VARCHAR(200)
)
RETURNS int
AS
BEGIN
-- Declare the return variable here
DECLARE #str VARCHAR(max)
SELECT #str = definition
FROM sys.sql_modules
WHERE object_id = (OBJECT_ID(N'dbo.CustOrderHist'));
--parse #str string value and do your stuffs: #str has the function and stored procedure codes.
RETURN 0;
END
GO

Inserting multiple rows through a variable

Actually I want to insert multiple rows to a table.
The Structure of the table is
Create Table tbl_username
(id int autoincrement,
username varchar(100),
Primary key(id))
and I tried to insert multiple rows like
Declare #s as varchar(100)
set #s='(''name1''),(''name2'')'
insert into tbl_username(username)values #s;
but I get the output as
id username
1 (''name1''),(''name2'')
Actually the required output for me is
id username
1 name1
2 name2
How can I achieve this?
Use dynamic SQL
Declare #s as varchar(100)
Declare #sql as varchar(max)
set #s='(''name1''),(''name2'')'
set #sql = 'insert into tbl_username(username) values ' + #s;
execute(#sql);
However I would avoid dynamic SQL where possible.
The standard way to do it if your values are not in a variable is:
INSERT INTO tbl_username(username) values ('name1'),('name2')
Or
INSERT INTO tbl_username(username) values ('name1')
INSERT INTO tbl_username(username) values ('name2')
If possible, opt for one of the above instead of the dynamic option originally mentioned.
insert into tbl_username(username)values ('name1'),('name2'),.....;
Here because username is of type varchar so it's considering #s as single value and inserting it in one row.
The below logic makes use of substring feature:
DECLARE #s as varchar(100), #Delimiter VARCHAR(1)
SET #s = 'name1,name2'
SET #Delimiter = ','
DECLARE #Position INT, #ListItem VARCHAR(MAX)
WHILE CHARINDEX(#Delimiter, #s) > 0
BEGIN
SELECT #Position = CHARINDEX(#Delimiter, #s)
SELECT #ListItem = SUBSTRING(#s, 1, #Position-1)
INSERT INTO tbl_username
SELECT #ListItem
SELECT #s = SUBSTRING(#s, #Position+1, LEN(#s)-#Position)
END
INSERT INTO tbl_username
Select #s
SELECT * FROM tbl_username
Please try: http://sqlfiddle.com/#!6/d0f76/1/0

SQL Server: How to achieve re-usability yet flexibility in TSQL

I am using SQL Server 2008 R2. I am having some problems finding an effective coding pattern for SQL which supports code re-usability as well as flexibility. By re-usability, what I mean is keeping SQL queries in Stored Procedures and User Defined Functions.
Now, if I choose Stored Procedures, I will be sacrificing its usability in a query directly. If I choose User Defined Functions, I won't be able to use DML statements.
For example, suppose I created a Stored Procedures which inserts one contact record. Now, if I am having a table which can act as a source of multiple contact records, all I am left with are either WHILE loops or CURSORs, which is clearly not a recommended option, due to its performance drawbacks. And due to the fact that DML statements are not allowed in User Defined Functions, I simply cannot use them for this purpose.
Although, If I am not concerned with code re-usability, then instead of using Stored Procedures I can surely use same set of queries again and again to avoid while loops.
What pattern should I follow?
Here is a similar Stored Procedures:-
ALTER Proc [dbo].[InsertTranslationForCategory]
(
#str nvarchar(max),
#EventId int,
#CategoryName NVarchar(500),
#LanguageId int,
#DBCmdResponseCode Int Output,
#KeyIds nvarchar(max) Output
)as
BEGIN
DECLARE #XmlData XML
DECLARE #SystemCategoryId Int
DECLARE #CategoryId Int
Declare #Counter int=1
Declare #tempCount Int
Declare #IsExists int
Declare #TranslationToUpdate NVarchar(500)
Declare #EventName Varchar(200)
declare #Locale nvarchar(10)
declare #Code nvarchar(50)
declare #KeyName nvarchar(200)
declare #KeyValue nvarchar(500)
select #Locale=locale from languages where languageid = #LanguageId
SET #DBCmdResponseCode = 0
SET #KeyIds = ''
select #EventName = eventName from eventLanguages
where eventID = #EventId
--BEGIN TRY
Select #SystemCategoryId=CategoryId from SystemCategories where Name=rtrim(ltrim(#CategoryName))
Select #CategoryId=CategoryId from Categories where Name=rtrim(ltrim(#CategoryName)) and EventId=#EventId
if (#str='deactivate')
Begin
Delete from Codetranslation where CategoryId=#CategoryId
Update Categories set [Status]=0, Isfilter=0 where CategoryId=#CategoryId and Eventid=#EventId
Set #DBCmdResponseCode=2
return
End
set #XmlData=cast(#str as xml)
DECLARE #temp TABLE
(
Id int IDENTITY(1,1),
Code varchar(100),
Translation varchar(500),
CategoryId int
)
Insert into #temp (Code,Translation,CategoryId)
SELECT
tab.col.value('#Code', 'varchar(200)'),
tab.col.value('#Translation', 'varchar(500)'),#SystemCategoryId
FROM #XmlData.nodes('/Data') AS tab (col)
select #tempCount=Count(*) from #temp
if(IsNull(#CategoryId,0)>0)
Begin
While (#Counter <= #tempCount)
Begin
Select #IsExists= count(sc.categoryid) from #temp t Inner Join SystemCodetranslation sc
On sc.categoryid=t.CategoryId
where ltrim(rtrim(sc.code))=ltrim(rtrim(t.code)) and ltrim(rtrim(sc.ShortTranslation))=ltrim(rtrim(t.Translation))
and t.Id= #Counter
print #IsExists
Select #Code = Code , #KeyValue = Translation from #temp where id=#counter
set #KeyName = ltrim(rtrim(#EventName)) + '_' + ltrim(rtrim(#CategoryName)) + '_' + ltrim(rtrim(#Code)) + '_LT'
exec dbo.AddUpdateKeyValue #EventId,#Locale, #KeyName,#KeyValue,NULL,12
select #KeyIds = #KeyIds + convert(varchar(50),keyvalueId) + ',' from dbo.KeyValues
where eventid = #EventId and keyname = #KeyName and locale = #Locale
set #KeyName = ''
set #KeyValue = ''
Set #Counter= #Counter + 1
Set #IsExists=0
End
End
--- Inser data in Codetranslation table
if(isnull(#CategoryId,0)>0)
Begin
print #CategoryId
Delete from codetranslation where categoryid=#CategoryId
Insert into codetranslation (CategoryId,Code,LanguageId,ShortTranslation,LongTranslation,SortOrder)
SELECT
#CategoryId,
tab.col.value('#Code', 'varchar(200)'), #LanguageId,
tab.col.value('#Translation', 'varchar(500)'),
tab.col.value('#Translation', 'varchar(500)'),0
FROM #XmlData.nodes('/Data') AS tab (col)
Update Categories set [Status]=1 where CategoryId=#CategoryId and Eventid=#EventId
End
Set #DBCmdResponseCode=1
set #KeyIds = left(#KeyIds,len(#KeyIds)-1)
END
You can use table variable parameter for your user defined functions.
following code is an example of using table variable parameter in stored procedure.
CREATE TYPE IdList AS TABLE (Id INT)
CREATE PROCEDURE test
#Ids dbo.IdList READONLY
AS
Select *
From YourTable
Where YourTable.Id in (Select Id From #Ids)
End
GO
In order to execute your stored procedure use following format:
Declare #Ids dbo.IdList
Insert into #Ids(Id) values(1),(2),(3)
Execute dbo.test #Ids
Edit
In order to return Inserted Id, I don't use from Table Variable Parameter. I use following query sample for this purpose.
--CREATE TYPE NameList AS TABLE (Name NVarChar(100))
CREATE PROCEDURE test
#Names dbo.NameList READONLY
AS
Declare #T Table(Id Int)
Insert Into YourTable (Name)
OUTPUT Inserted.Id Into #T
Select Name
From #Names
Select * From #T
End
GO

retrieving all Foreign Keys and their records

What I'm trying to achieve is the following.
When I delete a record I want to check if there are any FK relationships and it needs to be recursive. That way I can display a list of all records that are related to the one you want to delete.
So a small example of nested links
project 1 -> phase 1 -> block 1 -> ..
So when I try to delete project 1 I need to get a list of the items you need to delete first:
phase 1
block 1
....
I wanted to do this with a stored procedure that takes an ID and a tablename (format [chema].[tablename]) and finds all these linked records.
The problem I'm having is with the recursive part.
Here's my code so far:
ALTER PROCEDURE core.usp_CanBeDeleted
#entityId int,
#entityName nvarchar(250)
AS
BEGIN
DECLARE #NumberRecords int, #RowCount int
DECLARE #childId int
DECLARE #query nvarchar(max)
DECLARE #eName nvarchar(250) , #keyName nvarchar(250)
DECLARE #columnName nvarchar(250)
DECLARE #keys TABLE(
RowID int IDENTITY(1, 1),
name nvarchar(250),
entityName nvarchar(250),
columnName nvarchar(250)
)
if not exists (select * from sysobjects where name='partialResults' and xtype='U')
BEGIN
CREATE TABLE partialResults(
RowID int IDENTITY(1, 1),
id int,
parentId int,
name nvarchar(250),
FK_name nvarchar(250)
)
END
DECLARE #recusiveResults TABLE(
RowID int,
id int,
parentId int,
name nvarchar(250),
FK_name nvarchar(250)
)
DECLARE #results TABLE(
RowID int,
id int,
parentId int,
name nvarchar(250),
FK_name nvarchar(250)
)
SET #RowCount = 1
-- get all FK's of the entity
INSERT INTO #keys
SELECT name, '[' + OBJECT_SCHEMA_NAME(parent_object_id) + '].[' + OBJECT_NAME(parent_object_id)+ ']',cu.column_name
from sys.foreign_keys k
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE CU
ON k.name = CU.CONSTRAINT_NAME
where k.referenced_object_id = OBJECT_ID(#entityName)
-- set variable to number of records in temp table
SET #NumberRecords = ##ROWCOUNT
-- loop through the FK's an get all linked entities
WHILE(#RowCount <= #NumberRecords)
BEGIN
SELECT #keyName = name, #eName = entityName, #columnName = columnName
FROM #keys
WHERE RowId = #RowCount
-- get all FK information
SET #query = 'INSERT INTO partialResults(FK_name, name, id, parentId)'
+ ' SELECT ''' + #keyName + ''','''+ #eName + ''',' + 'id,' + cast(#entityId as varchar(25)) + ' as parentid'
+ ' FROM ' +#eName
+ ' WHERE id in '
+ ' (SELECT ' + #columnName
+ ' FROM ' + #entityName
+ ' WHERE id = ' + cast(#entityId as varchar(25))
+ ' )'
--print #query
EXEC (#query)
SET #RowCount = #RowCount + 1
END
-- rest number of records
SET #RowCount = 1
SELECT #NumberRecords = count(id)
FROM partialResults
-- save partialResults
INSERT INTO #results--(FK_name, name, id, parentId)
SELECT *--FK_name, name, id, parentId
FROM partialResults
DELETE FROM partialResults
WHILE(#RowCount <= #NumberRecords)
BEGIN
-- select next row
SELECT #childId = id, #eName = name
FROM #results
WHERE RowId = #RowCount
INSERT INTO #recusiveResults
EXEC core.usp_CanBeDeleted #childId, #eName
SET #RowCount = #RowCount + 1
END
INSERT INTO #results
SELECT *
FROM #recusiveResults
if exists (select * from sysobjects where name='partialResults' and xtype='U')
BEGIN
-- remove temp tables
DROP TABLE partialResults
END
-- return results
SELECT *
FROM #results
END
GO
the problem lies here:
INSERT INTO #recusiveResults
EXEC core.usp_CanBeDeleted #childId, #eName
Apparantly you can't nest an insert exec.
however I don't really see any other way to do it.
I've tried converting it into a function but then there are other problems like the dynamic query.
Any help would be greatly apreciated.
Split the procedure into an outer and an inner procedure.
In the outer procedure create a #results temp-table and then call the inner procedure.
In the inner procedure put all the logic including the recursion, but instead of selecting out the result at the end insert the result into the already existing #results table.
That way you safe a lot of time because you dont have to move data around as much. You also don't have to nest INSERT...EXEC anymore.
You also don't need the dbo.PartialResults table anymore as you can write directly into the #results table within the dynamic statement. If you still need it, to make the recursion work replace it with a #partialResults temp table that you create in the inner procedure (DON'T check for existence, just create the new one. See http://sqlity.net/en/1109/temp-tables-scoping-eclipsing/ for an explanation of temp table scoping). That way each execution is creating its own temp table and you don't have to deal with the clean-up. This is also a little less heavy compared to using a real table.
Finally, all the table variables can go too.
At the end of the inner procedure you can then do a simple SELECT * FROM #results; to output all the collected results.

Loops within dynamic SQL

I have code that I'd like to apply to a number of tables but rather than simply copy and replace table names, I'd like to use some kind of loop or cursor to simplify things.
I envision setting up an array of my tables names and using an index to iterate over the list, retrieving each table name and using dynamic SQL to intersperse the table name where applicable in my code.
Since there's no 'array' construct, as far as I know, within SQL, I'm not sure how this would work.
Any ideas about how to go about this?
Here is one way of doing it:
--Declare a table variable to hold your table names (and column names in case needed)
declare #listOfTablesToUpdate table (tableName varchar(100), columnNameToUpdate varchar(50))
--insert the tables that you want to work with.
insert into #listOfTablesToUpdate values ('Table1', 'column2')
insert into #listOfTablesToUpdate values ('Table2', 'column3')
insert into #listOfTablesToUpdate values ('Table3', 'column4')
--Cursor for iterating
declare #tableCursor cursor,
#tableName varchar(100),
#columnName varchar(50)
set #tableCursor = cursor for select * from #listOfTablesToUpdate
open #tableCursor
fetch next from #tableCursor into #tableName, #columnName
while(##fetch_status = 0)
begin
--dynamic sql
declare #sql varchar(max)
--Your logic here...this is just an example
set #sql = 'update '+#tableName+' set '+#columnName+' = '+<value>+' where '+#columnName +' = '+<someothervalue>
exec #sql
fetch next from #tableCursor into #tableName, #columnName
end
close #tableCursor
deallocate #tableCursor
Another approach involves preparing a helper function and a procedure that allow one to apply different SQL statements to each object (table, database, et cetera) in a list. The helper function comes from a SSRS Parameter question and splits apart a comma delimited list into a table.
-- from https://stackoverflow.com/questions/512105/passing-multiple-values-for-a-single-parameter-in-reporting-services
CREATE FUNCTION [dbo].[fn_MVParam]
(#RepParam NVARCHAR(4000), #Delim CHAR(1)= ',')
RETURNS #Values TABLE (Param NVARCHAR(4000))AS
BEGIN
DECLARE #chrind INT
DECLARE #Piece NVARCHAR(100)
SELECT #chrind = 1
WHILE #chrind > 0
BEGIN
SELECT #chrind = CHARINDEX(#Delim,#RepParam)
IF #chrind > 0
SELECT #Piece = LEFT(#RepParam,#chrind - 1)
ELSE
SELECT #Piece = #RepParam
INSERT #Values(Param) VALUES(CAST(#Piece AS VARCHAR))
SELECT #RepParam = RIGHT(#RepParam,LEN(#RepParam) - #chrind)
IF LEN(#RepParam) = 0 BREAK
END
RETURN
END
GO
Below is the code for the ProcessListSQL procedure.
-- #SQL to execute shall include {RP} as the replacement expression that
-- will evaluate to all the items in the comma delimited list
-- Also, please include a double quote " rather than two single quotes ''
-- in the input statement.
CREATE PROCEDURE [dbo].[ProcessListSQL] (
#CommaDelimitedList AS NVARCHAR(MAX),
#SQLtoExecute AS NVARCHAR(MAX) )
AS BEGIN
DECLARE #Statements TABLE
( PK INT IDENTITY(1,1) PRIMARY KEY,
SQLObject NVARCHAR (MAX)
)
SET #SQLtoExecute = REPLACE (#SQLtoExecute, '"', '''')
INSERT INTO #Statements
SELECT PARAM FROM [dbo].[fn_MVParam](#CommaDelimitedList,',')
DECLARE #i INT
SELECT #i = MIN(PK) FROM #Statements
DECLARE #max INT
SELECT #max = MAX(PK) FROM #Statements
DECLARE #SQL AS NVARCHAR(MAX) = NULL
DECLARE #Object AS NVARCHAR(MAX) = NULL
WHILE #i <= #max
BEGIN
SELECT #Object = SQLObject FROM #Statements WHERE PK = #i
SET #SQL = REPLACE(#SQLtoExecute, '{RP}', #Object)
-- Uncommend below to check the SQL
-- PRINT #SQL
EXECUTE sp_executesql #SQL
SELECT #Object = NULL
SELECT #SQL = NULL
SET #i = #i + 1
END
END
GO
The ProcessListSQL procedure take two parameters. The first is a comma delimited string that contains the list of objects that will be cycled through. The second parameter is a string that contains the SQL that will be executed with each of the objects in the first parameter.
In the below example, four databases are created. Note that {rp} is replaced with each of the objects in the first parameter and double quotes are needed in each place where single quotes are needed in the SQL statement.
EXECUTE ProcessListSQL 'rice,apples,cheese,tomatos',
'CREATE DATABASE [{rp}] CONTAINMENT = NONE
ON PRIMARY ( NAME = N"{rp}",
FILENAME = N"D:\data\user\{rp}.mdf" ,
SIZE = 4096KB ,
FILEGROWTH = 1024KB )
LOG ON
( NAME = N"{rp}_log",
FILENAME = N"D:\DATA\USER\{rp}_log.ldf" ,
SIZE = 1024KB ,
FILEGROWTH = 10%)'