Trigger to prevent infinite loop in a sql tree - sql

I have node table with (NodeId, NodeName) and structure table (ParentNodeId, ChildNodeId). How can I write a trigger that check if an insert update or delete statement can cause infinite relation?

Here is my solution, and so far it works as expected.
CREATE TRIGGER [dbo].[CheckNodeDependence] ON [dbo].[ObjectTrees]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
DECLARE #CTable TABLE(ChildId INT NOT NULL,
ParentId INT NOT NULL,
[Level] INT NOT NULL,
RowId INT NOT NULL)
DECLARE #Level INT
SET #Level = 1
DECLARE #rows_affected INT
SET #rows_affected = 1
INSERT INTO #CTable
SELECT ObjectId, ParentId, 1, ObjectId FROM INSERTED
WHILE #rows_affected > 0
BEGIN
SET #Level = #Level + 1
INSERT INTO #CTable
SELECT T.ObjectId, T.ParentId, #Level, C.RowId
FROM ObjectTrees T
INNER JOIN #CTable C ON T.ParentId = C.ChildId
AND C.Level = #Level - 1
SET #rows_affected = ##rowcount
IF EXISTS(
SELECT * FROM #CTable B
INNER JOIN #CTable V ON B.level = 1
AND V.Level > 1
AND V.RowId = B.RowId
AND V.ChildId = B.RowId)
BEGIN
DECLARE #error_message VARCHAR(200)
SET #error_message = 'Operation would cause illegal circular reference in tree structure, level = ' + CAST(#Level AS VARCHAR(30))
RAISERROR(#error_message,16,1)
ROLLBACK TRANSACTION
RETURN
END
END
END
GO

You'll have to recursively check the circular dependency condition in which the parent doesn't become a child of its own child, either directly or indirectly.
In SQL Server 2005, you can write a recursive CTE for the same. An example -
WITH [RecursiveCTE]([Id], [ParentAccountId]) AS
(
SELECT
[Id],
[ParentAccountId]
FROM [Structure]
WHERE [Id] = #Id
UNION ALL
SELECT
S.[Id],
S.[ParentAccountId]
FROM [Structure] S INNER JOIN [RecursiveCTE] RCTE ON S.[ParentAccountId] = RCTE.[Id]
)
SELECT * FROM [RecursiveCTE]

Related

Improve the speed for using NOT EXISTS and CTE to get all children count?

I have a table of wsmp_object (about 66,000 rows), it has the PK of OBJECT_ID, and it has parent/children relation:
CREATE TABLE [dbo].[WSMP_OBJECT]
(
[OBJECT_ID] [int] IDENTITY(1,1) NOT NULL,
[PARENT_ID] [int] NOT NULL,
[LAYER_ID] [int] NOT NULL,
[OBJECT_NAME] [nvarchar](50) NOT NULL,
[OBJECT_STATE] [int] NOT NULL
)
If OBJECT_STATE is set to 0 to hide one object, then all its children and children of children ... will be seen as hidden no matter what value the children's OBJECT_STATE is set(it has -2,-1,0,1, 1 is normal state).
Now I want to retrieve all children count of one object whose object_id = -1 from this table where OBJECT_TYPE = 'ZONE' and not a hidden object.
Below is my SQL and it works. But it consumes a long time about 5-6s.
IF OBJECT_ID(N'tempdb.dbo.#TREE_DTT_SLM', N'U') IS NOT NULL
BEGIN
DROP TABLE #TREE_DTT_SLM;
END;
WITH _dtt AS
(
SELECT
dtt.OBJECT_ID, dtt.PARENT_ID, dtt.OBJECT_STATE
FROM
WSMP_OBJECT dtt
WHERE
dtt.OBJECT_STATE != 1
UNION ALL
SELECT
dtt.OBJECT_ID, dtt.PARENT_ID, dtt.OBJECT_STATE
FROM
_dtt _dt, WSMP_OBJECT dtt
WHERE
dtt.PARENT_ID = _dt.OBJECT_ID
)
SELECT OBJECT_ID
INTO #TREE_DTT_SLM
FROM _dtt;
DECLARE #AREA_DMA_COUNT INT
SELECT #AREA_DMA_COUNT = COUNT(0)
FROM [dbo].[FNNC_GET_AREACHILD]('-1') FGA
INNER JOIN [dbo].[WSMP_OBJECT] WO ON FGA.AREAID = WO.OBJECT_ID
AND WO.OBJECT_TYPE = 'ZONE'
AND NOT EXISTS (SELECT OBJECT_ID
FROM #TREE_DTT_SLM TDS
WHERE TDS.OBJECT_ID = WO.OBJECT_ID)
SELECT #AREA_DMA_COUNT AS AREA_DMA_COUNT
dbo.[FNNC_GET_AREACHILD]
ALTER FUNCTION [dbo].[FNNC_GET_AREACHILD] (#ID INT)
RETURNS #T TABLE
(
-- ID INT IDENTITY
-- PRIMARY KEY ,
SUBORDINATE INT ,
AREAID INT ,
AREAPID INT ,
AREALEVEL INT
)
AS
BEGIN
DECLARE #I INT;
SET #I = 1;
IF #ID IS NULL OR #ID = ''
BEGIN
SELECT TOP 1
#ID = OBJECT_ID
FROM
[dbo].[WSMP_OBJECT]
WHERE
OBJECT_TYPE IN ('ZONE');
END;
INSERT INTO #T
SELECT
OBJECT_ID, OBJECT_ID, PARENT_ID, #I
FROM
[dbo].[WSMP_OBJECT]
WHERE
OBJECT_TYPE IN ('ZONE')
AND OBJECT_ID = #ID;
WHILE ##rowcount <> 0
BEGIN
SET #I = #I + 1;
INSERT INTO #T
SELECT
#ID, A.OBJECT_ID, A.PARENT_ID, #I
FROM
[dbo].[WSMP_OBJECT] A, #T B
WHERE
A.OBJECT_TYPE IN ('ZONE')
AND A.PARENT_ID = B.AREAID
AND B.AREALEVEL = #I - 1;
END;
RETURN;
END;
The function is very quickly to get all levels, only when I add the NOT EXISTS part of SQL it wastes much time.
How to modify the SQL to make it more quicker to get all children count (hidden objects are excluded) of one object whose id is -1?

Looking for missing gaps for getting "Only one expression can be specified in the select list when the subquery is not introduced with EXISTS."

I have the following query to find missing gaps in the sort for each ModelID but I keep getting the following error and don't know why.
What I'm doing is in my first loop I am looping through the modelID's and in the inner loop I am looking for the missing gaps in the siSort column for that modelID and putting that into a temp table.
Msg 116, Level 16, State 1, Line 27
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS.
USE crm
GO
BEGIN
DECLARE #ID INT
DECLARE #MAXID INT
DECLARE #COUNT INT
DECLARE #iCustomListModelID INT
DECLARE #iCustomFieldID INT
DECLARE #MissingIds TABLE (ID INT)
DECLARE #Output TABLE (iCustomListModelID INT, siSort INT, iListItemID INT)
-- SELECT ALL DISTINCT ICustomListModelID's FROM CustomList Table
SELECT DISTINCT cl.iCustomListModelID
INTO #DistinctModelIDs
FROM dbo.CustomListModel clm
INNER JOIN dbo.CustomListType clt ON clm.iCustomListTypeID = clt.iCustomListTypeID
AND clt.vchCustomListTypeDescription = N'Household Custom Field'
INNER JOIN dbo.CustomList cl ON clm.iCustomListModelID = cl.iCustomListModelID
INNER JOIN dbo.CustomField cf ON cl.iListItemID = cf.iCustomFieldID
ORDER BY cl.iCustomListModelID
-- Get iCustomFieldID to insert into iListItemID
SET #iCustomFieldID = (SELECT * FROM dbo.CustomField cf WHERE vchLabel = '')
-- Begin Outer loop to go through each iCustomListModelID
WHILE (SELECT COUNT(iCustomListModelID) AS Total FROM #DistinctModelIDs) > 0
BEGIN
-- GRAB THE NEXT iCustomListModelID
SELECT #iCustomListModelID = (SELECT TOP 1 iCustomListModelID FROM #DistinctModelIDs);
DROP TABLE #List
SELECT siSort INTO #List FROM CustomList WHERE iCustomListModelID = #iCustomListModelID
SELECT #MAXID = siSort FROM dbo.CustomList WHERE iCustomListModelID = #iCustomListModelID
SET #ID = 1;
-- Inner loop to go through the missing gaps in siSort
WHILE #ID <= #MAXID
BEGIN
IF NOT EXISTS (
SELECT 'X' FROM #List WHERE siSort = #ID
)
INSERT INTO #MissingIDs (ID)
VALUES (#ID)
--INSERT THE MISSING ID INTO #outputTable Table
INSERT INTO #Output (iCustomListModelID, siSort, iListItemID)
VALUES (#iCustomListModelID, #ID, #iCustomFieldID)
SET #ID = #ID + 1;
END;
-- DELETE CURRENT iCustomListModelID
DELETE FROM #DistinctModelIDs WHERE iCustomListModelID = #iCustomListModelID
END
SELECT * FROM #Output
END;
One possibility is that the issue is this line:
SET #iCustomFieldID = (SELECT * FROM dbo.CustomField cf WHERE vchLabel = '')
If dbo.CustomerField doesn't have exactly one column (more than one column seems likely because vchLabel is already one column in the table), then this will generate an error of that type.

SQL Server generate script for views and how to decide order?

I am generating the script for views using SQL Server built-in feature (Task -> Generate script). I am creating separate file for each object (of view). I have say around 400 files (containing SQL script of all views) to be executed on another database and to do that automatically I have created BAT file which takes care of that.
There are views which are dependent on other views and due to that many views failed to execute. Is there any way by which we can set order of execution and get rid off the failure ?
Any pointers would be a great help.
Please let me know if you need more details.
Thanks
Jony
Could you try this query? You can execute the create scripts in order to "gen" (generation).
DECLARE #cnt int = 0, #index int;
DECLARE #viewNames table (number int, name varchar(max))
DECLARE #viewGen table (id uniqueidentifier, gen int, name varchar(max), parentId uniqueidentifier)
INSERT INTO #viewNames
SELECT ROW_NUMBER() OVER(ORDER BY object_Id), name FROM sys.views
SELECT #cnt = COUNT(*) FROM #viewNames
SET #index = #cnt;
WHILE ((SELECT COUNT(*) FROM #viewGen) < #cnt)
BEGIN
DECLARE #viewName varchar(200)
SELECT #viewName = name FROM #viewNames WHERE number = #index;
DECLARE #depCnt int = 0;
SELECT #depCnt = COUNT(*) FROM sys.dm_sql_referencing_entities ('dbo.' + #viewName, 'OBJECT')
IF (#depCnt = 0)
BEGIN
INSERT INTO #viewGen SELECT NEWID(), 0, name, null FROM #viewNames WHERE number = #index;
END
ELSE
BEGIN
IF EXISTS(SELECT * FROM sys.dm_sql_referencing_entities ('dbo.' + #viewName, 'OBJECT') AS r INNER JOIN #viewGen AS v ON r.referencing_entity_name = v.name)
BEGIN
DECLARE #parentId uniqueidentifier = NEWID();
INSERT INTO #viewGen SELECT #parentId, 0, name, null FROM #viewNames WHERE number = #index;
UPDATE v
SET v.gen = (v.gen + 1), parentId = #parentId
FROM #viewGen AS v
INNER JOIN sys.dm_sql_referencing_entities('dbo.' + #viewName, 'OBJECT') AS r ON r.referencing_entity_name = v.name
UPDATE #viewGen
SET gen = gen + 1
WHERE Id = parentId OR parentId IN (SELECT Id FROM #viewGen WHERE parentId = parentId)
END
END
SET #index = #index - 1
IF (#index < 0) BEGIN SET #index = #cnt; END
END
SELECT gen as [order], name FROM #viewGen ORDER BY gen
Expecting result:
order name
0 vw_Ancient
1 vw_Child1
1 vw_Child2
2 vw_GrandChild

SQL insert statement for each update row

Now i made cursor to update in 2 tables and insert in one table based on specific select statement this select statement return 2 columns x , y i need x to update in table "PX" because x is Primary key in this table and need x to update in table "FX" because x is foreign key in this table then insert in third table x data.
I need to change this cursor and use update and insert script i tried but i found that i need to make loop to achieve my target so if any one know if i can change this cursor .
And thanks in advance
DECLARE #id int
DECLARE #clientid uniqueidentifier
DECLARE #code int
DECLARE #Wtime int
DECLARE #closeComplaint CURSOR
SET #closeComplaint = CURSOR FAST_FORWARD
FOR
SELECT ComplaintId, [ClientId]
FROM complaint
WHERE ComplaintStatusId = 5
AND (waitingForCutomerCloseDateTime < GETDATE() OR
waitingForCutomerCloseDateTime = GETDATE())
OPEN #closeComplaint
FETCH NEXT FROM #closeComplaint INTO #id, #clientid
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT
waitingForCutomerCloseTime = #Wtime
FROM
SystemConfiguration
WHERE
ClientId = #clientid
SELECT
[Code] = #code
FROM
[dbo].[resp_users]
WHERE
ClientId = #clientid
UPDATE activity
SET ActivityStatus = 4,
CompletionDate = GETDATE(),
ClosedBy = #code
WHERE [ComplaintId] = #id
UPDATE [dbo].[Complaint]
SET ComplaintStatusId = 2
WHERE [ComplaintId] = #id
INSERT INTO [dbo].[Note] ([Note_Description], [ClientId], [User_Code], [Visible_Internal],
[ComplaintId], [Note_DateTime], [ComplainantId],
[OneStopDesk_CustomerEmail], [OneStopDesk_CustomerUsername], [Private])
VALUES (N'Automatically closed by system after ' + #Wtime, #clientid, #code, 1,
#id, GETDATE(), null, null, null, 1)
FETCH NEXT FROM #closeComplaint INTO #id, #clientid
END
CLOSE #closeComplaint
DEALLOCATE #closeComplaint
I'm not entirely sure I got everything right (you didn't post the table structures, so I can really only guess at times how those tables are connected) - but you should be able to basically do all of this in 3 simple, set-based statements - and that should be a LOT faster than the cursor!
-- declare table variable
DECLARE #Input TABLE (CompaintID INT, ClientID INT)
-- save the rows into a table variable
INSERT INTO #Input (ComplaintID, ClientID)
SELECT ComplaintID, ClientID
FROM dbo.Complaint
WHERE ComplaintStatusId = 5
AND waitingForCustomerCloseDateTime <= GETDATE()
UPDATE a
SET ActivityStatus = 4,
CompletionDate = GETDATE(),
ClosedBy = u.Code
FROM dbo.Activity a
INNER JOIN #Input i ON a.ComplaintId = i.ComplaintId
INNER JOIN dbo.resp_users u ON i.ClientId = u.ClientId
UPDATE dbo.Complaint
SET ComplaintStatusId = 2
WHERE
ComplaintStatusId = 5
AND waitingForCustomerCloseDateTime <= GETDATE()
INSERT INTO dbo.Note ([Note_Description], [ClientId], [User_Code], [Visible_Internal],
[ComplaintId], [Note_DateTime], [ComplainantId],
[OneStopDesk_CustomerEmail], [OneStopDesk_CustomerUsername], [Private])
SELECT
N'Automatically closed by system after ' + sc.waitingForCustomerCloseTime,
i.ClientId, u.Code, 1,
i.ComplaintId, GETDATE(), null, null, null, 1
FROM
#Input i
INNER JOIN
dbo.SystemConfiguration sc ON i.ClientId = sc.ClientId
INNER JOIN
dbo.resp_user u ON u.ClientId = i.ClientId

Using TOP(variable) with a subset of data

I have abstracted the problem to the following situation:
I have a table (A) that contains the number (Quantity) of items I want to update.
Next I have a table (SL) that contains the references to table (A) that I need to select from.
And finally the table that needs to get updated (B)
CREATE TABLE A
(
Id int,
Quantity int
)
CREATE TABLE SL
(
Id int,
A_Id int,
S_Id int
)
CREATE TABLE B
(
Id int,
StatusValue int,
A_Id int,
S_Id int NULL,
)
So let's insert some data for testing purposes:
INSERT INTO A Values (1, 4), (2, 2), (3, 3), (4, 4), (5, 2)
delete from B
declare #i int = 1;
declare #j int = 0;
declare #maxA int = 5;
declare #rows_to_insert int = 10;
while #i < #maxA
begin
while #j < #rows_to_insert * #i
begin
INSERT INTO B VALUES (10+#j, 0, #i, null)
set #j = #j + 1
end
set #i = #i + 1
end
select * from B
INSERT INTO SL Values (1, 1, 1), (2, 2, 1), (3 ,2, 1)
And now on to the problem. I want to update TOP(Quantity) of records in B relating to the records in SL. Basically this is what I want to do, but it is unsupported in SQL:
DECLARE #Sale_Id int = 1;
WITH AB (AId, AQuantity, SaleId)
AS
(
SELECT A.Id, A.Quantity, SL.S_Id FROM A
INNER JOIN SL on A.Id = SL.A_Id
WHERE SL.S_Id = #Sale_Id
)
UPDATE TOP(AB.Quantity) B
SET StatusValue = 1,
S_Id = AB.SaleId
FROM AB
WHERE StatusValue = 0 -- Edited
AND B.A_Id = AB.AId
The error message is
Msg 4104, Level 16, State 1, Line 55
The multi-part identifier "AB.Quantity" could not be bound.
what are my options of getting this done?
(There is always the Cursor but is that a good option?)
Note: The data has a funny side to it that in SL there is 2 times a record referencing A_Id = 2. This implies that the result needs to have 4 B records with A_Id = 2 updated.
This should work
DECLARE #Sale_Id int = 1;
with tmp as (
SELECT A.Id, A.Quantity, SL.S_Id SaleID, B.S_ID, b.id b_id, B.StatusValue,
rn=dense_rank() over (partition by a.id order by b.id)
FROM A
JOIN SL on A.Id = SL.A_Id
JOIN B ON B.A_Id = A.Id
WHERE SL.S_Id = #Sale_Id
)
update tmp
set S_ID = SaleID,
StatusValue = 1
where rn <= quantity;
However, your data looks funny with A.id=2 being sold twice on the same SL.id=1.
SQL Fiddle
So it seems the Cursor is the way I'll need to go:
DECLARE #A_Id int, #Quantity int;
DECLARE ABCursor CURSOR LOCAL READ_ONLY FOR
SELECT A.Id, A.Quantity FROM A
INNER JOIN SL on A.Id = SL.A_Id
WHERE SL.S_Id = #Sale_Id
OPEN ABCursor
FETCH NEXT FROM ABCursor
INTO #A_Id, #Quantity
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE TOP(#Quantity) B
SET StatusValue = 1,
S_Id = #Sale_Id
WHERE StatusValue = 0
AND B.A_Id = #A_Id
AND S_Id is null
FETCH NEXT FROM ABCursor
INTO #A_Id, #Quantity
END
CLOSE ABCursor;
DEALLOCATE ABCursor;
Now must read up on what is the best definition for the Cursor i.e.: LOCAL READ_ONLY