I would imagine that this would be an easy question for someone who works a lot with T-SQL and especially Triggers:
I want to enforce the following constraints on all updates and inserts to this table:
If DiscountTypeId = 1, then FlatFee must not be NULL.
If DiscountTypeId = 2, then DiscountRate must not be null.
If either one of these two conditions fail on an insert or update to the table, I'd like to return an appropriate error.
The trigger appears not to do anything yet. .Can you provide the necessary changes so it performs as described?
USE [PandaVisa2008]
GO
/****** Object: Table [dbo].[CustomerSpeed] Script Date: 11/04/2010 15:51:10 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[CustomerSpeed](
[CustomerSpeedId] [int] NOT NULL,
[CustomerId] [int] NULL,
[SpeedId] [int] NOT NULL,
[DiscountTypeId] [int] NOT NULL,
[FlatFee] [money] NULL,
[DiscountRate] [decimal](3, 3) NULL,
CONSTRAINT [PK_AgentFee] PRIMARY KEY CLUSTERED
(USE [PandaVisa2008]
GO
/****** Object: Trigger [dbo].[TRG_CustomerSpeed_OnInsertUpdate] Script Date: 11/04/2010 15:38:06 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER TRIGGER [dbo].[TRG_CustomerSpeed_OnInsertUpdate]
ON [dbo].[CustomerSpeed]
FOR INSERT, UPDATE
AS
BEGIN
DECLARE #DiscountTypeId INT
DECLARE #FlatFee MONEY
DECLARE #DiscountRate DECIMAL(3, 3)
SELECT
#DiscountTypeId = DiscountTypeId,
#FlatFee = FlatFee,
#DiscountRate = DiscountRate
FROM
inserted
IF #DiscountTypeId = 1
AND #FlatFee IS NULL
BEGIN
RAISERROR (N'If #DiscountTypeId is 1, FlatFee must not be NULL',
10,
1)
END
IF #DiscountTypeId = 2
AND #DiscountRate IS NULL
BEGIN
RAISERROR (N'If #DiscountTypeId is 2, #DiscountRate must not be NULL',
10,
1)
END
END
[CustomerSpeedId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[CustomerSpeed] WITH CHECK ADD CONSTRAINT [CK_CustomerSpeed] CHECK (([DiscountRate]>(0) AND [DiscountRate]<(1)))
GO
ALTER TABLE [dbo].[CustomerSpeed] CHECK CONSTRAINT [CK_CustomerSpeed]
GO
EDIT
I got it to work. I haven't read up on Triggers to remedy my fundamental lack of understanding, but t his seemed to work, although I believe that the Check Constraint is the better approach:
ALTER TRIGGER [dbo].[TRG_CustomerSpeed_OnInsertUpdate]
ON [dbo].[CustomerSpeed]
FOR INSERT, UPDATE
AS
BEGIN
IF EXISTS (SELECT
1
FROM
inserted I
WHERE I.DiscountTypeId = 1
AND I.FlatFee IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR (N'If DiscountTypeId is 1, FlatFee must not be NULL',
10,
1)
END
IF EXISTS (SELECT
1
FROM
inserted I
WHERE I.DiscountTypeId = 2
AND I.DiscountRate IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR (N'If DiscountTypeId is 2, DiscountRate must not be NULL',
10,
1)
END
/*
IF #DiscountTypeId = 2
AND #DiscountRate IS NULL
BEGIN
Rollback Transaction
RAISERROR (N'If #DiscountTypeId is 2, DiscountRate must not be NULL',
10,
1)
END
*/
END
Your comments are welcomed.
I'd use a CHECK constraint, not a triggers
ALTER TABLE Mytable WITH CHECK ADD
CONSTRAINT CK_MyTable_GoodName CHECK (
NOT (DiscountTypeId = 1 AND Flatfee IS NULL)
AND
NOT (DiscountTypeId = 2 AND DiscountRate IS NULL)
)
Also, need to consider "if DiscountTypeId <> 1, does Flatfee have to be NULL" etc
You fundamentally do not understand triggers. The very first thing you need to do is go read about triggers in Books Online with particular emphasis on learning about the inserted and deleted psuedotables. Next thing you need to know is a trigger should NEVER be written as if it will handle only one record at a time. Triggers operate on batches of records and trigger code must account for that.
I don't believe triggers can raise errors, problem #1.
Related
I understand that perhaps the problem is that I use a select on the same table that I update or insert a record, but this trigger throws an exception in most cases. Then what should I rewrite?
The purpose of the trigger is to block inserting or updating entries if the room is already occupied on a certain date, i.e. the dates overlap
CREATE TABLE [dbo].[settlements]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[client_id] [int] NOT NULL,
[checkin_date] [date] NOT NULL,
[checkout_date] [date] NOT NULL,
[room_id] [int] NOT NULL,
[employee_id] [int] NULL
);
ALTER TRIGGER [dbo].[On_Hotel_Settlement]
ON [dbo].[settlements]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #room_id int
DECLARE #checkin_date Date, #checkout_date Date
DECLARE cursor_settlement CURSOR FOR
SELECT room_id, checkin_date, checkout_date
FROM inserted;
OPEN cursor_settlement;
FETCH NEXT FROM cursor_settlement INTO #room_id, #checkin_date, #checkout_date;
WHILE ##FETCH_STATUS = 0
BEGIN
IF EXISTS (SELECT 1
FROM settlements AS s
WHERE s.room_id = #room_id
AND ((s.checkin_date >= #checkin_date AND s.checkin_date <= #checkout_date)
OR (s.checkout_date >= #checkin_date AND s.checkout_date <= #checkout_date)))
BEGIN
RAISERROR ('Room is not free', 16, 1);
ROLLBACK;
END;
FETCH NEXT FROM cursor_settlement INTO #room_id, #checkin_date, #checkout_date;
END;
CLOSE cursor_settlement;
DEALLOCATE cursor_settlement;
RETURN
I tried to test the code by removing the condition with dates and leaving only the room _id, but the trigger does not work correctly in this case either.
I tried query like
IF EXISTS (SELECT 1
FROM settlements AS s
WHERE s.room_id = 9
AND ((s.checkin_date >= '2022-12-10' AND s.checkin_date <= '2022-12-30')
OR (s.checkout_date >= '2022-12-10' AND s.checkout_date <= '2022-12-30')))
BEGIN
RAISERROR ('Room is not free', 16, 1);
END;
and it worked correctly. Problem is that is not working in my trigger
As noted by comments on the original post above, the cursor loop is not needed and would best be eliminated to improve efficiency.
As for the date logic, consider a new record with a check-in date that is the same as the checkout date from a prior record. I believe that your logic will consider this an overlap and throw an error.
My suggestion is that you treat the check-in date as inclusive (that night is in use) and the checkout date as exclusive (that night is not in use).
A standard test for overlapping dates would then be Checkin1 < Checkout2 AND Checkin2 < Checkout1. (Note use of inequality.) It may not be obvious, but this test covers all overlapping date cases. (It might be more obvious if this condition is inverted and rewritten as NOT (Checkin1 >= Checkout2 OR Checkin2 >= Checkout1).)
Also, if you are inserting multiple records at once, I would suggest that you also check the inserted records for mutual conflicts.
Suggest something like:
ALTER TRIGGER [dbo].[On_Hotel_Settlement]
ON [dbo].[settlements]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS(
SELECT *
FROM inserted i
JOIN settlements s
ON s.room_id = i.room_id
AND s.checkin_date < i.checkout_date
AND i.checkin_date < s.checkout_date
AND s.id <> i.id
)
BEGIN
RAISERROR ('Room is not free', 16, 1);
ROLLBACK;
END;
RETURN;
END
One more note: Be careful with an early rollback of a transaction. If your overall logic could potentially execute additional DML after the error is thrown, that would now execute outside the transaction and there would be no remaining transaction to roll back.
Take the following script:
EXEC sp_MSforeachtable #command1 = "DROP TABLE ?";
GO
CREATE TABLE _adminServices (
[ServiceID] INT CHECK ([ServiceID] > 0) NOT NULL IDENTITY,
[ServiceName] NVARCHAR(255) DEFAULT NULL,
[ManagerStaffID] INT CHECK ([ManagerStaffID] > 0) DEFAULT NULL,
[ODMStaffID] INT CHECK ([ODMStaffID] > 0) DEFAULT NULL,
[ReferralInactivityDays] INT DEFAULT NULL,
[TargetSupportHours] INT DEFAULT NULL,
[ShowInLists] SMALLINT NOT NULL DEFAULT '1',
[RecordEntryDate] DATETIME2(0) DEFAULT NULL,
[RecordModDate] DATETIME2(0) DEFAULT NULL,
PRIMARY KEY ([ServiceID])
) ;
CREATE INDEX [ManagerStaffID] ON _adminServices ([ManagerStaffID]);
CREATE INDEX [ODMStaffID] ON _adminServices ([ODMStaffID]);
CREATE INDEX [ShowInLists] ON _adminServices ([ShowInLists]);
GO
EXEC sp_MSforeachtable #command1 = 'IF (
select COUNT(TABLE_NAME)
from INFORMATION_SCHEMA.COLUMNS
where COLUMNPROPERTY(object_id(TABLE_SCHEMA+''.''+TABLE_NAME), COLUMN_NAME, ''IsIdentity'') = 1
AND TABLE_SCHEMA+''.''+TABLE_NAME = ''?''
) = 1
BEGIN
SET IDENTITY_INSERT ? ON;
END;';
GO
INSERT INTO _adminServices (ServiceID, ServiceName, ManagerStaffID, ODMStaffID, ReferralInactivityDays, TargetSupportHours, ShowInLists, RecordEntryDate, RecordModDate) VALUES
(1, 'Service 1', 16, 18, 0, NULL, 1, '2017-07-21 11:59:56', '2017-10-25 09:38:02');
GO
When I execute the aforementioned script in SSMS I get the following error:
Msg 544, Level 16, State 1, Line 36
Cannot insert explicit value for identity column in table '_adminServices' when IDENTITY_INSERT is set to OFF.
Can anyone tell me why?
EDIT: My goal is the following: I have multiple tables and multiple inserts.
For the Inserts I have the scripts. Since I don't want to write the SET IDENTITY_INSERT ON and OFF for every table since I just have the INSERT INTO queries on a file, I want a way to do the following:
Delete all the tables in the DB
Create all the tables (I have the SQL for this)
Set all the required identities to ON
Run the Inserts (I have the SQL for this)
Set all the required identities to OFF
sp_MSforeachtable run on another session in relation to your insert
At any time, only one table in a session can have the IDENTITY_INSERT property set to ON.
General scheme of inserting the original value:
CREATE TABLE
SET IDENTITY INSERT ON
INSERT VALUES
SET IDENTITY INSERT OFF
If you table not have single structure sp_MSforeachtable you will not be suitable
I have a small table (code below) with PageKey (int), SendNotification (bit) with 5 possible PageKeys at the moment.
The requirement is to not have more than 1 flag (SendNotification) active at the same time, so I want to create a trigger that would not allow changing the flag to True when there is already another pageKey with active flag.
I wrote the following trigger but it doesn't seem to work. Also, if you have any other (better) solutions to accomplish this in SQL Server 2012, I'd appreciate it.
CREATE TRIGGER trg_Notification_Flag
ON dbo.t_Notification_Flag
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #countOfExistingFlags INT;
DECLARE #PageKey INT;
SELECT #countOfExistingFlags = COUNT(*)
FROM dbo.t_Notification_Flag
WHERE SendNotification = 1;
SELECT #PageKey = inserted.PageKey
FROM inserted;
IF #countOfExistingFlags > 1
BEGIN
PRINT 'There is an existing active flag. No changes has been done.';
ROLLBACK TRANSACTION;
END;
ELSE
BEGIN
UPDATE dbo.t_Notification_Flag
SET SendNotification = 1
WHERE PageKey = #PageKey;
END;
END;
GO
Table is:
CREATE TABLE [dbo].[t_Notification_Flag]
(
[PageKey] [int] NOT NULL,
[SendNotification] [bit] NOT NULL,
[NotificationMessage] [nvarchar](100) NOT NULL,
[NotificationHeader] [nvarchar](50) NULL,
[Country_Key] [int] NULL
) ON [PRIMARY]
You can achieve your goal without trigger - but just with filtered unique index.
Something like:
create table dbo.#Temp (PageKey int, Send_Notification bit)
create unique index IX_Temp on dbo.#temp(Send_Notification) where Send_Notification = 1
This will not allow your table to have more than one record having Send_Notification = 1, so if you need to set Send_Notification flag for some record - you'll have to clear it first for the record currently having this flag set.
I would not use a trigger at all for this. Instead I would use a CHECK CONSTRAINT that checks a UDF that returns true if there are no other active flags, and false if there are.
What I want to do is add a value to a column. Now it might be that the column does not yet exist.
So what I do is check if that column exists and if not I'm adding it and then insert the value.
IF COL_LENGTH('version', 'minor') = NULL
BEGIN
ALTER TABLE version ADD minor INT null;
END
GO
UPDATE version SET minor= 4;
The problem is that the parser complains about this as the column minor does not exist at parse time.
Is there a way to make this pass in a single script?
Use either:
SET ANSI_NULLS OFF
GO
IF COL_LENGTH('version', 'minor') = NULL
BEGIN
ALTER TABLE [version] ADD minor INT null;
END
GO
UPDATE [version] SET minor= 4;
OR
IF COL_LENGTH('version', 'minor') IS NULL
BEGIN
ALTER TABLE [version] ADD minor INT null;
END
GO
UPDATE [version] SET minor= 4;
You are trying to compare NULL = NULL, with ANSI_NULLS ON
Raj
EDIT: I added all of the code for the logon trigger.
Why am I getting some incorrect results when trying to retrieve the total time it takes for a query to run? This is within the context of a logon trigger on SQL Server 2008 R2.
For testing purposes, I want to get the a rough estimate of the total time it takes for a logon trigger to run. Here is a small sample of the results:
LogonId AppId TotalLogonTime (in MS)
101 1 0
253 2 3
289 2 3
985 1 -3
325 1 0
How can the total time evaluate to be negative? Out of the 2.3 million executions so far, there have been 25 that resulted in a time of -3 ms. Here is the code:
CREATE trigger [trgAudit]
on all server
with execute as 'TriggerLogonUser'
for logon
as
begin
set nocount on
set transaction isolation level read committed
declare
#LoginTime datetime
,#IsWindowsUser bit
,#TotalLogonTimeMS int
,#ClientNetAddress varchar(48)
,#MacAddress nchar(12)
,#CurrentUserId int -- UserNames
,#ApplicationId int
,#HonId int --HostName
,#LogonId int --Logon
,#MacId int -- MacAddress
,#CnaId int -- ClientNetAddress
begin try
/*
*** Get the login time, user type, mac address, and client net address
*/
select
#LoginTime = getdate(),
#IsWindowsUser = case
when len(nt_domain) = 0
then 0
else 1
end,
#MacAddress = p.net_address,
#ClientNetAddress = convert(varchar(48),connectionproperty('client_net_address'))
from sys.sysprocesses p
where
p.spid = ##spid
/*
*** Client Net Address
*/
select top 1
#CnaId = CnaId
from master.sysmaintenance.ClientNetAddress with(index(IX_CnaAddress))
where #ClientNetAddress = CnaAddress
--if the ip does not exist, insert it.
if #CnaId is null
begin
insert master.sysmaintenance.ClientNetAddress(CnaAddress)
values (#ClientNetAddress)
select #CnaId = ##identity
end
/*
*** Applications
*/
select top 1
#ApplicationId = AppId
from master.sysmaintenance.Applications with(index(IX_AppName))
where app_name() = AppName
if #ApplicationId is null
begin
insert master.sysmaintenance.Applications (AppName)
values (app_name())
select #ApplicationId = ##identity
end
/*
*** HostName
*/
select top 1
#HonId = HonId
from master.sysmaintenance.HostName with(index(IX_HonName))
where HonName = host_name()
if #HonId is null
begin
insert master.sysmaintenance.HostName
values (host_name())
select #HonId = ##identity
end
/*
*** UserNames
*/
select top 1
#CurrentUserId = UsnId
from master.sysmaintenance.Usernames with(index(IX_UsnName))
where UsnName = original_login()
if #CurrentUserId is null
begin
insert master.sysmaintenance.Usernames
values (original_login())
select #CurrentUserId = ##identity
end
/*
*** MacAddress
*/
select top 1
#MacId = MacId
from master.sysmaintenance.MacAddress with(index(IX_MacAddress))
where MacAddress = #MacAddress
-- same logic is continued as in the applications
if #MacId is null
begin
insert master.sysmaintenance.MacAddress (MacAddress)
values (#MacAddress)
select #MacId = ##identity
end
/*
*** Get the total logon time
*/
select #TotalLogonTimeMS = datediff(ms,#LoginTime, getdate())
-- insert ids of the data gathered on the logon event into the logon table.
insert master.sysmaintenance.Logon ( LogAppId,
LogHonId,
IsWindowsLogon,
CurrentLogonId,
LogonDatetime,
LogCnaId,
LogMacId,
LogonTimeMS )
values ( #ApplicationId,
#HonId,
#IsWindowsUser,
#CurrentUserId,
#LoginTime,
#CnaId,
#MacId,
#TotalLogonTimeMS
)
end try
begin catch
print cast(error_number() as nvarchar(11))
print cast(error_severity() as nvarchar(11))
print cast(error_state() as nvarchar(11))
print cast(error_procedure() as nvarchar(126))
print cast(error_line() as nvarchar(11))
print cast(error_message() as nvarchar(2048))
end catch
end
Here is the DDL to the Logon Table:
CREATE TABLE [sysmaintenance].[Logon](
[LogonId] [bigint] IDENTITY(1,1) NOT NULL,
[LogAppId] [int] NULL,
[LogHonId] [int] NULL,
[LogMacId] [int] NULL,
[LogCnaId] [int] NULL,
[IsWindowsLogon] [bit] NULL,
[CurrentLogonId] [int] NULL,
[LogonDatetime] [datetime] NULL,
[LogonTimeMS] [int] NULL
) ON [PRIMARY]
CREATE UNIQUE CLUSTERED INDEX [PK_Logon] ON [sysmaintenance].[Logon]
(
[LogonId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
Any help or insight is appreciated.
GETDATE():
Returns the current database system timestamp as a datetime value without the database time zone offset. This value is derived from the operating system of the computer on which the instance of SQL Server is running.
Now, bearing in mind that computer clocks drift, I'd imagine that if the computer's time was corrected (either manually or automatically), the time reported by two successive calls to GETDATE() (that aren't part of a single query) could go backwards. All of the datetime methods ultimately rely on the computer's clock.
It's a shame you're not logging one of these GETDATE() results alongside the timing, so you could see if they all occurred at the same "time".