I am trying to figure out how to perform on update cascade on a self-referencing temporal table using triggers. While I found that this post (On delete cascade for self-referencing table) is the closest it can get to my answer, I had the below questions:
The Answer and the question seems incomplete in the post. Can you please tell me what is contained in Deleted table in the post? What is id in Deleted table and Comments table? Is it a primary key? What if primary key is a pair of columns? Also, I am not sure why is IDs inside the CTE IDs. Seems incorrect. I am not sure.
What would the on update cascade specific trigger look like if the table has other foreign key constraints?
I have the below table setup. Can you please help me create a trigger for it on update cascade on the foreign key FK_Son_Height_Weight?
I could use a surrogate key here but there are several tables that have foreign key reference to PK_Height_Weight. Is there a way to make sure I dont need a surrogate key?
(Note: the table has been modified for privacy reasons)
CREATE TABLE no.Man (
Height varchar(100) NOT NULL,
Weight varchar(50) NOT NULL,
CONSTRAINT PK_Height_Weight PRIMARY KEY (Height, Weight),
CONSTRAINT FK_Weight FOREIGN KEY (Weight)
REFERENCES no.Human (Weight)
On Update Cascade,
Son_Height varchar (100) NOT NULL,
Son_Weight varchar (50) NOT NULL,
CONSTRAINT FK_Son_Height_Weight FOREIGN KEY (Son_Height, Son_Weight)
REFERENCES no.Man(Height,Weight)
On Update Cascade
)
Since the table is a temporal table, only 'After Update' trigger can be used on it and not the 'Instead Of Update' trigger (link for reference). I used the below solution for now to set stuff to null. This isnt a complete answer - yet trying to share with the community what I know.
CREATE or ALTER TRIGGER no.Weight_Height
ON no.Man
After UPDATE
AS
Set nocount on
IF ( UPDATE (Height) AND UPDATE (Weight) )
;With BA_DEL_Join as
(
select d.Height as d_Height, d.Weight as d_Weight,
d.Son_Height_Number as d_Son_Height_Num, d.Son_Weight as
d_Son_Weight, ba.Height as ba_Height, ba.Weight as ba_Weight,
ba.Son_Height_Number as ba_Son_Height_Num, ba.Son_Weight as
ba_Son_Weight
from deleted d
inner join no.Man ba
on d.Height = ba.Son_Height_Number and d.Weight = ba.Son_Weight
)
Update no.Man
set Son_Height_Number = NULL, Son_Weight = NULL
from BA_DEL_Join
where d_Height = ba_Son_Height_Num and d_Weight = ba_Son_Weight
Go
Even though this code mimics a cascading event - it does not enfore referential integrity.
(For background I'm using Postgres 12.4)
I'm unclear why deletes work when there are circular FKs between two tables and both FKs are set to ON DELETE CASCADE.
CREATE TABLE a (id bigint PRIMARY KEY);
CREATE TABLE b (id bigint PRIMARY KEY, aid bigint references a(id) on delete cascade);
ALTER TABLE a ADD COLUMN bid int REFERENCES b(id) ON DELETE CASCADE ;
insert into a(id) values (5);
insert into b(id, aid) values (10,5);
update a set bid = 10 where id=5;
DELETE from a where id=5;
The way that I am thinking about this, when you delete the row in table 'a' with PK id = 5, postgres looks at tables that have a referential constraint referencing a(id), it finds b, it tries to delete the row in table b with id = 10, but then it has to look at tables referencing b(id), so it goes back to a, and then it should just end up in an infinite loop.
But this does not seem to be the case. The delete completes without error.
It's also not the case, as some sources say online, that you cannot create the circular constraint. The constraints are created successfully, and neither of them is deferrable.
So my question is - why does postgres complete this circular cascade even when neither constraint is set to deferrable, and if it's able to do so, then what is the point of even having a DEFERRABLE option?
Foreign key constraints are implemented as system triggers.
For ON DELETE CASCADE, this trigger will run a query like:
/* ----------
* The query string built is
* DELETE FROM [ONLY] <fktable> WHERE $1 = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
* ----------
*/
The query runs is a new database snapshot, so it cannot see rows deleted by previous RI triggers:
/*
* In READ COMMITTED mode, we just need to use an up-to-date regular
* snapshot, and we will see all rows that could be interesting. But in
* transaction-snapshot mode, we can't change the transaction snapshot. If
* the caller passes detectNewRows == false then it's okay to do the query
* with the transaction snapshot; otherwise we use a current snapshot, and
* tell the executor to error out if it finds any rows under the current
* snapshot that wouldn't be visible per the transaction snapshot. Note
* that SPI_execute_snapshot will register the snapshots, so we don't need
* to bother here.
*/
This makes sure that no RI trigger will try to delete the same row a second time, and thus circularity is broken.
(All quotations taken from src/backend/utils/adt/ri_triggers.c.)
How could I set a constraint on a table so that only one of the records has its isDefault bit field set to 1?
The constraint is not table scope, but one default per set of rows, specified by a FormID.
Use a unique filtered index
On SQL Server 2008 or higher you can simply use a unique filtered index
CREATE UNIQUE INDEX IX_TableName_FormID_isDefault
ON TableName(FormID)
WHERE isDefault = 1
Where the table is
CREATE TABLE TableName(
FormID INT NOT NULL,
isDefault BIT NOT NULL
)
For example if you try to insert many rows with the same FormID and isDefault set to 1 you will have this error:
Cannot insert duplicate key row in object 'dbo.TableName' with unique
index 'IX_TableName_FormID_isDefault'. The duplicate key value is (1).
Source: http://technet.microsoft.com/en-us/library/cc280372.aspx
Here's a modification of Damien_The_Unbeliever's solution that allows one default per FormID.
CREATE VIEW form_defaults
AS
SELECT FormID
FROM whatever
WHERE isDefault = 1
GO
CREATE UNIQUE CLUSTERED INDEX ix_form_defaults on form_defaults (FormID)
GO
But the serious relational folks will tell you this information should just be in another table.
CREATE TABLE form
FormID int NOT NULL PRIMARY KEY
DefaultWhateverID int FOREIGN KEY REFERENCES Whatever(ID)
From a normalization perspective, this would be an inefficient way of storing a single fact.
I would opt to hold this information at a higher level, by storing (in a different table) a foreign key to the identifier of the row which is considered to be the default.
CREATE TABLE [dbo].[Foo](
[Id] [int] NOT NULL,
CONSTRAINT [PK_Foo] PRIMARY KEY CLUSTERED
(
[Id] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[DefaultSettings](
[DefaultFoo] [int] NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[DefaultSettings] WITH CHECK ADD CONSTRAINT [FK_DefaultSettings_Foo] FOREIGN KEY([DefaultFoo])
REFERENCES [dbo].[Foo] ([Id])
GO
ALTER TABLE [dbo].[DefaultSettings] CHECK CONSTRAINT [FK_DefaultSettings_Foo]
GO
You could use an insert/update trigger.
Within the trigger after an insert or update, if the count of rows with isDefault = 1 is more than 1, then rollback the transaction.
CREATE VIEW vOnlyOneDefault
AS
SELECT 1 as Lock
FROM <underlying table>
WHERE Default = 1
GO
CREATE UNIQUE CLUSTERED INDEX IX_vOnlyOneDefault on vOnlyOneDefault (Lock)
GO
You'll need to have the right ANSI settings turned on for this.
I don't know about SQLServer.But if it supports Function-Based Indexes like in Oracle, I hope this can be translated, if not, sorry.
You can do an index like this on suposed that default value is 1234, the column is DEFAULT_COLUMN and ID_COLUMN is the primary key:
CREATE
UNIQUE
INDEX only_one_default
ON my_table
( DECODE(DEFAULT_COLUMN, 1234, -1, ID_COLUMN) )
This DDL creates an unique index indexing -1 if the value of DEFAULT_COLUMN is 1234 and ID_COLUMN in any other case. Then, if two columns have DEFAULT_COLUMN value, it raises an exception.
The question implies to me that you have a primary table that has some child records and one of those child records will be the default record. Using address and a separate default table here is an example of how to make that happen using third normal form. Of course I don't know if it's valuable to answer something that is so old but it struck my fancy.
--drop table dev.defaultAddress;
--drop table dev.addresses;
--drop table dev.people;
CREATE TABLE [dev].[people](
[Id] [int] identity primary key,
name char(20)
)
GO
CREATE TABLE [dev].[Addresses](
id int identity primary key,
peopleId int foreign key references dev.people(id),
address varchar(100)
) ON [PRIMARY]
GO
CREATE TABLE [dev].[defaultAddress](
id int identity primary key,
peopleId int foreign key references dev.people(id),
addressesId int foreign key references dev.addresses(id))
go
create unique index defaultAddress on dev.defaultAddress (peopleId)
go
create unique index idx_addr_id_person on dev.addresses(peopleid,id);
go
ALTER TABLE dev.defaultAddress
ADD CONSTRAINT FK_Def_People_Address
FOREIGN KEY(peopleID, addressesID)
REFERENCES dev.Addresses(peopleId, id)
go
insert into dev.people (name)
select 'Bill' union
select 'John' union
select 'Harry'
insert into dev.Addresses (peopleid, address)
select 1, '123 someplace' union
select 1,'work place' union
select 2,'home address' union
select 3,'some address'
insert into dev.defaultaddress (peopleId, addressesid)
select 1,1 union
select 2,3
-- so two home addresses are default now
-- try adding another default address to Bill and you get an error
select * from dev.people
join dev.addresses on people.id = addresses.peopleid
left join dev.defaultAddress on defaultAddress.peopleid = people.id and defaultaddress.addressesid = addresses.id
insert into dev.defaultaddress (peopleId, addressesId)
select 1,2
GO
You could do it through an instead of trigger, or if you want it as a constraint create a constraint that references a function that checks for a row that has the default set to 1
EDIT oops, needs to be <=
Create table mytable(id1 int, defaultX bit not null default(0))
go
create Function dbo.fx_DefaultExists()
returns int as
Begin
Declare #Ret int
Set #ret = 0
Select #ret = count(1) from mytable
Where defaultX = 1
Return #ret
End
GO
Alter table mytable add
CONSTRAINT [CHK_DEFAULT_SET] CHECK
(([dbo].fx_DefaultExists()<=(1)))
GO
Insert into mytable (id1, defaultX) values (1,1)
Insert into mytable (id1, defaultX) values (2,1)
This is a fairly complex process that cannot be handled through a simple constraint.
We do this through a trigger. However before you write the trigger you need to be able to answer several things:
do we want to fail the insert if a default exists, change it to 0 instead of 1 or change the existing default to 0 and leave this one as 1?
what do we want to do if the default record is deleted and other non default records are still there? Do we make one the default, if so how do we determine which one?
You will also need to be very, very careful to make the trigger handle multiple row processing. For instance a client might decide that all of the records of a particular type should be the default. You wouldn't change a million records one at a time, so this trigger needs to be able to handle that. It also needs to handle that without looping or the use of a cursor (you really don't want the type of transaction discussed above to take hours locking up the table the whole time).
You also need a very extensive tesing scenario for this trigger before it goes live. You need to test:
adding a record with no default and it is the first record for that customer
adding a record with a default and it is the first record for that customer
adding a record with no default and it is the not the first record for that customer
adding a record with a default and it is the not the first record for that customer
Updating a record to have the default when no other record has it (assuming you don't require one record to always be set as the deafault)
Updating a record to remove the default
Deleting the record with the deafult
Deleting a record without the default
Performing a mass insert with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record inserts
Performing a mass update with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record updates
Performing a mass delete with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record deletes
#Andy Jones gave an answer above closest to mine, but bearing in mind the Rule of Three, I placed the logic directly in the stored proc that updates this table. This was my simple solution. If I need to update the table from elsewhere, I will move the logic to a trigger. The one default rule applies to each set of records specified by a FormID and a ConfigID:
ALTER proc [dbo].[cpForm_UpdateLinkedReport]
#reportLinkId int,
#defaultYN bit,
#linkName nvarchar(150)
as
if #defaultYN = 1
begin
declare #formId int, #configId int
select #formId = FormID, #configId = ConfigID from csReportLink where ReportLinkID = #reportLinkId
update csReportLink set DefaultYN = 0 where isnull(ConfigID, #configId) = #configId and FormID = #formId
end
update
csReportLink
set
DefaultYN = #defaultYN,
LinkName = #linkName
where
ReportLinkID = #reportLinkId
If I have a table, T, with a primary key, ID,
CREATE TABLE T (
ID INT NOT NULL PRIMARY KEY,
Data BIT NOT NULL DEFAULT (0)
)
and create an after insert trigger on it
CREATE TRIGGER T_Trigger ON T
FOR INSERT, UPDATE, DELETE
AS
BEGIN
-- Trigger body
END
is it ever possible that the INSERTED table will have duplicate values for the ID column? Or for the DELETED table to have duplicate values for the ID column?
Clearly it's possible for the ID to appear once in each of the INSERTED and DELETED tables.
I can't imagine a DML statement that could lead to a trigger firing with the same primary key occurring twice in either the INSERTED or DELETED table. Even MERGE is carefully documented to say that "The MERGE statement cannot update the same row more than once, or update and delete the same row." The PRIMARY KEY constraint on ID and the limited power of each DML statement seem like they should combine to keep it unique in each trigger firing.
No. The INSERTED and DELETED tables are the final (i.e. "after") state of the operation, in terms of FOR / AFTER triggers. For INSTEAD OF triggers, the INSERTED and DELETED tables are what would have been the final state. Well, the DELETED table is really the prior state of those rows whereas INSERTED is the current state.
Do Foreign Key constraints get checked on an SQL update statement that doesn't update the columns with the Constraint? (In MS SQL Server)
Say I have a couple of tables with the following columns:
OrderItems
- OrderItemID
- OrderItemTypeID (FK to a OrderItemTypeID column on another table called OrderItemTypes)
- ItemName
If I just update
update [dbo].[OrderItems]
set [ItemName] = 'Product 3'
where [OrderItemID] = 2508
Will the FK constraint do it's lookup/check with the update statement above? (even thought the update is not change the value of that column?)
No, the foreign key is not checked. This is pretty easy to see by examining the execution plans of two different updates.
create table a (
id int primary key
)
create table b (
id int,
fkid int
)
alter table b add foreign key (fkid) references a(id)
insert into a values (1)
insert into a values (2)
insert into b values (5,1) -- Seek on table a's PK
update b set id = 6 where id = 5 -- No seek on table a's PK
update b set fkid = 2 where id = 6 -- Seek on table a's PK
drop table b
drop table a
No. Since the SQL update isn't updating a column containing a constraint, what exactly would SQL Server be checking in this case? This is similar to asking, "does an insert trigger get fired if I only do an update?" Answer is no.
There is a case when the FK not existing will prevent updates to other columns even though the FK is not changed and that is when the FK is created WITH NOCHECK and thus not checked at the time of creation. Per Books Online:
If you do not want to verify new CHECK or FOREIGN KEY constraints
against existing data, use WITH NOCHECK. We do not recommend doing
this, except in rare cases. The new constraint will be evaluated in
all later data updates. Any constraint violations that are suppressed
by WITH NOCHECK when the constraint is added may cause future updates
to fail if they update rows with data that does not comply with the
constraint.