Count Rows Error in Trigger - sql

I wrote this trigger below. it is generating Error at COUNT(*) From. I want that when any row inserts into table 'Users' with this trigger currently present folders in Assignments should assign to User.
For Example: I add new row to table Users suppose userD. Then with the help of this trigger Present folders like folderA, folderB, folderC should assign to userD with folder right Visible by default. I have written this Trigger below but it is givig Error at Count(*) From
CREATE TRIGGER Trigger_Insert ON Users
FOR INSERT
AS
declare #userid int;
declare #username nvarchar(50);
declare #useremail nvarchar(50);
declare #userpassword nvarchar(50);
select #userid=i.user_id from inserted i;
select #username=i.user_name from inserted i;
select #useremail=i.user_email from inserted i;
select #userpassword=i.user_password from inserted i;
DECLARE #intFlag INT
SET #intFlag =1
WHILE (#intFlag <=COUNT(*) FROM Assignments;) // Error Here
BEGIN
insert into UAssignment_Rights(
ua_rights_id,ua_rights_name,assignment_id,user_id)
values(#userid,'Visible','','');
SET #intFlag = #intFlag + 1
PRINT 'AFTER INSERT trigger fired.'
END
GO
Can you please help me to solve this issue.

Answer updated to comply with info from comments.
My point is that most of the times you do not need loops or cursor in Sql Server. Usually set based approach is simpler and faster. In this case you might use form of insert that does not insert fixed values but rather result of select.
CREATE TRIGGER Trigger_Insert ON Users
FOR INSERT
AS
-- this is mandatory in trigger - you do not want ##rowcount reported to applications
-- to change as a result of statement in trigger (all of them, including set and select)
set nocount on
-- You can simply take assignment_id directly from Assignments table
insert into UAssignment_Rights(ua_rights_name, assignment_id, user_id)
select 'Visible', a.assignment_id, i.user_id
from Inserted i
cross join dbo.Assignments a
PRINT 'AFTER INSERT trigger fired.'
GO
P.S. cartesian product is result of join without join condition. Meaning that returned rows are all possible cobinations of each row from left table with each row from right table.

COUNT(*) should only be used as part of a SELECT or HAVING statement, see MSDN for more information on aggregate functions.
If Assignments will not change as part of the WHILE loop, try replacing...
WHILE (#intFlag <=COUNT(*) FROM Assignments;) // Error Here
With...
DECLARE #AssignmentsCount INT
SELECT #AssignmentsCount = COUNT(*) FROM Assignments
WHILE #intFlag <= #AssignmentsCount
This means that the COUNT(*) is only done once.
However, if the number of Assignments could change during the loop, the replace it with...
WHILE #intFlag <= (SELECT COUNT(*) FROM Assignments)

Related

Why trigger works only once?

I have to create query that after delete will insert data in other table. But I've deleted 3 rows in my table, but table with insert has only one row. And it's the first row that was deleted.
This is my trigger:
CREATE trigger trigger1
ON Sales.SalesPerson
FOR DELETE
AS
BEGIN
DECLARE #ID int
SELECT #ID = BusinessEntityID FROM deleted
INSERT INTO dbo.Deleted(ID)
VALUES (#ID)
PRINT 'TRIGGER'
END
What did I do wrong?
In SQL Server, triggers fire per operation, not per row.
If you delete three rows, your SELECT assignment is going to (at least logically) assign each of those three values to the variable one at a time, and so the value that ultimately ends up in your logging table is the arbitrary value that happened to be assigned last.
You can simulate this as follows:
DECLARE #id int;
SELECT #id = database_id FROM sys.databases;
PRINT #id;
There are multiple rows in sys.databases, why did only one value get printed?
Instead of using a scalar variable and expecting it to somehow hold multiple values (or for the insert to happen multiple times), you need to insert as a set in a single operation:
INSERT dbo.Deleted(ID)
SELECT BusinessEntityID from deleted;
Further reading.

Stored procedure with AS MERGE not returning anything?

EDIT: Sequential invoice numbering is the law in multiple countries.
EDIT: Poor variable naming on my part suggested I wanted to use my generated Id as a key. This is not the case. Should have stuck with 'invoiceNumber'.
I have the exact same question as posed here https://stackoverflow.com/a/24196374/1980516
However, since the proposed solution threw a syntax error, I've adapted it to use a cursor.
First, there is the stored procedure that generates a new Nr, for a given Business+Year combination:
CREATE PROCEDURE PROC_NextInvoiceNumber #businessId INT, #year INT, #Nr NVARCHAR(MAX) OUTPUT
AS MERGE INTO InvoiceNextNumbers ini
USING (VALUES (#businessId, #year)) Incoming(BusinessId, Year)
ON Incoming.BusinessId = ini.BusinessId AND Incoming.Year = ini.Year
WHEN MATCHED THEN UPDATE SET ini.Nr = ini.Nr + 1
WHEN NOT MATCHED BY TARGET THEN INSERT (BusinessId, Year, Nr)
VALUES(#businessId, #year, 1)
OUTPUT INSERTED.Nr;
Then, using that stored procedure, I've created an INSTEAD OF INSERT trigger:
CREATE TRIGGER TRIG_GenerateInvoiceNumber ON Invoices INSTEAD OF INSERT
AS
BEGIN
DECLARE #BusinessId INT
DECLARE #InvoiceId INT
DECLARE #BillingDate DATETIME2(7)
-- Cursors are expensive, but I don't see any other way to call the stored procedure per row
-- Mitigating factor: Mostly, we're only inserting one Invoice at a time
DECLARE InsertCursor CURSOR FAST_FORWARD FOR
SELECT BusinessId, Id, BillingDate FROM INSERTED
OPEN InsertCursor
FETCH NEXT FROM InsertCursor
INTO #BusinessId, #InvoiceId, #BillingDate
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #year INT
SET #year = year(#BillingDate)
DECLARE #Number NVARCHAR(MAX)
EXEC PROC_NextInvoiceNumber #BusinessId, #year, #Number OUTPUT
-- SET #Number = 'this works'
INSERT INTO Invoices (BusinessId, Id, BillingDate, Number)
VALUES (#BusinessId, #InvoiceId, #BillingDate, #Number)
FETCH NEXT FROM InsertCursor
INTO #BusinessId, #InvoiceId, #BillingDate
END
CLOSE InsertCursor
DEALLOCATE InsertCursor
END
If I uncomment SET #Number = 'this works', then in my database that exact string ('this works') is successfully set in Invoice.Number.
Somehow, my OUTPUT parameter is not set and I can't figure out why not.. Can someone shed a light on this?
EDIT update in response to comments (thank you):
I have a composite key (BusinessId, Id) for Invoice. The desired end result is a unique Invoice Identifier Number of the form '20180001' that is a continuous sequence of numbers within the businessId. So business 1 has invoice Numbers 20180001, 20180002, 20180003 and business 2 also has invoice numbers 20180001, 20180002, 20180003. (But different composite primary keys)
I don't want that cursor either, but I saw no other way within the framework as suggested by the question I refer to up above.
Manual call of PROC_NextInvoiceNumber with existing business id and year returns NULL.
If I try to set Id in PROC_NextInvoiceNumber, I get A MERGE statement must be terminated by a semi-colon (;). if I set it inside the MERGE or The multi-part identifier "INSERTED.Nr" could not be bound. if I set outside the MERGE.
Your OUTPUT parameter is never set. You are using the OUTPUT clause of the MERGE statement to create a result set. This is unrelated to assigning a value to a parameter.
MERGE INTO..
USING ... ON ...
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED BY TARGET THEN INSERT ...
OUTPUT INSERTED.Nr; /* <-- HERE this is the OUTPUT *clause* */
Change the code to actually assign something to #Nr:
SET #Nr = ...
The typical way is to use the OUTPUT clause to store the desired value into a table variable and then assign the value to the desired output *variable:
DECLARE #t TABLE (Nr NVARCHAR(MAX));
MERGE INTO..
USING ... ON ...
WHEN MATCHED THEN UPDATE ...
WHEN NOT MATCHED BY TARGET THEN INSERT ...
OUTPUT INSERTED.Nr INTO #t;
SELECT #Nr = Nr FROM #t;

How to get a inserted id in other table from inside a trigger?

I have 3 tables tbl_Users, tbl_Protocol and tbl_ProtocolDetails and inside of my trigger on Users, I have to inserted into Protocol and then insert into ProtocolDetails, but I don't know how work the inserted scope.
Something like that:
CREATE TRIGGER tg_Users ON tbl_Users
AFTER INSERT, UPDATE AS
BEGIN
DECLARE #UserId = Int
DECLARE #ProtocolId = Int
DECLARE #UserDetail = NVARCHAR(255)
SELECT
#UserId = user_id,
#UserDetail = user_detail + '#' + user_explanation
FROM INSERTED
INSERT INTO tbl_Protocol (user_id, inserted_date)
VALUES (#UserId, GetDate())
-- Return Inserted Id from tbl_Protocol into #ProtocolDetail then
INSERT INTO tbl_ProtocolDetails (protocol_id, protocol_details)
VALUES (#ProtocolId, #UserDetail)
END
Your trigger has a MAJOR flaw in that you seems to expect to always have just a single row in the Inserted table - that is not the case, since the trigger will be called once per statement (not once for each row), so if you insert 20 rows at once, the trigger is called only once, and the Inserted pseudo table contains 20 rows.
Therefore, code like this:
Select #UserId = user_id,
#UserDetail = user_detail + '#' + user_explanation
From INSERTED;
will fail, since you'll retrieve only one (arbitrary) row from the Inserted table, and you'll ignore all other rows that might be in Inserted.
You need to take that into account when programming your trigger! You have to do this in a proper, set-based fashion - not row-by-agonizing-row stlye!
Try this code:
CREATE TRIGGER tg_Users ON tbl_Users
AFTER INSERT, UPDATE AS
BEGIN
-- declare an internal table variable to hold the inserted "ProtocolId" values
DECLARE #IdTable TABLE (UserId INT, ProtocolId INT);
-- insert into the "tbl_Protocol" table from the "Inserted" pseudo table
-- keep track of the inserted new ID values in the #IdTable
INSERT INTO tbl_Protocol (user_id, inserted_date)
OUTPUT Inserted.user_id, Inserted.ProtocolId INTO #IdTable(UserId, ProtocolId)
SELECT user_id, SYSDATETIME()
FROM Inserted;
-- insert into the "tbl_ProtocolDetails" table from both the #IdTable,
-- as well as the "Inserted" pseudo table, to get all the necessary values
INSERT INTO tbl_ProtocolDetails (protocol_id, protocol_details)
SELECT
t.ProtocolId,
i.user_detail + '#' + i.user_explanation
FROM
#IdTable t
INNER JOIN
Inserted i ON i.user_id = t.UserId
END
There is nothing in this trigger that would handle a multiple insert/update statement. You will need to either use one scenario that will handle multiple records or check how many records were effected with a IF ##ROWCOUNT = 1 else statement. In your example, I would just use something like
insert into tbl_Protocol(user_id, inserted_date)
select user_id, user_detail + '#' + user_explanation
From INSERTED;
As for your detail table, I see Marc corrected his answer to include the multiple lines and has a simple solution or you can create a second trigger on the tbl_Protocol. Another solution I have used in the past is a temp table for processing when I have very complicated triggers.

Delete trigger and getting field from another table

I have this delete trigger on an SQL database. The record deletes currently and gets written to an audit table. I have been asked to include in this history table a field from another table that is related to the record being deleted based on SurveyID. I thought I could do something like
select #Status = Status from table where Survey = deleted.Survey
But this is incorrect syntax.
ALTER trigger [dbo].[table_Selfdelete]
on [dbo].[table]
after delete
as
Begin
Set nocount on;
Declare #SurveyId int
Declare #StudentUIC varchar(10)
Declare #Status varchar(10)
select #SurveyId = deleted.SurveyID,
#StudentUIC = deleted.StudentUIC
from deleted
select #Status = Status from tbly when SurveyID = deleted.SurveyID
insert into fupSurveyAudit
values(#SurveyId,#StudentUIC,#Status)
End
Arrgh. I think you want this insert in your trigger (and nothing else):
insert into fupSurveyAudit(SurveyId, StudentUIC, status)
select d.SurveyId, d.StudentUIC, y.status
from deleted d left join
tbly y
on d.SurveyId = y.SurveyId;
Notes:
deleted could contain more than one row, so assuming that it has one row can lead to a run-time error or incorrect results.
A left join is needed in case there is no matching row for the status.
You should always include the columns in an insert
Your archive table should have additional columns, such as an identity column and the date of the insert, which are set automatically (and hence not explicitly part of the insert).
Triggers are fired once for each statement (Delete,insert,update) not for each row inside the statement.
You cannot use variables here because when multiple lines are deleted from the table only one line will be inserted in the Audit table because the variable can only hold one value.
You just need a simple insert from the deleted table into the Audit table something like this....
ALTER trigger [dbo].[table_Selfdelete]
on [dbo].[table]
after delete
as
Begin
Set nocount on;
insert into fupSurveyAudit(SurveyId, StudentUIC,[Status])
select d.SurveyID
,d.StudentUIC
,y.[Status]
from deleted d
INNER JOIN tbly y ON y.SurveyID = deleted.SurveyID
End
Try this
ALTER trigger [dbo].[table_Selfdelete]
on [dbo].[table]
after delete
as
Begin
Set nocount on;
insert into fupSurveyAudit -- Better listed the column list here
select
d.SurveyID, d.StudentUIC, y.Status
from
deleted d JOIN tbly y ON d.SurveyID = y.SurveyID
End

T-SQL - anti-duplicate trigger

I need to write a trigger which prevents from inserting more than one record at the same time and also checks if the place is already in the database. Code compiles but it doesn't work as it should - it displays error message even if I try to add a non-existing address
Here's my code:
CREATE TRIGGER address_duplicate ON place
AFTER INSERT
AS
BEGIN
DECLARE #counter INT
SELECT #counter=COUNT(*) FROM place WHERE street IN (SELECT street FROM inserted) AND number IN(SELECT number FROM inserted)
AND city IN(SELECT city FROM inserted) AND postcode IN(SELECT postcode FROM inserted)
IF #counter>0
BEGIN
RAISERROR('This record is already in the database',1,1)
ROLLBACK
END
IF ##ROWCOUNT>1
BEGIN
RAISERROR('You can add only one record at the same time',1,2)
ROLLBACK
END
END
GO
Your logic of identifying duplicate place is not correct.
Try something like this:
select #counter= count(*) from place p join inserted n
where p.address=n.address and p.city=n.city and p.postcode=n.postcode
and p.number=n.number;
Also want you to know, using triggers to avoid duplicate can be very expensive.
Personally, I'd use a unique constraint and probably use TRY-CATCH when inserting, but if you really want to do it in a trigger, try this out:
CREATE TRIGGER address_duplicate ON place
INSTEAD OF INSERT
AS
BEGIN
DECLARE #newValue TABLE (ID INT);
IF ##ROWCOUNT > 1
BEGIN
RAISERROR('Insert cancelled. You can add only one record at the same time.',1,2);
END
ELSE
BEGIN
INSERT INTO place
OUTPUT inserted.ID INTO #newValue
SELECT *
FROM inserted
EXCEPT
SELECT *
FROM place
IF (SELECT 1 FROM #newValue) IS NULL
RAISERROR('Insert cancelled. This record is already in the database',1,1)
END
END