SQL Trigger insert problems - sql

I want to make a trigger where I can check if the value from the stock of a product 0 is.
Then the trigger should make the value 1.
My Trigger
CREATE TRIGGER [IfStockIsNull]
ON [dbo].[Products]
FOR DELETE, INSERT, UPDATE
AS
BEGIN
if [Products].Stock = 0
UPDATE [Products] SET [Stock] = 1
FROM [Products] AS m
INNER JOIN inserted AS i
ON m.ProductId = i.ProductId;
ELSE
raiserror('Niks aan het handje',10,16);
END
I'm getting the error:
Altering [dbo].[IfStockIsNull]...
(53,1): SQL72014: .Net SqlClient Data Provider: Msg 4104, Level 16, State 1,
Procedure IfStockIsNull, Line 7 The multi-part identifier "Products.Stock"
could not be bound.
(47,0): SQL72045: Script execution error. The executed script:
ALTER TRIGGER [IfStockIsNull]
ON [dbo].[Products]
FOR DELETE, INSERT, UPDATE
AS BEGIN
IF [Products].Stock = 0
UPDATE [Products]
SET [Stock] = 1
FROM [Products] AS m
INNER JOIN
inserted AS i
ON m.ProductId = i.ProductId;
ELSE
RAISERROR ('Niks aan het handje', 10, 16);
END
An error occurred while the batch was being executed.
Maybe you guys can help me out?

A number of issues and surprises in what you're trying to run here. But basically, don't try to run procedural steps when you're dealing with sets. inserted can contain 0, 1 or multiple rows, so which row's stock are you asking about in your IF?
Better to deal with it in the WHERE clause:
CREATE TRIGGER [IfStockIsNull]
ON [dbo].[Products]
FOR INSERT, UPDATE --Removed DELETE, because ?!?
AS
BEGIN
UPDATE [Products] SET [Stock] = 1
FROM [Products] AS m
INNER JOIN inserted AS i
ON m.ProductId = i.ProductId;
WHERE m.Stock = 0
--Not sure what the error is for - the above update may have updated
--some number of rows, between 0 and the number in inserted.
--What circumstances should produce an error then?
END
Simple demo script that the UPDATE is correctly targetting only matching rows from inserted:
declare #t table (ID int not null, Val int not null)
insert into #t(ID,Val) values (1,1),(2,2),(3,3)
update
#t
set
Val = 4
from
#t t
inner join
(select 2 as c) n
on
t.ID = n.c
select * from #t
Shows that only the row with ID of 2 is updated.
Even Microsoft's own example of UPDATE ... FROM syntax uses the UPDATE <table> ... FROM <table> <alias> ... syntax that Gordon asserts doesn't work:
USE AdventureWorks2012;
GO
UPDATE Sales.SalesPerson
SET SalesYTD = SalesYTD + SubTotal
FROM Sales.SalesPerson AS sp
JOIN Sales.SalesOrderHeader AS so
ON sp.BusinessEntityID = so.SalesPersonID
AND so.OrderDate = (SELECT MAX(OrderDate)
FROM Sales.SalesOrderHeader
WHERE SalesPersonID = sp.BusinessEntityID);
GO
This example does have an issue which is further explained below it but that relates to the "multiple matching rows in other tables will result in only a single update" flaw that anyone working with UPDATE ... FROM should make themselves aware of.

If you want to stop the insert if any rows are bad, then do that before making the changes:
ALTER TRIGGER [IfStockIsNull] ON [dbo].[Products]
FOR INSERT, UPDATE -- DELETE is not really appropriate
AS BEGIN
IF (EXISTS (SELECT 1
FROM Products p JOIN
inserted i
ON m.ProductId = i.ProductId
WHERE p.Stock = 0
)
)
BEGIN
RAISERROR ('Niks aan het handje', 10, 16);
END;
UPDATE p
SET Stock = 1
FROM Products p INNER JOIN
inserted AS i
ON p.ProductId = i.ProductId;
END;
Notes:
The comparison in the IF checks if any of the Product rows have a 0. If so, the entire transaction is rolled back.
You might complain that you cannot reject a single row. But that is how transactions work. If any row fails in an INSERT, the entire INSERT is rolled back, not just a single row.
The UPDATE is not correct, because you have Products in the FROM clause and the UPDATE clause. You need to use the alias for the first one.

Related

Optimistic concurrency while in transaction

In trying to fix data errors due to concurrency conflicts I realized I'm not completely sure how optimistic concurrency works in SQL Server. Assume READ_COMMITTED isolation level. A similar example:
BEGIN TRAN
SELECT * INTO #rows FROM SourceTable s WHERE s.New = 1
UPDATE d SET Property = 'HelloWorld' FROM DestinationTable d INNER JOIN #rows r ON r.Key = d.Key
UPDATE s SET Version = GenerateRandomVersion() FROM SourceTable s
INNER JOIN #rows r on r.Key = s.Key AND r.Version = s.Version
IF ##ROWCOUNT <> SELECT COUNT(*) FROM #rows
RAISEERROR
END IF
COMMIT TRAN
Is this completely atomic / thread safe?
The ON clause on UPDATE s should prevent concurrent updates via the Version and ROWCOUNT check. But is that really true? What about the following, similar query?
BEGIN TRAN
SELECT * INTO #rows FROM SourceTable s WHERE s.New = 1
UPDATE s SET New = 0 AND Version = GenerateRandomVersion() FROM SourceTable s
INNER JOIN #rows r on r.Key = s.Key AND r.Version = s.Version
IF ##ROWCOUNT <> SELECT COUNT(*) FROM #rows
RAISEERROR
END IF
UPDATE d SET Property = 'HelloWorld' FROM DestinationTable d INNER JOIN #rows r ON r.Key = d.Key
COMMIT TRAN
My worry here is that concurrent execution of the above script will reach the UPDATE s statement, get a ##ROWCOUNT that is transient / not actually committed to DB yet, so both threads / executions will continue past the IF statement and perform the important UPDATE d statement, which in this case is idempotent but not so in my original production case.
I think what you want to do is remove the very small race condition in your script by making it as set based as possible, e.g.
BEGIN TRAN
DECLARE #UpdatedSources Table (Key INT NOT NULL);
UPDATE s SET New = 0
FROM SourceTable s
WHERE s.New = 1
OUTPUT Inserted.Key into #UpdatedSources
UPDATE d SET Property = 'HelloWorld'
FROM DestinationTable d
INNER JOIN #UpdatedSources r ON r.Key = d.Key
COMMIT TRAN
I think the 'version' column in your table is confusing things - you're trying to build atomicity into your table rather than just letting the DB transactions handle that. With the script above, the rows where New=1 will be locked until the transaction commits, so subsequent attempts will only find either 'actually' new rows or rows where new=0.
Update after comment
To demonstrate the locking on the table, if it is something you want to see, then you could try and initiate a deadlock. If you were to run this query concurrently with the first one, I think you may eventually deadlock, though depending on how quickly these run, you may struggle to see it:
BEGIN TRAN
SELECT *
FROM DestinationTable d
INNER JOIN SourceTable ON r.Key = d.Key
WHERE s.New = 1
UPDATE s SET New = 0
FROM SourceTable s
WHERE s.New = 1
COMMIT TRAN

Run update statement on inserted rows only through a trigger

I have created a Trigger to update a custom date [Invoice.ContractStartDate] on an invoice record when added to the Invoice Table [InvoiceTable].
The Trigger below works but I would like this to run for Inserted records only and avoid using the WHERE clause for DateTimeCreated.
I will still require the WHERE clause for Contract.Days <> ' '
Is this possible using using AFTER INSERT or INSTEAD OF INSERT?
CREATE TRIGGER UpdateContractStartDate ON InvoiceTable
FOR INSERT AS
UPDATE InvoiceTable
SET InvoiceTable.ContractStartDate = InvoiceTable.InvoiceDate + Contract.Days
FROM Contract
INNER JOIN InvoiceTable
ON InvoiceTable.ContractID = Contract.ContractID
WHERE (CAST(InvoiceTable.DateTimeCreated AS DATE) = CAST(GETDATE() AS DATE))
AND (Contract.Days <> '')
You don't provide any information on your table schema, but assuming you have a unique key "Id"
The typical syntax would be
Update it set
it.ContractStartDate = it.InvoiceDate + c.Days
from inserted i
join InvoiceTable it on it.Id=i.Id
join Contract c on c.ContactId=it.ContractId and c.Days <> ''

Delete Trigger with Update Subquery Statement with Multirow Delete

This trigger is working fine with single row delete, but on multiple row
CREATE TRIGGER [dbo].[AfterDeleteStockUpdate]
ON [dbo].[ItemLedger]
AFTER DELETE
AS
DECLARE #productId uniqueidentifier
SELECT #productId = del.Product
FROM deleted del;
UPDATE products
SET inStock = (SELECT SUM(ISNULL(inQty, 0)) - SUM(ISNULL(outQty, 0))
FROM itemledger
WHERE product = #productId),
PurchasedValue = (SELECT SUM(ISNULL(PurchaseAmount, 0)) - SUM(ISNULL(Amount, 0))
FROM itemledger
WHERE product = #productId)
WHERE id = #productId
The Deleted pseudo-table can have 0-N rows in it, which you need to handle. And like all T-SQL you want to be using a fully set-based approach wherever possible anyway as thats what SQL Server is optimised for.
I believe the following should accomplish what you are wanting.
CREATE TRIGGER [dbo].[AfterDeleteStockUpdate]
ON [dbo].[ItemLedger]
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;
UPDATE P SET
inStock = L.inStock
, PurchasedValue = L.PurchasedValue
FROM Products P
INNER JOIN (
SELECT SUM(ISNULL(L.inQty, 0)) - SUM(ISNULL(L.outQty, 0)) inStock
, SUM(ISNULL(L.PurchaseAmount, 0)) - SUM(ISNULL(L.Amount, 0)) PruchasedValue
FROM itemledger L
GROUP BY L.product
) L ON L.product = P.id
WHERE P.id IN (SELECT Product from Deleted);
END;

TSQL: Prevent Nulls update over existing data

I have 2 tables:
Orders (Table I update and want keep existing data, and prevent overwriting with nulls)
WRK_Table (This table is identical to Orders but can not guarantee all columns will have data when running update)
PK column 'Master_Ordernum'
There are many columns in the tables, the WRK_Table will have the PK, but data in other columns can not be counted on.
I want the WRK_Table to update Orders only with actual data and not Nulls.
I was thinking along this line:
UPDATE [Orders]
SET [Status] = CASE WHEN S.[Status] IS NOT NULL THEN S.[Status] END
FROM [WRK_TABLE] S
WHERE [Orders].[Master_Ordernum] = S.[Master_Ordernum]
The existing data in Orders is being updated with nulls
does this help?
update orders set name = work.name
from orders
inner join work on orders.id = work.id and work.Name is not null
Not the same col / table name but you should get the idea
EDIT
Your SQL, NOT TESTED
UPDATE Orders
SET [Status] = WRK_Table.[Status]
FROM [Orders]
inner join WRK_Table on [Orders].[Master_Ordernum] = [WRK_Table].[Master_Ordernum] and WRK_Table.[Status] is not null
If you want to test and undo your data just do something like (assuming you don't have 100s of rows)
begin tran
/* .. SQL THAT UPDATES ... */
select * from orders // review the data
rollback tran // Undo changes
If you're going to use a Case statement, you need to include an else to prevent it from just inserting that null. Try this - if the work table has a null, let it just overwrite the value with the existing value.
UPDATE [Orders]
SET [Status] = CASE WHEN S.[Status] IS NOT NULL THEN S.[Status] else [Orders].[Status] END
FROM [WRK_TABLE] S
WHERE [Orders].[Master_Ordernum] = S.[Master_Ordernum]
Not sure if you can use
Update my_table set col = ISNULL(#newval,#existingval) where id=#id
https://msdn.microsoft.com/en-us/library/ms184325.aspx

How can I do a Trigger in SQL?

I am trying to learn about triggers, I know how to do really basic ones, but I can't wrap my head around this. I have two tables Services(master) and Sales.
Services(ServiceID,ServiceCost,SalesTotal)
Sales(TransactionID,TransactionDate,Amount,ServiceID)
I am trying to write trigger for Update,Delete,Insert. When ever you enter a new sale in the Sales table, the SalesTotal will get updated in the Services table according to ServiceID.
ex:
INSERT INTO Sales(TransactionID,TransactionDate,Amount,ServiceID)
VALUES ('16','2014-11-19','50','101');
So if the SalesTotal for TransactionID '101' was 1000, after the insert it would be 1050 and the opposite if I deleted/updated.
I think I have to use join tables, but I am currently stumped.
use this trigger, or split insert / dalete / update:
Code:
CREATE TRIGGER [dbo].[t_Update_Services] on Sales AFTER UPDATE,INSERT, DELETE
AS
BEGIN
UPDATE a
SET SalesTotal = SalesTotal - b.Amount
FROM Services a
JOIN deleted b ON a.ServiceID = b.ServiceID
UPDATE a
SET SalesTotal = SalesTotal + b.Amount
FROM Services a
JOIN inserted b ON a.ServiceID = b.ServiceID
END
Add this inside the trigger
BEGIN
if exists (select * from deleted)
begin
UPDATE Services
SET SalesTotal = SalesTotal - deleted.Amount
FROM Services
JOIN deleted ON Services.ServiceID = deleted.ServiceID
end
else
begin
UPDATE Services
SET SalesTotal = SalesTotal + inserted .Amount
FROM Services
JOIN inserted ON Services.ServiceID = inserted.ServiceID
end
END