Here is my setting: within my schema I have a stored procedure (shown below) which will insert a dummy row (for testing) into a remote table. The table MY_REMOTE_TABLE is a synonym pointing to the correct remote table.
I can successfully query it like
SELECT * FROM MY_REMOTE_TABLE;
on my local system. This does work.
So for testing I created the procedure with a dummy test value in it, which should be inserted into the remote table if the row is new to the remote table. Hint: the remote table is empty at the time of insertion, so it really should perform the insert.
But it always fails giving me a return value of -6. I have no clue what -6 stands for or even what the error could be. All I have is -6 and I know that nothing will be inserted into the remote table. Interestingly when I copy the insert statement to the remote server, replace the synonym with the real table name on the remote machine and execute, it works all fine.
So I'm really lost here seeking for your help!
CREATE PROCEDURE [dbo].[my_Procedure]
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN DISTRIBUTED TRANSACTION
-- LEFT JOIN AND WHERE NULL WILL ONLY FIND NEW RECORDS -> THEREFORE INSERT INTO TARGET REMOTE TABLE
INSERT INTO MY_REMOTE_TABLE
(--id is not needed because it's an IDENTITY column
user_id,
customer_id,
my_value, year,
Import_Date, Import_By, Change_Date, Change_By)
SELECT
Source.user_id,
Source.customer_id,
Source.my_value,
Source.year,
Source.Import_Date,
Source.Import_By,
Source.Change_Date,
Source.Change_By
FROM
(SELECT
null as id,
126616 as user_id,
17 as customer_id,
0 as my_value,
2012 as year,
GETDATE() AS Import_Date,
'test' AS Import_By,
GETDATE() AS Change_Date,
'test' AS Change_By) AS Source
LEFT JOIN
MY_REMOTE_TABLE AS Target ON Target.id = Source.id
AND Target.user_id = Source.user_id
AND Target.customer_id = Source.customer_id
AND Target.year = Source.year
WHERE
Target.id IS NULL; -- BECAUSE OF LEFT JOIN NEW RECORDS WILL BE NULL, SO WE ONLY SELECT THEM FOR THE INSERT !!!
IF (##TRANCOUNT > 0 AND XACT_STATE() = 1)
BEGIN
COMMIT TRANSACTION
END
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0AND XACT_STATE() = -1)
ROLLBACK TRANSACTION
END CATCH;
END
another question related to this one. if my insert would violate an FK constraint on my remote table, how could I manage to promote the error message from the remote DB server to my local procedure to capture it?
Look here: http://msdn.microsoft.com/de-de/library/ms188792.aspx
Short version:
XACT_ABORT must be set ON for data modification statements in an
implicit or explicit transaction against most OLE DB providers,
including SQL Server. The only case where this option is not required
is if the provider supports nested transactions.
So insert a SET XACT_ABORT ON at the start of the stored procedure.
Related
To ensure version control, I created a For Update trigger on my table. I have two tables. Account table, step one Second, the Account history table, which is utilized in the trigger, has a column called Version. If any of my columns are modified, I have Version+1 written in the column, and the old record from the Account table will be inserted in the Account history in the trigger. Additionally, I have a trigger new condition written. The newer version ought to be grated. version, If I run an update query on my main (Account) table to perform negative testing while keeping the older version, I get a trigger-defined error, but my update statement still updates the Account table, even though it shouldn't. I need to add transaction(BEGIN TRY BEGIN CATCH TRAN) on my update?, If my trigger fails my update statement should fail
ALTER TRIGGER tr_AccountHistory
ON account
FOR UPDATE
AS
BEGIN
SELECT old.column
FROM deleted
SELECT new.Version
FROM inserted
SELECT old.Version FROM deleted
IF #Old_Version >= #New_Version
BEGIN
RAISERROR ('Improper version information provided',16,1);
END
ELSE
BEGIN
INSERT INTO AccountHistory
(
insert column
)
VALUES
(
old.column
);
END
END
UPDATE account
SET id= 123456,
Version = 1
WHERE id =1
Instead of using RAISERROR, you should use THROW. This will respect XACT_ABORT and automatically rollback the transaction.
You also have other fatal flaws in your trigger:
It expects there to be exactly one row modified. It may be multiple or zero rows.
You have not declared any variables and are instead selecting back out to the client.
Either way, you should just join the inserted and deleted tables by primary key.
CREATE OR ALTER TRIGGER tr_AccountHistory
ON account
FOR UPDATE
AS
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM inserted) -- early bailout
RETURN;
IF EXISTS (SELECT 1
FROM inserted i
JOIN deleted d ON d.YourPrimaryKey = i.YourPrimaryKey
WHERE d.Version >= i.Version
)
THROW 50001, 'Improper version information provided', 1;
INSERT INTO AccountHistory
(
insert column
)
SELECT
columsHere
FROM deleted;
I'm currently trying to write a canonical schema to multiple related tables within a SQL DB, but I'm experience DUPLICATE KEY ID conflicts when it's evaluating whether the record exists prior to UPDATING/INSERTING.
BizTalk receives change records from the student management system every 5 minutes, maps them to a stored procedure and then calls that procedure which writes the changes to 5 tables in our master database.
I believe this is because I'm using an incorrect design pattern in the stored procedure.
Current Design:
IF EXISTS (Select student_id FROM student_modules WHERE student_id #student_id and module_id = #module_id)
-- THEN UPDATE THE RECORD
ELSE
-- INSERT THE RECORD
Logically this makes sense, but as BizTalk receives 2 change records with the exact same student and module ID at the same time, and then attempts to call the stored procedure for each record.
SQL then panics, because whilst it's evaluating the logic in the first message, it tries to execute the INSERT whilst evaluating the same logic in the second message - and tells me I'm trying to insert a DUPLICATE KEY.
I've tried using an UPSERT pattern that i found at the below link (design below), but that seems to lock the student_modules table completely.
BEGIN TRANSACTION;
UPDATE dbo.t WITH (UPDLOCK, SERIALIZABLE) SET val = #val WHERE [key] = #key;
IF ##ROWCOUNT = 0
BEGIN
INSERT dbo.t([key], val) VALUES(#key, #val);
END
COMMIT TRANSACTION;
https://sqlperformance.com/2020/09/locking/upsert-anti-pattern
Is there a cleaner approach to this that I'm missing?
You could use the MERGE Transact-SQL command
INSERT tbl_A (col, col2)
SELECT col, col2
FROM tbl_B
WHERE NOT EXISTS (SELECT col FROM tbl_A A2 WHERE A2.col = tbl_B.col);
You will also want to consider either changing your Orchestration so that it subscribes to further updates for the same student ID (a singleton type pattern) or to set your send port to ordered delivery, to prevent trying to update the same record at the same time.
I have a database Users that has four fields: Name, Client, ID, Time. Client is an integer (0-99). How to write a trigger that will find latest user from Users (latest according to Time) during Insert and if the Client of this user equals Client of inserted user then I'd like to Rollback
I tried like this:
CREATE TRIGGER DoubledData ON Users
FOR INSERT
AS
DECLARE #client DECIMAL(2)
DECLARE #client_old DECIMAL(2)
DECLARE #name Varchar(50)
SELECT #name = Name from inserted
SELECT #client = Client from inserted
//This doesn't work, "Syntax error near Select":
SELECT #client_old = Select top(1) Client from Users where Name like #name order by Time desc;
IF #client = #client_old
BEGIN
ROLLBACK
END
The problem is that I can assign same values to Client for one user but they can't be one after another (eg for client this order is correct 1-2-3-1-3 -> order is important, but this isn't correct: 1-2-3-3 -> after 2nd occurrence of '3' in a row it needs to be rollbacked)
I'm using MS SQL
[EDIT]
I have found that I can execute it without Select top(1) like:
SELECT #client_old = Client from Users where Name like #name order by Time desc;
But the trigger doesn't execute afer insert
First, you clearly don't understand triggers in SQL Server and the inserted pseudo-tables. These can have more than one row, so your code will fail when multiple rows are inserted. Sadly, there is no compile check for this situation. And code can unexpectedly fail (even in production, alas).
Second, the right way to do this is probably with a unique constraint. That would be:
alter table users
add constraint unq_users_name_client unique (name, client);
This would ensure no duplication, so it is a stronger condition than your trigger.
I am running SQL Server 2008. I written code that should be pretty safe to prevent any records being deleted or updated, but would be much happier if I could do this at the database level. Is it possible to mark a table so that once a row has been inserted it can never be modified or deleted?
Edit per comments. It seems you are actually looking for Versioning which really shouldn't be done via triggers but it can be with a performance impact and a Lot more coding.
Most appropriate method to combat your concern. Maintain transactional backups every X# of minutes so you can roll back if it gets messed up.
However sql-server does have 2 change tracking methods built in that you could explore.
Change Tracking - which simply identifies if a record was modified, and is really useful when synchronizing changes to a DB. Basically it increments a BIGINT for every row for every operation so you just have to check for records greater than the previous synchronize number.
Change Data Capture - this will capture the insert/update/delete and the state of the record (row).
For your particular worry Change Data Capture might be plausible and way more easy to maintain than triggers. I haven't tried it myself because Change Tracking was enough for my needs but here is a link to Microsoft's Documentation https://msdn.microsoft.com/en-us/library/cc645937(v=sql.110).aspx
If you insist on Triggers here would be an example, you will have to maintain the original primary key for referential integrity or you might not know which change caused the problem
CREATE TABLE TblName (
PrimaryKeyID INT IDENTITY(1,1) NOT NULL PRIMARY KEY
,Col1 INT NULL
,Col2 INT NULL
,OriginalPrimaryKeyId INT NULL
,CreateDate DATETIME DEFAULT(GETDATE())
,UpdateDate DATETIME DEFAULT(GETDATE())
,IsLatestVersion BIT NOT NULL DEFAULT(1)
)
GO
CREATE TRIGGER dbo.TrigForInsertEnforceVersionColTblName ON dbo.TblName
FOR INSERT
AS
BEGIN
BEGIN TRY
IF (SELECT COUNT(*) FROM TblName WHERE IsLatestVersion <> 1 AND OriginalPrimaryKeyId IS NULL) > 0
BEGIN
;THROW 51000, 'Attempted to insert a record identified as Previous Version without referencing another record', 1
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK TRANSACTION
--this will mean the loss of some Primary Keys but it is better than an
--INSTEAD of INSERT because you wont have to handle the insert code
;THROW
END CATCH
END
GO
CREATE TRIGGER dbo.TriggerName ON dbo.TblName
INSTEAD OF UPDATE, DELETE
AS
BEGIN
BEGIN TRY
IF EXISTS (
SELECT *
FROM
TblName t
INNER JOIN inserted i
ON t.PrimaryKeyID = i.PrimaryKeyID
AND (t.OriginalPrimaryKeyId <> i.OriginalPrimaryKeyId
OR t.IsLatestVersion <> i.IsLatestVersion)
)
BEGIN
;THROW 51000, 'OriginalPrimaryKeyId Column or IsLatestVersion Column was attempted to be updated', 1
END
--don't have to test count can just run the update statement
IF ((SELECT COUNT(*) FROM inserted) > 0)
BEGIN
--It's an UPDATE Operations so insert new row but maintain original primary key
--so you know what the new row is a version of
INSERT INTO dbo.TblName (Col1, Col2, OriginalPrimaryKeyId)
SELECT
i.Col1
,i.Col2
,OriginalPrimaryKeyId = CASE
WHEN t.OriginalPrimaryKeyId IS NULL THEN t.PrimaryKeyID
ELSE t.OriginalPrimaryKeyId
END
FROM
inserted i
INNER JOIN TblName t
ON i.PrimaryKeyID = t.PrimaryKeyID
END
UPDATE t
SET IsLatestVersion = 0
,UpdateDate = GETDATE()
FROM
TblName t
INNER JOIN deleted d
ON t.PrimaryKeyID = d.PrimaryKeyID
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK TRANSACTION
;THROW
END CATCH
END
Permissions discussion:
For anything row level and to block everyone including database owner role or administrators you would have to create a trigger. But those roles could always remove the trigger too and modify the table. Perhaps simple Permissions would be enough such as
on an entire role, which would be best if you put the users in a role
GRANT INSERT ON SchemaName.TableName TO RoleName
DENY UPDATE ON SchemaName.TableName TO RoleName
DENY DELETE ON SchemaName.TableName TO RoleName
OR for specific users same commands just change RoleName to username
DENY UPDATE ON SchemaName.TableName TO UserName
This would grant the ability to insert but revoke ability to update or delete a record.
You can also deny execute, alter, and a bunch more here is Microsoft's documentation: https://msdn.microsoft.com/en-us/library/ms173724.aspx
Using a trigger instead of security permissions is a lot messier and if someone with enough permissions wants to make a change they still can it will just slow them down but not by much. So if you are worried about that ability make sure you have good backups.
Im trying to make this trigger work when trying to delete a record. The way it is suposed to work is, when someone tries to delete a record it rollbacks and inserts an audit record to TbAudit table which by the way, all columns have a NOT NULL constraint. However, turns out it wont do it, because for some reason I dont understand when I try to delete a record it will display the message and rollback BUT all my variables within the select statement are getting NULL values even though Im pulling them directly from the "deleted" table. Please help.
USE BdPlan
GO
CREATE TRIGGER TrAudit
ON Plan.TPlan
AFTER DELETE
AS
BEGIN
DECLARE #IdPlan = int,
#IdEmployee int,
#Month int,
#Year int
ROLLBACK
PRINT 'CANT DELETE RECORDS'
-- All variables are getting NULL
SELECT #IdPlan = D.IdPlan,
#IdEmployee = D.IdEmployee ,
#Month = D.Month,
#Year = D.Year
FROM deleted AS D
INSERT INTO BdAudit.dbo.TbAudit
VALUES
(
#IdPlan,
#IdEmployee,
#Month,
#Year,
SUSER_NAME(),
GETDATE()
)
END
I believe there may be problems with this approach:
you are trying to access the DELETED pseudotable after the transaction has been rolled back - it will have zero rows after the rollback (see below)
your trigger only attempts to deal with a single row deletion - it should be able to handle multi row deletes
It is also noted that inserting directly into the Audit table from the Deleted pseudotable before ROLLBACK will of course roll the audit data back as well.
From here it is apparent you can cache the data to be audited in a #Temporary table variable, then do the ROLLBACK (which doesn't undo the #Temp table), and then do the Audit insertion:
ALTER trigger d_foo ON FOO AFTER DELETE
AS BEGIN
DECLARE #Temp AS TABLE
(
ID INT,
-- Obviously add all your fields go here
);
INSERT INTO #Temp(ID)
SELECT ID FROM DELETED;
ROLLBACK TRAN;
insert into fooaudit(id)
select id from #Temp;
END;
Simplified SqlFiddle here with multiple row deletion.
To confirm, the DELETED pseudotable contains zero rows after a ROLLBACK in a trigger, as this modified Fiddle demonstrates.