SQL: Can I write a CHECK constraint that validates data in a foreign table using a foreign key? - sql

I am designing a test database in SQL Server 2008 R2 and/or SQL Azure. (All of my code will run on both, so far.)
I have a table with a foreign key, and I need to add a constraint that references a field in the foreign table.
Normally I would have the foreign table manage it's own validation checks, but there are cases where that is impossible (or illogical). I've provided some sample-code that displays what I am trying to accomplish.
CREATE TABLE CustomerOrder
(
ID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES Customer(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
ProductID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES Product(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
Quantity INT NOT NULL DEFAULT 1,
IsPaid BIT NOT NULL DEFAULT 0
)
GO
CREATE TABLE RMA
(
ID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerOrderID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES CustomerOrder(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
-- Add constraint to prevent RMAs from being
-- created for orders that have not been paid.
-- This could be a column constraint, or a table constraint.
CHECK ( CustomerOrderID.IsPaid = 1 )
-- ERROR: 'The multi-part identifier "CustomerOrderID.IsPaid"
-- could not be bound.'
)
GO
In this example, it doesn't make sense to put the CHECK constraint in the CustomerOrder table, because a row in the CustomerOrder table is perfectly happy being unpaid as long as there are no RMAs for the order. Furthermore, a constraint in the CustomerOrder table would still need to reference the RMA table to confirm whether there is an RMA, so the same issue remains.
I've also tried:
CHECK (EXISTS(SELECT co.ID FROM CustomerOrder co
WHERE co.ID=CustomerOrderID AND
co.IsPaid=1))
-- ERROR: 'Subqueries are not allowed in this context.
-- Only scalar expressions are allowed.'
Since this is a static constraint for basic data validation, and will never be referenced by any other object, I'd like to avoid making this into a scalar function or stored procedure.
However, in order to avoid using a scalar function, I will need to define the constraint within SQL (preferrably at the same time my database is deployed and the table is created).
What SQL syntax could I use here to define this type of constraint?

Another option (since you asked for a solution without adding triggers or enlarging the foreign key), is removing the IsPaid column and adding another table for paid orders:
CREATE TABLE CustomerOrder
(
ID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES Customer(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
ProductID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES Product(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
Quantity INT NOT NULL DEFAULT 1
)
GO
CREATE TABLE CustomerOrderPaid
(
ID INT NOT NULL PRIMARY KEY
FOREIGN KEY REFERENCES CustomerOrder(ID)
ON DELETE NO ACTION ON UPDATE CASCADE
)
GO
CREATE TABLE RMA
(
ID INT NOT NULL IDENTITY PRIMARY KEY,
CustomerOrderID INT NOT NULL UNIQUE
FOREIGN KEY REFERENCES CustomerOrderPaid(ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
)
GO

Related

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)
);

Check if data exists in another table on insert?

Table A
(
Table_A_ID int
)
Table B
(
Table_B_ID int
Value int
)
Say I want to insert data into Table B, where 'Value' would be the same as a Table_A_ID.
How would I make a constraint or check that the data actually exists in the table on insertion?
You probably need to enforce data integrity not only on INSERT into Table B, but also on UPDATE and DELETE in both tables.
Anyway options are:
FOREIGN KEY CONSTRAINT on Table B
TRIGGERs on both tables
As a last resort if for some reason 1 and 2 is not an option STORED PROCEDUREs for all insert, delete update operations for both tables
The preferred way to go in most cases is FOREIGN KEY CONSTRAINT.
Yap, I agree with #peterm.
Cause, if your both Table_A_ID and Table_B_Id are primary keys for both tables, then you don't even need two tables to store the value. Since, your two tables are seems to be on 'one-to-one' relationship. It's one of the database integrity issues.
I think you didn't do proper normalisation for this database.
Just suggesting a good idea!
I found this example which demonstrates how to setup a foreign key constraint.
Create employee table
CREATE TABLE employee (
id smallint(5) unsigned NOT NULL,
firstname varchar(30),
lastname varchar(30),
birthdate date,
PRIMARY KEY (id),
KEY idx_lastname (lastname)
) ENGINE=InnoDB;
Create borrowed table
CREATE TABLE borrowed (
ref int(10) unsigned NOT NULL auto_increment,
employeeid smallint(5) unsigned NOT NULL,
book varchar(50),
PRIMARY KEY (ref)
) ENGINE=InnoDB;
Add a constraint to borrowed table
ALTER TABLE borrowed
ADD CONSTRAINT FK_borrowed
FOREIGN KEY (employeeid) REFERENCES employee(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
NOTE: This tells MySQL that we want to alter the borrowed table by adding a constraint called ‘FK_borrowed’. The employeeid column will reference the id column in the employee table – in other words, an employee must exist before they can borrow a book.
The final two lines are perhaps the most interesting. They state that if an employee ID is updated or an employee is deleted, the changes should be applied to the borrowed table.
NOTE: See the above URL for more details, this is just an excerpt from that article!
Create a foreign key constraint on the column 'Value' on table B that references the 'Table_A_ID' column.
Doing this will only allow values that exist in table A to be added into the 'Value' field of table B.
To accomplish this you first need to make Table_A_ID column the primary key for table A, or it at least has to have some sort of unique constraint applied to it to be a foreign key candidate.
BEGIN TRANSACTION -- REMOVE TRANSACTION AND ROLLBACK AFTER DONE TESTING
--PUT A PRIMARY KEY ON TABLE A
CREATE TABLE A
( Table_A_ID int CONSTRAINT PK_A_Table_A_ID PRIMARY KEY)
--ON VALUE ADD A FOREIGN KEY CONSTRAINT THAT REFERENCEs TABLE A
CREATE TABLE B
( Table_B_ID int,
[Value] int CONSTRAINT FK_B_Value_A REFERENCES A(Table_A_ID)
)
-- TEST VALID INSERT
INSERT A (Table_A_ID) VALUES (1)
INSERT B (Table_B_ID, [Value]) VALUES (1,1)
--NOT ALLOW TO INSERT A VALUE THAT DOES NOT EXIST IN A
--THIS WILL THROW A FOREIGN KEY CONSTRAINT ERROR
INSERT B (Table_B_ID, [Value]) VALUES (1,2) -- 2 DNE in table A
ROLLBACK
Note: there is no magic to 'FK_B_Value_A' or 'PK_A_Table_A_ID' it simply a naming convention and be called anything. The syntax on the foreign key and primary key lines work like this:
column-definition CONSTRAINT give-the-constraint-a-name REFERENCES table-name ( table-column )
column-definition CONSTRAINT give-the-constraint-a-name PRIMARY KEY

Why is this a cyclical foreign key constraint?

I came upon this code, marked "error," in an application I'm to update. Running it on a test database gives a cyclical reference error:
The referential relationship will result in a cyclical reference that is not allowed (Constraint name = descriptions_fk_2)
I named the constraints to see which one caused the problem.
CREATE TABLE items (
id INT NOT NULL UNIQUE IDENTITY,
name NCHAR(100) NOT NULL UNIQUE,
PRIMARY KEY (id)
);
CREATE TABLE sources (
id INT NOT NULL UNIQUE IDENTITY,
item_id INT NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (item_id)
REFERENCES items(id) ON UPDATE NO ACTION ON DELETE CASCADE
);
CREATE TABLE descriptions (
id INT NOT NULL UNIQUE IDENTITY,
item_id INT NOT NULL,
source_id INT NOT NULL,
PRIMARY KEY (id),
CONSTRAINT descriptions_fk_1 FOREIGN KEY (item_id)
REFERENCES items(id) ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT descriptions_fk_2 FOREIGN KEY (source_id)
REFERENCES sources(id) ON UPDATE NO ACTION ON DELETE CASCADE
);
Why is this a cyclical reference? The descriptions table is linked to two separate tables, but none of them link back to descriptions.
It's not strictly cyclical - but there are multiple cascade paths. So you could cascade delete a row in items two ways:
1) description -> item
2) description -> source -> item
And, for that reason, it's disallowed.
I believe it's a performance concern, as PostGres will allow cycles like that and will just work it out, but deletes under those circumstances can be quite slow.
For some further reading about why it's disallowed, please see this answer.

Sybase constraint with foreign key and conditional check against foreign table column value

Is it possible on Sybase to define a constraint(s) which require a column to be a foreign key and also satisfy a condition based on the value of a foreign column, e.g. in the below example tables could a constraint be created on the "product" table such that "product.code is a foreign key of a brand.code which has valid=1"?
CREATE TABLE brand (
code char(8) NOT NULL,
valid int NOT NULL,
rowid numeric(10,0) IDENTITY,
CONSTRAINT brand_pk PRIMARY KEY (code),
CONSTRAINT valid_check CHECK (valid IN (0,1))
)
CREATE TABLE product (
code char(8) NOT NULL,
CONSTRAINT product_pk PRIMARY KEY (code)
)
I think it's best to change the structure just a little bit.
CREATE TABLE brand (
code char(8) NOT NULL,
valid int NOT NULL,
rowid numeric(10,0) IDENTITY,
CONSTRAINT brand_pk PRIMARY KEY (code),
-- The following UNIQUE constraint lets the pair of values be the target of
-- a foreign key reference.
CONSTRAINT brand_is_valid UNIQUE (code, valid),
CONSTRAINT valid_check CHECK (valid IN (0,1))
);
CREATE TABLE product (
code char(8) NOT NULL,
valid int NOT NULL,
-- The column "code" is a PK in the referenced table, so this still works. It's
-- a 1:0 or 1:1 relationship.
CONSTRAINT product_pk PRIMARY KEY (code),
-- The next constraint requires a unique constraint on the pair of
-- columns in the table "brand". By itself, it references every row
-- in "brand". That's too many rows.
CONSTRAINT product_fk FOREIGN KEY (code, valid)
REFERENCES brand (code, valid),
-- But this constraint restricts the foreign key references to only those
-- rows that have valid = 1 in the table "brand".
CHECK (valid = 1)
);
In order to bypass the creation of a foreign-key based on the "valid" condition, you will need to modify your table design and create a trigger to set the product.code = NULL. Pardon my syntax (I haven't coded Sybase for a while), but this is the general idea:
Add a new column to serve as the primary key since we will need to set product.code = NULL when valid=0:
CREATE TABLE product (
rowid int identity primary key,
code char(8) NULL,
CONSTRAINT brand_fk FOREIGN KEY (code) REFERENCES brand(code)
)
Then create a trigger similar to this one:
create trigger FK_WhenValid
on product
for insert
AS
IF (SELECT COUNT(*) FROM brand b inner join inserted i on b.code = i.code AND b.valid=0 ) > 0
BEGIN
UPDATE product SET code = NULL WHERE code in (SELECT i.code from brand b join inserted i on b.code = i.code and b.valid = 0)
END
Note: this trigger only supports product insertions. If "valid" can change, another approach is required.
You could also implement the foreign key as a trigger instead of a declarative constraint and only set product.code = inserted.code when valid = 1

How can I get around this foreign key constraint having to have a unique name?

I'm not sure why these have to be unique, but from reading the MySQL forums it appears that they do. However, I think it has something more to do with the INDEX name. I have two tables that have foreign key constraints referencing the same primary key on a third table. If it helps, I'm using MySQL workbench to design the schema.
I usually name my foreign key on each table the same name as the primary key it references. I guess this isn't possible. It will create the first table with the foreign key constraint, but when it tries to create the second table it throws an error. Here is the second table it throws the error on:
CREATE TABLE IF NOT EXISTS `joe`.`products_to_categories` (
`product_to_category_id` INT NOT NULL AUTO_INCREMENT ,
`category_id` INT NOT NULL ,
`product_id` INT NOT NULL ,
PRIMARY KEY (`product_to_category_id`) ,
INDEX `category_id` (`category_id` ASC) ,
INDEX `product_id` (`product_id` ASC) ,
CONSTRAINT `category_id`
FOREIGN KEY (`category_id` )
REFERENCES `joe`.`categories` (`category_id` )
ON DELETE CASCADE
ON UPDATE NO ACTION,
CONSTRAINT `product_id`
FOREIGN KEY (`product_id` )
REFERENCES `joe`.`products` (`product_id` )
ON DELETE CASCADE
ON UPDATE NO ACTION)
ENGINE = InnoDB;
I want the foreign key names to be the same as the primary key in both of the other tables. What should I remove here so that I can use these names. What is the best practice here.
It is not possible because you would have a conflict in the filename for the file that is used for the index IIRC. I probably would name the key < tablename >_< column_name > or something like that.
You are creating an index (constraint) by the name of product_id via:
INDEX product_id
Then you are going and creating another constraint (for the foreign key) with the same name:
CONSTRAINT product_id
What you need to do is allow the server to provide a default, unique constraint name by removing the
CONSTRAINT product_id
See this URL: http://dev.mysql.com/doc/refman/5.1/en/innodb-foreign-key-constraints.html
"If the CONSTRAINT symbol clause is given, the symbol value must be unique in the database. If the clause is not given, InnoDB creates the name automatically."
In PostgreSQL, the default for naming indexes is to append "_pkey" and "_fkey" to the name of the primary and foreign key, respectively. So your case would look like:
INDEX `product_id_fkey` (`product_id` ASC) ,
UPDATE: I just tried this and it worked. See if that's what you had in mind.
use test;
create table if not exists test.product
(
product_id int not null auto_increment,
name varchar(80) not null,
primary key(product_id)
);
create table if not exists test.category
(
category_id int not null auto_increment,
name varchar(80) not null,
primary key(category_id)
);
create table if not exists test.product_category
(
product_id int,
category_id int,
primary key(product_id, category_id),
constraint product_id_fkey
foreign key(product_id) references product(product_id)
on delete cascade
on update no action,
constraint category_id_fkey
foreign key(category_id) references category(category_id)
on delete cascade
on update no action
);
insert into test.product(name) values('teddy bear');
insert into test.category(name) values('toy');
insert into test.product_category
select p.product_id, c.category_id from product as p, category as c
where p.name = 'teddy bear' and c.name = 'toy';