I'm having a problem updating rows in a temporal history table in MS SQL Server 2016.
From the documentation, the steps that should be needed are as follows:
Disable system versioning
Modify the history table
Enable system versioning again
I tried creating a procedure that does this, but got this error:
Msg 13561, Level 16, State 1, Line 23
Cannot update rows in a temporal history table 'db.dbo.FooHistory'.
Here is my SQL:
CREATE TABLE Foo(
id int primary key not null
, title nvarchar(50) not null
, startTime datetime2 GENERATED ALWAYS AS ROW START NOT NULL
, endTime datetime2 GENERATED ALWAYS AS ROW END NOT NULL
, PERIOD FOR SYSTEM_TIME (startTime, endTime) )
ALTER TABLE Foo
SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.FooHistory));
GO
CREATE PROCEDURE [dbo].[UpdateFooHistory] AS
BEGIN
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
ALTER TABLE dbo.Foo SET (SYSTEM_VERSIONING = OFF);
UPDATE dbo.FooHistory
SET title = 'Foo';
ALTER TABLE dbo.Foo SET (SYSTEM_VERSIONING = ON (
HISTORY_TABLE = dbo.FooHistory,
DATA_CONSISTENCY_CHECK = ON
));
COMMIT TRANSACTION
RETURN 0
END
GO
It seems like SQL Server is checking if a table is temporal at "compile time" rather than at runtime. Is this true? Is there a way to work around it?
Change the UPDATE statement to this, and it will let you create the procedure:
EXEC(N'UPDATE dbo.FooHistory SET title = ''Foo''');
With SQL Server 2019 it does not work any more (neither with exec nor with sp_executesql). You need to use a workaround.
In order to prevent any other user to change something while the versioning is off first start a serializable transaction:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
Temporarily disable SYSTEM_VERSIONING on the table:
ALTER TABLE Foo SET (SYSTEM_VERSIONING = OFF);
Modify the history table:
UPDATE dbo.FooHistory SET title = 'Foo';
Re-enable the versioning:
ALTER TABLE Foo SET (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.FooHistory, DATA_CONSISTENCY_CHECK = ON));
Commit the transaction:
COMMIT TRANSACTION;
Related
I am adding the date to a column in SQL when the 'workstatus' is 'completed', but my problem is, when I open and save the same job again in the software, it runs the trigger and changes the date again to a new value which I don't want.
I want the trigger to run only if the 'workstatus' value is something else than 'completed'.
GO
/****** Object: Trigger [dbo].[TRJCD_JOBREQUEST] Script Date: 06/25/2021 15:49:04 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[TRJCD_JOBREQUEST] ON [dbo].[TBL_JOBREQUEST]
AFTER UPDATE,INSERT
AS
if (Update (workstatus))
begin
DECLARE #Jobcompletiondate datetime
DECLARE #workstatus VARCHAR(15)
DECLARE #jobid int
select #workstatus = workstatus from inserted
select #jobid = jobid from inserted
select #Jobcompletiondate = GETDATE()
begin
if #workstatus='Completed'
update TBL_JOBREQUEST set JobCompDate=#Jobcompletiondate where jobid = #jobid
end
end
The following is how you should construct your trigger.
There is no need to assign any values to variables, triggers fire once per batch and always operate on the set of updated rows.
If you update a status to Completed you need to check it's not currently Completed, also if you want to retain the first JobCompDate even if the status is amended afterwards simply use a case expression to only update the column where it's currently NULL.
create or alter trigger [dbo].[TRJCD_JOBREQUEST] on [dbo].[TBL_JOBREQUEST]
after update,insert
as
if ##RowCount=0 return
set nocount on
if Update (workstatus)
begin
update t set
t.JobCompDate=case when t.JobCompDate is null then GetDate() else t.JobCompDate end
from inserted i join TBL_JOBREQUEST t on t.jobid=i.jobid
where i.workstatus='Completed'
and not exists (
select * from deleted d
where d.jobid=i.jobid and d.workstatus=i.workstatus
)
end
Please note that I do not have your data set, so I'm unable to test the trigger, however, based on what you provided in your question, I believe this is the answer you are seeking:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[TRJCD_JOBREQUEST] ON [dbo].[TBL_JOBREQUEST]
AFTER UPDATE,INSERT
AS
if (Update (workstatus))
begin
DECLARE #Jobcompletiondate datetime
DECLARE #currentworkstatus VARCHAR(15)
DECLARE #oldworkstatus VARCHAR(15)
DECLARE #jobid int
select #oldworkstatus = workstatus from deleted
select #currentworkstatus = workstatus from inserted
select #jobid = jobid from inserted
select #Jobcompletiondate = GETDATE()
begin
if #currentworkstatus='Completed' and #oldworkstatus <> 'Completed'
update TBL_JOBREQUEST set JobCompDate=#Jobcompletiondate where jobid = #jobid
end
end
You needed to check if the deleted workstatus does not equal Completed and only then should the trigger fire.
I need to use existing global temp tables or create them and fill if they don't exist.
Example of one table
BEGIN TRANSACTION
if OBJECT_ID('tempdb..##myTable') IS NULL
begin
create table ##myTable (
--stuff
)
end
COMMIT TRANSACTION
BEGIN TRANSACTION
if (select count(*) from ##myTable) = 0
begin
insert into ##myTable
--select stuff
end
COMMIT TRANSACTION
Sometimes it works and sometimes error "Table ##myTable already exists" shows. I am the only one who uses those global temp tables
This is a classic race condition. Multiple sessions can execute the if OBJECT_ID('tempdb..##myTable') IS NULL query at the same time. If the table doesn't exist, both will attempt to create the table and only one will succeed.
One way to address the issue is with an application lock to serialize the code block among multiple sessions. For example:
SET XACT_ABORT ON; --best practice with explict T-SQL transactions
BEGIN TRANSACTION;
EXEC sp_getapplock
#Resource = 'create ##myTable'
,#LockMode = 'Exclusive'
,#LockOwner = 'Transaction';
if OBJECT_ID('tempdb..##myTable') IS NULL
begin
create table ##myTable (
--select stuff
);
end;
COMMIT TRANSACTION; --also releases app lock
I am confused about why this doesn't work:
BEGIN TRANSACTION
SET QUOTED_IDENTIFIER ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
SET CONCAT_NULL_YIELDS_NULL ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE dbo.Accounts ADD CONSTRAINT
CK_client_not_null CHECK (site <> 'LWOP' and client is not null)
GO
ALTER TABLE dbo.Accounts SET (LOCK_ESCALATION = TABLE)
GO
COMMIT
will throw this error:
Msg 547, Level 16, State 0, Line 13
The ALTER TABLE statement conflicted with the CHECK constraint "CK_client_not_null". The conflict occurred in database "ss", table "dbo.Accounts".
Msg 3902, Level 16, State 1, Line 18
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
but when I run the following:
select *
from Accounts
where site = 'LWOP' or client is null
I am getting only that row that can have client as null.
What am I missing here?
Once again: I want to make a constraint that will restrict null values on client column ONLY if site <> 'LWOP', so how to do it?
Thanks,
Dejan
I found the solution already:
BEGIN TRANSACTION
SET QUOTED_IDENTIFIER ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
SET CONCAT_NULL_YIELDS_NULL ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
COMMIT
BEGIN TRANSACTION
GO
ALTER TABLE dbo.Accounts ADD CONSTRAINT
CK_client_not_null CHECK (site <> 'LWOP' and client is not null OR site = 'LWOP')
GO
ALTER TABLE dbo.Accounts SET (LOCK_ESCALATION = TABLE)
GO
COMMIT
site <> 'LWOP' and client is not null OR site = 'LWOP' - that covers all situations for check to work.
I have a script where I'm adding a column to the table, and immediately after I populate that column with data from another table. I'm getting 'Invalid column name' error on the column that I am adding.
The error, specifically, is Invalid column name 'tagID'.
The code between BEGIN TRANSACTION and COMMIT is actually an excerpt of a much larger script, but this is the relevant excerpt (and I need all of it to succeed or simply roll back):
BEGIN TRY
BEGIN TRANSACTION
ALTER TABLE [Items] ADD tagID [uniqueidentifier] NULL
MERGE INTO
Items AS target
USING
Tags AS t ON t.tag = target.tag
WHEN MATCHED THEN
UPDATE SET target.tagID = t.id;
COMMIT
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
GO
SQL Server tries to compile all statements in the batch. If the table doesn't exist compilation of the statement is deferred but there is no deferred compilation for missing columns.
You can use
BEGIN TRY
BEGIN TRANSACTION
ALTER TABLE [Items] ADD tagID [uniqueidentifier] NULL
EXEC('
MERGE INTO
Items AS target
USING
Tags AS t ON t.tag = target.tag
WHEN MATCHED THEN
UPDATE SET target.tagID = t.id;
')
COMMIT
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION
END CATCH
GO
To push the usage of the column into a child batch compiled after the column is created. It still belongs to the same transaction opened in the parent scope.
Transaction scope are for DML operations and not for DDL operations. So not sure why you are having the ALTER statement in the same transaction block. If not wrong, you should be having that ALTER statement outside the transaction block.
ALTER TABLE [Items] ADD tagID [uniqueidentifier] NULL
BEGIN TRANSACTION
MERGE INTO
.....
Also, I would remove those [] from the datatype of the column from your ALTER statement
ALTER TABLE [Items] ADD tagID UniqueIdentifier;
Instead of trigger i am planning to write a procedure which we can run using job which will work same way as TRIGGER
with these two tables in the same way.
how can i do that?
here are my tables with column names
1.tblcal
ID(int,not null)
UID(varchar(10),null)
Desc(varchar(200),null)
Date(datetime,null)
avbl(varchar(5),null)
2.tblEvent
ID(int,notnull)
UID(varchar(10),null)
Desc(varchar(200),null)
Date(datetime,null)
Down is my trigger on tblEvent..
ALTER TRIGGER [dbo].[trU] ON [dbo].[tblEvent]
FOR INSERT
AS
Declare #CuID char(6),
#CuDesc char(40),
#CuDate datetime
SET NOCOUNT ON
Select #CuID = i.UID , #CuDesc=i.Desc, #CuDate=i.Date From Inserted i
If(#CuDesc !='available')
Begin
Update tblCal set avbl='Out', Desc=#CurDesc where cadate=#CuDate and UID=#CuID
ENd
SET NOCOUNT OFF
I have another problem with Desc column.Desc which are going to be in and out Basically we need to update tblcal differently for different descriptions;in that case I don't think trigger is that reliable;Means for example for 10 Desc we need to update in and for other 10 we need to update out
Actually every thursday on the tblevent data is loaded once its loaded it fired a trigger and will update in tblcal.
but my client is looking for a procedure which we can schedule as a job after the tblevent entry done on Thursday.
How can i do with stored procedure?
Procedure
CREATE PROCEDURE dbo.usp_UpdateEventData
AS
BEGIN
SET NOCOUNT ON;
UPDATE C
SET c.avbl = 'Out'
,c.[Desc] = e.[Desc]
FROM [dbo].tblCal C
INNER JOIN [dbo].[tblEvent] e ON c.[UID] = e.[UID]
AND c.cadate = e.[Date] --<-- check if you only want
WHERE e.[Desc] <> 'available' -- to join on date not datetime
END -- CAST both columns to DATE
Also if you are keeping your Trigger as it is you will need to modify the trigger definition to handle multiple Inserts, You can use the same logic as in this procedure to update your trigger definition.
Trigger Fix
ALTER TRIGGER [dbo].[trU] ON [dbo].[tblEvent]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE C
SET c.avbl = 'Out'
,c.[Desc] = i.[Desc]
FROM [dbo].tblCal C
INNER JOIN inserted i ON c.[UID] = i.[UID]
AND c.cadate = i.[Date]
WHERE i.[Desc] <> 'available'
END