Select / Delete of Three self referencing many to many relationships SQL Query - tsql

I have a table ProjectLayerContent designed as following:
ProjectLayerContentID [INT]
ProjectLayerContentName [NVARCHAR(200)]
ProjectLayerContentParentID [INT]
ScopeIsProjectLayerContent [BIT]
ScopeIsProjectLayerContentID [INT]
DataTypeIsContentOfContent [BIT]
ContentOfContentID [INT]
The project layer content might have a parent, and might not, also the same for the children.
The project layer content scope might include another project layer content.
The project layer content data type might be a type of another content.
What I am trying to do is in the delete process, I want to show a list includes the count of what's is going to be deleted if you will delete this project layer content.
Pretty much this is what I got into, but I know those results isn't right
DECLARE #ProjectLayerContentID INT = 1;
DECLARE #resTable TABLE (
ProjectLayerContentIDP int,
ProjLContentType nvarchar(100));
DECLARE #ProjectLayerContentIDRecursionParam int;
DECLARE #Type NVARCHAR(MAX);
DECLARE #resultString NVARCHAR(MAX) = '';
-- SELECT Project layer content children with recursion
-- SELECT the count of the result from the recursion of the children of the project layer content id passed with the sql parameter in the query
DECLARE ProjectLayerContentChildren_Cursor CURSOR FOR
SELECT ProjectLayerContentID, 'Child(ren)'
FROM ProjectLayerContent
WHERE ProjectLayerContentParentID = #ProjectLayerContentID
OPEN ProjectLayerContentChildren_Cursor
FETCH NEXT FROM ProjectLayerContentChildren_Cursor INTO #ProjectLayerContentIDRecursionParam, #Type
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO #resTable SELECT #ProjectLayerContentIDRecursionParam, #Type;
;WITH ProjectLayerContentTotalScopeChildren AS(
SELECT ProjectLayerContentID,CAST('Content(s) of Child(ren)' as varchar(259)) AS ProjLContentType
FROM ProjectLayerContent
WHERE ContentOfContentID = #ProjectLayerContentIDRecursionParam
UNION ALL
SELECT Scopechildren.ProjectLayerContentID, CAST('Children Content Of Content of Child(ren)' as varchar(259)) AS ProjLContentType
FROM ProjectLayerContent Scopechildren INNER JOIN
ProjectLayerContentTotalScopeChildren projLContents ON Scopechildren.ContentOfContentID = projLContents.ProjectLayerContentID
UNION ALL
SELECT children.ProjectLayerContentID, CAST('Child(ren) of Child(ren) of Child(ren)' as varchar(259)) AS ProjLContentType
FROM ProjectLayerContent children INNER JOIN
ProjectLayerContentTotalScopeChildren projLContents ON children.ProjectLayerContentParentID = projLContents.ProjectLayerContentID
)
INSERT INTO #resTable SELECT ProjectLayerContentID, ProjLContentType from ProjectLayerContentTotalScopeChildren
FETCH NEXT FROM ProjectLayerContentChildren_Cursor INTO #ProjectLayerContentIDRecursionParam, #Type
END
-- close the cursor
CLOSE ProjectLayerContentChildren_Cursor
DEALLOCATE ProjectLayerContentChildren_Cursor
-----------------------------------------------------------------------------------------------------------
-- SELECT Project layer content Scope children with recursion
DECLARE ProjectLayerContent_Cursor CURSOR FOR
SELECT ProjectLayerContentID, 'Content Of Content'
FROM ProjectLayerContent
WHERE ContentOfContentID = #ProjectLayerContentID
OPEN ProjectLayerContent_Cursor
FETCH NEXT FROM ProjectLayerContent_Cursor INTO #ProjectLayerContentIDRecursionParam, #Type
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO #resTable SELECT #ProjectLayerContentIDRecursionParam, #Type;
;WITH ProjectLayerContentTotalScopeContentOfContent AS(
SELECT ProjectLayerContentID,CAST('Children Content Of Content' as varchar(259)) AS ProjLContentType
FROM ProjectLayerContent
WHERE ContentOfContentID = #ProjectLayerContentIDRecursionParam
UNION ALL
SELECT Scopechildren.ProjectLayerContentID, CAST('Children Content Of Content of Children Content Of Content' as varchar(259)) AS ProjLContentType
FROM ProjectLayerContent Scopechildren INNER JOIN
ProjectLayerContentTotalScopeContentOfContent projLContents ON Scopechildren.ContentOfContentID = projLContents.ProjectLayerContentID
UNION ALL
SELECT children.ProjectLayerContentID, CAST('Children of Children Content Of Content' as varchar(259)) AS ProjLContentType
FROM ProjectLayerContent children INNER JOIN
ProjectLayerContentTotalScopeContentOfContent projLContents ON children.ProjectLayerContentParentID = projLContents.ProjectLayerContentID
)
INSERT INTO #resTable SELECT ProjectLayerContentID, ProjLContentType from ProjectLayerContentTotalScopeContentOfContent
FETCH NEXT FROM ProjectLayerContent_Cursor INTO #ProjectLayerContentIDRecursionParam, #Type
END
-- close the cursor
CLOSE ProjectLayerContent_Cursor
DEALLOCATE ProjectLayerContent_Cursor
--select the result to present it.
SELECT * FROM #resTable;
Thanks in advance.

I found that this solution to get the needed functionality as needed.
I created the stored procedure to get all children for this project layer first, and created a table to add the result in. Because one of the first error to apply this solution was INSERT INTO EXEC wouldn't work if you have a chain of stored procedures calling each other. It only works for the first time, but the other times will show an error.
DECLARE #ProjectLayerContentID INT = 1;
DECLARE #ProjectLayerContentIDRecursionParam INT;
DECLARE #Type NVARCHAR(MAX);
DECLARE #resultString NVARCHAR(MAX) = '';
-------Create temp table to insert the results into it
CREATE TABLE #ResTable(
ProjectLayerContentIDP int,
ProjLContentType nvarchar(100));
Then I created a cursor for the select statement of all children who have that relationship of being child of this project layer, or being content of it.
-- SELECT Project layer content children with recursion
-- SELECT the count of the result from the recursion of the children of the project layer content id passed with the sql parameter in the query
DECLARE ProjectLayerContent_Cursor CURSOR FOR
SELECT ProjectLayerContentID, CASE WHEN ProjectLayerContentParentID = #ProjectLayerContentID THEN 'Child(ren)' ELSE 'Content(s) of Content(s)' END
FROM ProjectLayerContent
WHERE ProjectLayerContentParentID = #ProjectLayerContentID
OR ContentOfContentID = #ProjectLayerContentID
OPEN ProjectLayerContent_Cursor
FETCH NEXT FROM ProjectLayerContent_Cursor INTO #ProjectLayerContentIDRecursionParam, #Type
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO #ResTable SELECT #ProjectLayerContentIDRecursionParam, #Type;
Then I will call the recursion stored procedure to get each of the current project layer content chlidren, and content of the children.
Exec DeleteCheck_ProjectLayerContentChildren #ProjectLayerContentIDRecursionParam;
FETCH NEXT FROM ProjectLayerContent_Cursor INTO #ProjectLayerContentIDRecursionParam, #Type
END
-- close the cursor
CLOSE ProjectLayerContent_Cursor
DEALLOCATE ProjectLayerContent_Cursor
At this part here, we will select the results, and then drop the temp table from the database server.
--select the result to present it.
SELECT #resultString = #resultString + '<br/><u>' + CONVERT(NVARCHAR(MAX), Count(ProjectLayerContentIDP)) + '</u> ' + ProjLContentType
FROM #ResTable
GROUP BY ProjLContentType;
SELECT #resultString;
DROP TABLE #ResTable
Here is the recursion stored procedure that we called already in the previous query
CREATE PROCEDURE [dbo].[DeleteCheck_ProjectLayerContentChildren]
#ProjectLayerContentID INT
AS
BEGIN
DECLARE #ProjectLayerContentIDRecursionParam INT;
DECLARE #Type NVARCHAR(MAX);
-- SELECT Project layer content children with recursion
-- SELECT the count of the result from the recursion of the children of the project layer content id passed with the sql parameter in the query.
Notice here we added Local into the deceleration of the cursor, because it will show an error of creating the cursor with the same name
DECLARE ProjectLayerContentChildren_Cursor CURSOR LOCAL FOR
SELECT ProjectLayerContentID, CASE WHEN ProjectLayerContentParentID = #ProjectLayerContentID THEN 'Child(ren) of Child(ren)' ELSE 'Child(ren) Content(s) of Content(s)' END
FROM ProjectLayerContent
WHERE ProjectLayerContentParentID = #ProjectLayerContentID
OR ContentOfContentID = #ProjectLayerContentID
Pretty much most of this query is the same, as the previous one, but this is for the project layer content children of children counts, and we don't create the result table again because it will be already created, then we just add the result, and we have the needed functionality, and you can apply it to more than two or three self relationships.
OPEN ProjectLayerContentChildren_Cursor
FETCH NEXT FROM ProjectLayerContentChildren_Cursor INTO #ProjectLayerContentIDRecursionParam, #Type
WHILE ##FETCH_STATUS = 0
BEGIN
INSERT INTO #ResTable SELECT #ProjectLayerContentIDRecursionParam, #Type;
Exec DeleteCheck_ProjectLayerContentChildren #ProjectLayerContentIDRecursionParam
FETCH NEXT FROM ProjectLayerContentChildren_Cursor INTO #ProjectLayerContentIDRecursionParam, #Type
END
-- close the cursor
CLOSE ProjectLayerContentChildren_Cursor
DEALLOCATE ProjectLayerContentChildren_Cursor
END

Related

T-SQL passing a table name dynamically inside WITH statement

I have the following code in T-SQL that reads table names from a cursor.
But I have problem with the scoping table name variable inside the WITH statement.
I can run this code when I explicitly set dbo.#sys_name to a synonym name like dbo.mysysnonym but when I put it as variable name like dbo.#syn_name it does not work.
-- drop duplicates records from synonyms
DECLARE #syn_name varchar(50)
DECLARE s_cursor CURSOR FOR
SELECT name
FROM sys.synonyms
WHERE base_object_name LIKE 'xyz%'
OPEN s_cursor;
FETCH NEXT FROM s_cursor INTO #syn_name;
WHILE ##FETCH_STATUS = 0
BEGIN
FETCH NEXT FROM s_cursor INTO #syn_name;
WITH dedupTable AS
(
SELECT
sys_id,
row_number() OVER (PARTITION BY sys_id ORDER BY sys_id) AS nr
FROM
dbo.#syn_name
)
DELETE FROM dedupTable
WHERE nr > 1
END;
CLOSE s_cursor
DEALLOCATE s_cursor
As far as I know, you cannot use variables as table names, so dbo.#syn_name will not work in a FROM clause. Instead, you will have to use Dynamic SQL.
Something like:
...
FETCH NEXT FROM s_cursor INTO #syn_name;
DECLARE #sql nvarchar(4000)
SET #sql = N'
WITH dedupTable
AS (
SELECT sys_id, row_number()
OVER ( PARTITION BY sys_id ORDER BY sys_id ) AS nr
FROM dbo.' + #syn_name + '
)
DELETE FROM dedupTable
WHERE nr > 1'
EXEC sp_executesql #sql

How to use WITH table AS result within cursor loop to run stored procedure

How to get result in WITH table AS into CURSOR loop? I have previously asked about how to get recursive results from my table
How to read all records recursively and show by level depth TSQL
;with C as
(
definition ...
)
I have created CURSOR loop where I want to run specific stored procedure for all results in table
declare #id int, #parent int
declare cur cursor local fast_forward
for
select id, parent from C
open cur
fetch next from cur into #id, #parent
while ##fetch_status = 0
begin
exec storedProcedure #id=#id, #parent=#parent
fetch next from cur into #id, #parent
end
close cur
deallocate cur
Problem is that CURSOR doesnt know table from WITH AS result.
Invalid object name 'C'.
You can create a temp table or a table variable to hold the rows returned by you CTE query and then you use that table as the source for your cursor.
declare #T table
(
id int,
parent int
)
;with C as
(
select 1 as id, 2 as parent
)
insert into #T
select id, parent
from C
declare cur cursor for select id, parent from #T

Loop in T-SQL, how get field value

In an SQL Server 2005 database, I have a stored procedure. I get some date in put them in a temp table. I'd like loop in this temp table and depending of the value of some fields change the value of others and make some check. I have to do this for each row.
How can I do this ?
thanks,
UPDATE1
BEGIN
SET NOCOUNT ON
--Create temp table
CREATE TABLE #MyTempTable(
id int IDENTITY(1, 1),
PriceMax int,
PriceMin int
)
-- Insert in temp table
INSERT INTO #tmpReconciliation (PriceMax, PriceMin)
SELECT PriceMax = PriceMaxProduct,
PriceMin = PriceMinProduct
FROM Products
DECLARE #RowNum int
SELECT #RowNum = Count(*) From #MyTempTable
WHILE #RowNum > 0
BEGIN
if(....)
PriceMin = 0
....
END
--Drop temp table
DROP TABLE #MyTempTable
END
I read MSDN documentation for WHILE loop and CURSOR.
For example, let's imagine your temp table is named Employee :
DECLARE #Emp_id int
DECLARE Employee_Cursor CURSOR FOR
SELECT EmployeeID
FROM Employee;
OPEN Employee_Cursor;
FETCH NEXT FROM Employee_Cursor INTO #Emp_id;
WHILE ##FETCH_STATUS = 0
BEGIN
-- Here your actions
PRINT #Emp_id
FETCH NEXT FROM Employee_Cursor INTO #Emp_id;
END;
CLOSE Employee_Cursor;
DEALLOCATE Employee_Cursor;
GO
Here I decided to print EmployeeId, but everything is possible.
Tell us what are your checks, and what your temp table looks like if you need more help.
Can't you just use a cursor and inside the cursor run an update statement??
Cursors: http://www.jackdonnell.com/articles/SQL_CURSOR.htm

Nested T-SQL Cursors Not Executing Correctly

Before I get any militant cursor-bashing, let me say that I'm trying to use nested cursors to do something I only need to do once, but if I ran the operative stored procedure once for each user and agency I have to do it a few hundred times.
I thought a nested cursor in this case would save me some work, however, when I run this script it goes through the outer cursor only once, while the inner works just fine for that run. In the test case, the outer cursor set consists of two rows, and the inner one has about fifty. It goes through the first row of the outer cursor, and all fifty of the inner, but then it's done.
As you can see, I am saving off the result of the outer fetch (the '##fetch_status') so it doesn't interfere with the inner cursor.
I can't see what the problem is (obviously). Can anyone see what I can't?
declare #fetch_user int
declare #fetch_agency int
declare user_cursor cursor for
select upn from #users
open user_cursor
fetch next from user_cursor into #upn
select #fetch_user = ##fetch_status
while #fetch_user = 0
begin
declare agency_cursor cursor for
select agency, subagency from agency_system where system_id = 1
open agency_cursor
fetch next from agency_cursor into #agency, #subagency
select #fetch_agency = ##fetch_status
while #fetch_agency = 0
begin
select #upn, #agency, #subagency
EXEC AddUserToAgencyInRole
#upn
, #agency
, #subagency
, #system_id
, #role_id
, #response output
fetch next from agency_cursor into #agency, #subagency
select #fetch_agency = ##fetch_status
end
close agency_cursor
deallocate agency_cursor
fetch next from user_cursor into #upn
select #fetch_user = ##fetch_status
end
close user_cursor
deallocate user_cursor
The code looks like it should work. Throw in a count at the start:
select count(*) from #users
to double check the number of rows in #users ?
I'm not sure about troubleshooting the nested cursor other than that there is a way to get rid of it and have only one cursor.
Make this select statement using a cross-join:
SELECT u.upn, a.agency, a.subagency
FROM #users u, agency_system a
WHERE a.system_id = 1
Use that as your cursor definition. It should have every combination of user and agency/subagency.
I agree with Andomar it should work. This test case goes through the outer loop 4 times and the inner loop twice per iteration. (Which matches the number of rows in the respective tables)
set nocount on
DECLARE #upn INT, #agency INT, #subagency INT
CREATE TABLE #users (upn INT)
insert into #users select 1 union select 2 UNION select 3 UNION select 4
CREATE TABLE #agency_system(
agency INT,
subagency INT,
system_id INT)
insert into #agency_system
select 1,1,1 UNION select 2,2,1
declare #fetch_user int
declare #fetch_agency int
declare user_cursor cursor for
select upn from #users
open user_cursor
fetch next from user_cursor into #upn
select #fetch_user = ##fetch_status
while #fetch_user = 0
begin
PRINT 'In Outer While Loop'
declare agency_cursor cursor for
select agency, subagency from #agency_system where system_id = 1
open agency_cursor
fetch next from agency_cursor into #agency, #subagency
select #fetch_agency = ##fetch_status
while #fetch_agency = 0
begin
PRINT 'In Inner While Loop'
fetch next from agency_cursor into #agency, #subagency
select #fetch_agency = ##fetch_status
end
close agency_cursor
deallocate agency_cursor
fetch next from user_cursor into #upn
select #fetch_user = ##fetch_status
end
close user_cursor
deallocate user_cursor
drop TABLE #users
drop TABLE #agency_system
I appreciate all the responses.
I ended up eliminating the outer cursor and just manually running the inner one. This saved me from manually entering 393 separate entries. I just had to run the script three times.

Reading inserted column names and values in a TSQL trigger

I've been asked to create history tables for every table in a database. Then create a trigger that will write to the history table whenever the primary table is updated.
The history tables have the same structure as the primary table, but with a couple of extra rows ('id' and 'update type')
I've never done anything with triggers before, but I would like to do is dynamically go through the columns in 'Inserted' and construct an insert statement to populate the history table.
However I cannot work out how to read the names of the columns and their individual values.
My half finished trigger currently looks like...
CREATE TRIGGER tr_address_history
ON address
FOR UPDATE
AS
DECLARE #colCount int
DECLARE #maxCols int
SET #colCount = 0
SET #maxCols = (SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'Inserted')
PRINT 'Number of columns = ' + CONVERT(varChar(10),#maxCols)
WHILE (#colCount <= #maxCols)
BEGIN
DECLARE #name varchar(255)
SELECT #name = column_name FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'Inserted'
DECLARE #value varchar(255)
SELECT #value = #name FROM Inserted
PRINT 'name = ' + #name + ' and value = ' + #value
SET #colCount = #colCount + 1
END
PRINT 'Done';
When the trigger runs it just says "Number of columns = 0"
Can anyone tell me what's wrong with :
SELECT COUNT(column_name) FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'Inserted'
Thanks...
First solution proposed by Beenay25 is good, but you should use affected table instead of 'inserted' pseudotable.
This is:
SELECT #name = column_name FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'AFFECTED_TABLE'
Instead of 'INSERTED'
Also, you should use dynamic SQL.
This will be a complete working solution:
ALTER TRIGGER [dbo].[tr_address_history]
ON [dbo].[address]
AFTER Insert
AS
DECLARE #ColumnName nvarchar(500)
DECLARE #TableName nvarchar(500)
DECLARE #value nvarchar(500)
DECLARE #Sql nvarchar(500)
Set #TableName='address'
DECLARE ColumnsCursor CURSOR FOR
select column_name FROM INFORMATION_SCHEMA.columns WHERE TABLE_NAME = 'address'
OPEN ColumnsCursor
FETCH NEXT FROM ColumnsCursor into #ColumnName
WHILE ##FETCH_STATUS=0
BEGIN
select * into #tmp from inserted
Set #Sql= 'SELECT #value =' + #ColumnName + ' FROM #tmp'
EXEC sp_executesql #Sql, N'#Value nvarchar(500) OUTPUT', #Value OUTPUT
DROP TABLE #TMP
print '[' + #ColumnName +'='+ ltrim(rtrim(#Value))+']'
FETCH NEXT FROM ColumnsCursor into #ColumnName
END
CLOSE ColumnsCursor
DEALLOCATE ColumnsCursor
The 'inserted' table is a pseudo-table; it doesn't appear in INFORMATION_SCHEMA.
There is the UPDATE() operator for use in triggers:
CREATE TRIGGER trigger_name ON tablename
FOR UPDATE
AS
SET NOCOUNT ON
IF (UPDATE(Column1) OR UPDATE(Column2))
BEGIN
your sql here
END
COLUMNS_UPDATED
UPDATE()
There is a way to do what the questioner requires:
I have made something inside a trigger that tests whether all the columns of a particular table actually participated in an insert to that table. If they did, I later copied them to a history table. If they did not, then rollback and print only complete rows may be inserted into the report table. Perhaps they could adapt this to their needs:
here it is:
[
if exists (select 1 from inserted) and not exists (select 1 from deleted) -- if an insert has been performed
begin -- and we want to test whether all the columns in the report table were included in the insert
declare #inserted_columncount int, #actual_num_of_columns int, #loop_columns int, #current_columnname nvarchar(300),
#sql_test nvarchar(max), #params nvarchar(max), #is_there bit
set #actual_num_of_columns = (
select count(*) from (
select COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'renameFilesFromTable_report') as z)
set #inserted_columncount = 0
set #loop_columns = 1
declare inserted_columnnames cursor scroll for -- these are not really the inserted ones, but we are going to test them 1 by 1
select COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'renameFilesFromTable_report'
set #params = '#is_there_in bit output'
open inserted_columnnames
fetch next from inserted_columnnames into #current_columnname
select * into #temp_for_dynamic_sql from inserted -- this is necessary because the scope of sp_executesql does not include inserted pseudo table
while (#loop_columns <= #actual_num_of_columns) -- looping with independent integer arithmetic
begin
set #sql_test = '
set #is_there_in = 0
if exists (select ['+#current_columnname+'] from #temp_for_dynamic_sql where ['+#current_columnname+'] is not null)
set #is_there_in = 1'
exec sp_executesql #sql_test, #params, #is_there output
if #is_there = 1
begin
fetch next from inserted_columnnames into #current_columnname
set #inserted_columncount = #inserted_columncount + 1
set #loop_columns = #loop_columns + 1
end
else if #is_there <> 1
begin
fetch next from inserted_columnnames into #current_columnname
set #loop_columns = #loop_columns + 1
end
end
close inserted_columnnames
deallocate inserted_columnnames
-- at this point we hold in two int variables the number of columns participating in the insert and the total number of columns
]
Then you can simply do if #inserted_columncount < #actual_num_of_columns ..........
I did this because i have a sp that inserts 1 complete line to the report table every time it runs. That's fine, but i don't want anyone else touching that table by mistake. not even myself. I also want to keep history. So i made this trigger to keep the history but also to check if an insert was attempted without values for all the columns in the report table, and further down the code it checks if an update or delete was attempted and it rollbacks.
i was thinking of expanding this to allow an update but in which all the columns are set.
this could possibly be done as follows:
if update was attempted,
and exists (
select possibly_excluded.COLUMN_NAME from (
select COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'renameFilesFromTable_report') as possibly_excluded
group by possibly_excluded.COLUMN_NAME
having COLUMN_NAME not in (
select COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'renameFilesFromTable_report' and
sys.fn_IsBitSetInBitmask(#ColumnsUpdated, COLUMNPROPERTY(OBJECT_ID(TABLE_SCHEMA + '.' + TABLE_NAME), COLUMN_NAME, 'ColumnID')) <> 0)
)
begin
rollback transaction
print 'Only updates that set the values for a complete row are allowed on the report table..'
end