Will a constraint be applied between two statements during a transaction? - sql

I have a configuration that a thing can have multiple properties and a property can belong to multiple things:
CREATE TABLE thing (
id integer NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE property (
id integer NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE thingproperty (
thing integer NOT NULL,
property integer NOT NULL
);
ALTER TABLE thingproperty
ADD CONSTRAINT tp_thing
FOREIGN KEY (thing) REFERENCES thing(id)
ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE thingproperty
ADD CONSTRAINT tp_property
FOREIGN KEY (property) REFERENCES property(id)
ON UPDATE CASCADE ON DELETE CASCADE;
I'd like to ensure that a property can only exist if it belongs to at least one thing, and for this I wrote this transaction that removes things (and then also properties if necessary) but I don't know whether it's correct:
START TRANSACTION;
DELETE FROM thing ... ;
DELETE FROM property
WHERE id NOT IN (
SELECT property
FROM thingproperty
);
COMMIT TRANSACTION;
So I basically trusted the engine that after the DELETE FROM thing ... query had run, it immediately applies the tp_thing constraint and removes the record(s) of thingproperty belonging to the deleted thing(s) before DELETE FROM property ... is executed.
Is this a safe way to do it?

By default constraints are applied immediately after every command in a transaction. You can (in this case you do not want to) change this behaviour declaring a constraint as deferrable. Read more about deferrable constraints in the documentation.
DEFERRABLE
NOT DEFERRABLE
This controls whether the constraint can be deferred. A constraint that is not deferrable will be checked immediately after every command. (...)

Related

Foreign key constraint cycles or multiple cascade paths

I have a database design like below. I have 3 tables Compartment, CompartmentRelation and CompartmentRelationType . CompartmentRelation table keeps the other compartments around the selected compartment (below,above,behind,infront,etc). CompartmentRelationType keeps the position. Think that i have compartments in the Compartment table named comp-1, comp-2, comp-3, comp-4 and insert the the compartments above comp-1 as comp-2,comp-3 in CompartmentRelation as below. Problem is that setting delete action as cascade for the column RelatedCompId in CompartmentRelation table throw the excaption as
Unable to create relationship 'FK_CompartmentRelation_Compartment1'.
Introducing FOREIGN KEY constraint 'FK_CompartmentRelation_Compartment1' on table 'CompartmentRelation' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
Which way should i follow ?
Compartment
comp-1
comp-2
comp-3
comp-4
Compartment Relation
comp-1 -> comp-2
comp-1 -> comp-3
CREATE TABLE [dbo].[Compartment] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (500) NOT NULL,
CONSTRAINT [PK_Compartment] PRIMARY KEY CLUSTERED ([Id] ASC),
CREATE TABLE [dbo].[CompartmentRelation] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[CompId] INT NOT NULL,
[RelationTypeId] INT NOT NULL,
[RelatedCompId] INT NOT NULL,
CONSTRAINT [PK_CompartmentRelation] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_CompartmentRelation_CompartmentRelationType] FOREIGN KEY ([RelationTypeId]) REFERENCES [dbo].[CompartmentRelationType] ([Id]) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT [FK_CompartmentRelation_Compartment1] FOREIGN KEY ([RelatedCompId]) REFERENCES [dbo].[Compartment] ([Id]),
CONSTRAINT [FK_CompartmentRelation_Compartment] FOREIGN KEY ([CompId]) REFERENCES [dbo].[Compartment] ([Id]) ON DELETE CASCADE ON UPDATE CASCADE);
CREATE TABLE [dbo].[CompartmentRelationType] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (200) NOT NULL,
[NameLan1] NVARCHAR (200) NOT NULL,
[NameLan2] NVARCHAR (200) NULL,
CONSTRAINT [PK_CompartmentRelationType] PRIMARY KEY CLUSTERED ([Id] ASC)
);
Problem is that setting delete action as cascade for the column
RelatedCompId in CompartmentRelation table throw the excaption as
Unable to create relationship
'FK_CompartmentRelation_Compartment1'. Introducing FOREIGN KEY
constraint 'FK_CompartmentRelation_Compartment1' on table
'CompartmentRelation' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other
FOREIGN KEY constraints. Could not create constraint or index.
See previous errors.
The basic issue appears to be that deletes from table Compartment (and updates to its PKs) can cascade to CompartmentRelation through two different foreign keys. If you intend to support Compartments being related to themselves, then that's end-of-story for cascading from Compartment to CompartmentRelation -- you can't do it.* If you intend to forbid self relationships then you could try adding a check constraint to CompartmentRelation to enforce that prohibition, though I'm not at all sure that SQL Server will take that into account.
If SQL Server won't accept the cascading deletes then you have at least three options:
Make it an application responsibility to clean up compartment relationships before deleting compartments. (And don't cascade.)
Create triggers to handle relationship deletion when compartments are deleted. (And don't cascade.)
Create a stored procedure for deleting compartments, and make it handle the needed relationship deletions. (And don't cascade.)
Which way should i follow ?
Whichever of those makes the most sense for your application. All have advantages and disadvantages.
Additionally,
Do not cascade updates of surrogate key columns, especially when the key values are machine generated, as all yours are. Those keys should never be updated in the first place, and if an attempt were ever made to update one then it would be better for the DB to reject it, for whatever reason, than to accept it.
You probably don't want to cascade deletions of CompartmentRelationType to ComponentRelation. Including such cascading allows for deleting all the relations of a given type by deleting the type itself, but such a cascade is more likely to be performed mistakenly than intentionally, and if it were performed mistakenly then the resulting data loss would be significant. It's probably better to make the application delete all those relations explicitly if that's what it really means to do, and otherwise to reject deletion of types that are in use by existing relations.
*Technically, you could do it by cascading from only one of the two FKs with Compartment, but it seems unlikely that such a half-measure would serve your purposes.

I have a problem with this query in Postgres

I have a problem with this table in Postgres, it give me this error:
ERROR: cannot use subquery in check constraint
LINE 66: check(Artista in(Select ID_Artista
create table DirigeF(
Artista int references Artista(ID_Artista) on delete cascade,
Film int references Film(ID_Contenuto) on delete cascade,
check(Artista in(Select ID_Artista
from Artista
where tipologia='REGISTA'or'AR')),
constraint DirigeF_PK primary key(Artista, Film)
);
I want to check that Artista in table DirigeF has tipologia='REGISTA' from another table.
As the error suggests, you cannot do this with a check constraint. One option is a trigger. Another is a foreign key constraint -- but that needs to be carefully arranged.
First you need a column that indicates whether the type in Artista is "REGISTA" or "AR". That would be:
alter table artista add is_regista_ar bool generated always as
(tipologia in ('REGISTA', 'AR'));
Then create a unique constraint or index:
alter table artista add unq_artista_tipologia_id
unique (is_regista_ar, id_artista)
Note: This requires Postgres 12+. But something similar can be done in earlier versions.
Then, add a boolean column to your table that is always true:
create table DirigeF (
Artista int references Artista(ID_Artista) on delete cascade,
Film int references Film(ID_Contenuto) on delete cascade,
is_regista_ar bool generated always as true,
constraint fk_artista_tipo_artista foreign key (is_regista_ar, Artista) references Artista(is_regista_ar, ID_Artista),
constraint DirigeF_PK primary key (Artista, Film)
);

May cause cycles or multiple cascade paths

I have a task:
If a "shop" is deleted all references to it is set to NULL.
When I try to create a table:
CREATE TABLE TEST
(
id int Primary Key,
shop int FOREIGN KEY REFERENCES TEST(id) ON DELETE SET NULL,
);
I get an error:
Introducing FOREIGN KEY constraint 'FK__TEST__shop__2882FE7D' on table 'TEST' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Msg 1750, Level 16, State 0, Line 1
What am I doing wrong?
You are getting an error because you are creating a table with a reference to itself. This is not a good idea, and also this is not what you want.
For your task, you need to create another table, with a relationship (a foreign key) that references your first table. The foreign key must be defined properly to hold the rule that sets the child to NULL when the parent is deleted, like :
CONSTRAINT fk_name
FOREIGN KEY (child_col)
REFERENCES parent_table (parent_col)
ON DELETE SET NULL
See this link on how to set option ON DELETE in a foreign key
You can do this, but not exactly as you wish. The following works:
CREATE TABLE TEST (
id int Primary Key,
shop int,
FOREIGN KEY (shop) REFERENCES TEST(id) ON DELETE NO ACTION,
);
To be honest, I'm not sure why the in-line definition doesn't work. The more important point is the action. The SET NULL is not allowed, because SQL Server is very cautious about potential cycles. However, NO ACTION is allowed.

Unique key with Empty values

This is the schema for the table, here defined a unique constraint with job_category_id, screening_type_id, test_id, sex.
CREATE TABLE job_profile
(
profile_id numeric(5,0) NOT NULL,
job_category_id numeric(5,0),
test_id numeric(5,0),
sex character(1),
default_yn character(1),
screening_type_id numeric(5,0),
CONSTRAINT job_profile_pkey PRIMARY KEY (profile_id),
CONSTRAINT fk_jobprofile_jobcate FOREIGN KEY (job_category_id)
REFERENCES job_category_mast (job_category_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT fk_jobprofile_test FOREIGN KEY (test_id)
REFERENCES test_mast (test_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT fk_prof_screentype FOREIGN KEY (screening_type_id)
REFERENCES screening_type_mast (screening_type_id) MATCH SIMPLE
ON UPDATE RESTRICT ON DELETE RESTRICT,
CONSTRAINT uk_job_test_sex_screening UNIQUE (job_category_id, screening_type_id, test_id, sex)
)
WITH (
OIDS=FALSE
);
ALTER TABLE job_profile
OWNER TO cdcis;
GRANT ALL ON TABLE job_profile TO cdcis;
GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE job_profile TO cdcis_app;
If one field is empty then the unique constraint fails here.
How to add constraint so that one empty value is accepted, so it will be unique.
Can handle this scenario in the application using JPA?
According to the documentation:
Null values are not considered equal
So unique constraint won't work this way.
You have 2 options:
Create a trigger that will manually check the data integrity and deny changes if the table contains more than one empty value.
Set default value to 0 and NOT NULL constraint on these columns. In that case, you will be able to have only one row containing empty (zero) value.
Update:
As Abelisto suggested, it can be easily done with functional indexes.
CREATE UNIQUE INDEX uix_job_test_sex_screening on job_profile(coalesce(job_category_id, -1), screening_type_id, test_id, sex);

Is there any "reverse" ON DELETE CASCADE option?

Let's say I have the following database in SQL Server:
CREATE TABLE [Order]
(
ID BIGINT IDENTITY(1,1)
CONSTRAINT PK_Order PRIMARY KEY CLUSTERED (ID)
);
CREATE TABLE OrderItem
(
ID BIGINT IDENTITY(1,1),
ORDER_ID BIGINT NOT NULL,
PRICE_ID BIGINT NOT NULL,
DISCOUNTED_PRICE_ID BIGINT NULL,
CONSTRAINT PK_OrderItem PRIMARY KEY CLUSTERED (ID)
);
CREATE TABLE Price
(
ID BIGINT IDENTITY(1,1),
AMOUNT FLOAT NOT NULL,
CURRENCY VARCHAR(3) NOT NULL,
CONSTRAINT PK_Price PRIMARY KEY CLUSTERED (ID)
);
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_Order
FOREIGN KEY (ORDER_ID) REFERENCES [Order](ID) ON DELETE CASCADE;
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_Price
FOREIGN KEY (PRICE_ID) REFERENCES Price(ID);
ALTER TABLE OrderItem ADD CONSTRAINT FK_OrderItem_DiscountedPrice
FOREIGN KEY (DISCOUNTED_PRICE_ID) REFERENCES Price(ID);
If I delete an order, all order items will be deleted (because of ON DELETE CASCADE on FK_OrderItem_Order constraint), but corresponding prices (normal and discounted) will remain in the database forever.
Is there any option in SQL Server (or generic SQL) to delete corresponding prices from Price table?
I can think of a trigger which is a perfect match, but it is too much hassle for such simple (and common) task. I would prefer to specify something on my constraints (FK_OrderItem_Price and FK_OrderItem_DiscountedPrice) that basically say "this is one-to-one relationship", delete parent (Price is a parent table in this case) if a child was deleted.
In a nutshell: no. Cascading works only from parent to child1, not the other way around.
It could be argued that some parents should be removed when they lose the last of their children, but that's simply not how current DBMSes are implemented.
You'll have to use a trigger for such "special" referential action, or a batch job it it doesn't have to happen immediately. Or hide the operations behind some sort of API (stored procedure, middle-tier method) that does that explicitly.
See also: order stability.
1 In you case, Order and Price both act as parents to OrderItem.
Add the OrderItemID to Price and set up a cascade delete relationship. This column is of course redundant but it allows you to have a cascade delete in the right direction.
Consider inlining the Price table two times into OrderItems. As this is a 1:1 relationship you can do that. It is a matter of taste whether you like this solution or not.
you can create a trigger which acts as a reverse cascade:
DELIMITER $$
CREATE TRIGGER reverse_cascade_OrderItem_Price
AFTER DELETE ON `OrderItem`
FOR EACH ROW
BEGIN
DELETE FROM `Price` WHERE ID = old.PRICE_ID;
END$$