Relational table that is constrained to a subset of its referee - sql

Say I have the following table:
create table foo (
id integer primary key,
mode integer not null check (mode in (1, 2))
);
Now I want to store additional information about records from this table, but only those that have, say, mode = 1. The way I thought I could do this is as follows:
create table foo_bar (
id integer primary key,
foo_id integer,
_mode integer not null default (1) check (_mode = 1),
foreign key (foo_id, _mode) references foo(id, mode)
);
That is, we have a dummy column in foo_bar that is forcibly always equal to 1 and include that in the foreign key constraint with foo.
However, not only does this not work (with pragma foreign_keys = ON;), but that foreign key constraint can be violated when you insert multiple values into foo!
sqlite> insert into foo(mode) values (1);
sqlite> insert into foo(mode) values (2);
sqlite> select * from foo;
1|1
2|2
sqlite> insert into foo_bar(foo_id) values (1);
Error: foreign key mismatch - "foo_bar" referencing "foo"
sqlite> insert into foo_bar(foo_id) values (2);
Error: foreign key mismatch - "foo_bar" referencing "foo"
sqlite> insert into foo(mode) values (1), (2);
Error: foreign key mismatch - "foo_bar" referencing "foo"
Is this a bug in SQLite (I'm using 3.17, it it matters), or am I doing it wrong?
An alternative option, without the foreign key, might be to use a trigger:
create trigger bad_mode
before insert on foo_bar when (select mode from foo where id = NEW.foo_id) <> 1
begin
select raise(fail, "Invalid mode");
end;
...but this seems a bit gross!

The documentation says:
Usually, the parent key of a foreign key constraint is the primary key of the parent table. If they are not the primary key, then the parent key columns must be collectively subject to a UNIQUE constraint or have a UNIQUE index.
That missing UNIQUE constraint is what causes the "foreign key mismatch" error.
It is not possible to use a subquery in a CHECK constraint, so the only way to enforce the mode = 1 constraint is to make it part of the foreign key constraint, or to use triggers.

I think you could improve the design, and no constraints other than FK are needed:
First of all foo.mode could be a foreign key to another table, say modes, no need to use a check constraint. You would store the values 1 and 2 there.
A second table, say bar_modes, could have a field mode, a FK to modes. You would store the value 1 in this table.
Finally, foo_bar.foo_id would be FK to foo.id, and foo_bar.mode a FK to bar_modes.mode.
Let me know what you think!
SQL Server example for M = 3, N = 5
create table modes (
id int primary key
)
create table foo (
id int primary key,
mode int not null foreign key references modes(id)
);
create table bar_modes (
id int primary key foreign key references modes(id)
)
create table foo_bar (
id int primary key,
foo_id int foreign key references foo(id),
other_data varchar(20)
)
insert modes (id) values(1)
insert modes (id) values(2)
insert modes (id) values(3)
insert modes (id) values(4)
insert modes (id) values(5)
insert bar_modes (id) values (1)
insert bar_modes (id) values (2)
insert bar_modes (id) values (3)
insert foo (id, mode) values (1000, 1)
insert foo (id, mode) values (2000, 2)
insert foo (id, mode) values (2500, 2)
insert foo (id, mode) values (5000, 5)
insert foo_bar (id, foo_id, other_data) values (100, 1000, 'data for foo 1000')
insert foo_bar (id, foo_id, other_data) values (200, 2000, 'data for foo 2000')
insert foo_bar (id, foo_id, other_data) values (250, 2500, 'data for foo 2500')

Related

Many-to-Many Link Table Foreign Key Modeling in SQLite

I have the following two tables in SQLite:
CREATE TABLE `Link` (
`link_id` integer NOT NULL,
`part_id` integer NOT NULL,
CONSTRAINT `link_pk` PRIMARY KEY(`link_id`,`part_id`)
);
CREATE TABLE `Main` (
`main_id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
`link_id` integer NOT NULL REFERENCES `Link`(`link_id`)
);
INSERT INTO `Link` (link_id, part_id) VALUES (1,10);
INSERT INTO `Link` (link_id, part_id) VALUES (1,11);
INSERT INTO `Link` (link_id, part_id) VALUES (1,12);
INSERT INTO `Link` (link_id, part_id) VALUES (2,15);
INSERT INTO `Main` (main_id, link_id) VALUES (1,1);
INSERT INTO `Main` (main_id, link_id) VALUES (2,1);
INSERT INTO `Main` (main_id, link_id) VALUES (3,2);
Many Main rows may reference the same link id, and many Link rows may have the same link id, such that select * from Main natural join Link where main_id=1 will return N rows, and select * from Main where link_id=1 will return K rows. The link id is important, and the original data each main has 1 link id, and each link has N part ids.
Using the schemas above, I am unable to insert any rows in Main due to the foreign key constraint (foreign key mismatch - "Main" referencing "Link": INSERT INTO Main (main_id, link_id) VALUES (1,1);), presumably because of the composite key requirement. I can get this to work by removing the foreign key constraint, but then I am obviously missing a constraint. Reversing the direction of the key wouldn't work either since, as stated above, it's a Many-to-Many relationship. Is there a way to properly model this in SQLite with a constraint that at least one row exists in Link for each link_id in Main?
I would propose a different design.
Each of the 2 entities link_id and part_id should be the primary key in 2 tables, something like:
CREATE TABLE Links (
link_id INTEGER PRIMARY KEY,
link_description TEXT
);
CREATE TABLE Parts (
part_id INTEGER PRIMARY KEY,
part_description TEXT
);
Then, create the junction table of the above tables (like your current Link table):
CREATE TABLE Links_Parts (
link_id INTEGER NOT NULL REFERENCES Links(link_id),
part_id INTEGER NOT NULL REFERENCES Parts(part_id),
PRIMARY KEY(link_id, part_id)
);
and the table Main:
CREATE TABLE Main (
main_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
link_id INTEGER NOT NULL REFERENCES Links(link_id)
);
All the relations are there and you have referential integrity guaranteed if you set foreign key support:
PRAGMA foreign_keys = ON;
See a simplified demo.

Ensure combination of two columns existing in another table

How could one ensure the combination of two columns existing in another table?
CREATE TABLE a (
aid INTEGER PRIMARY KEY
);
CREATE TABLE b (
aid INTEGER REFERENCES a(aid),
bid INTEGER,
PRIMARY KEY (aid, bid)
);
CREATE TABLE c (
aid INTEGER REFERENCES a(aid),
cid INTEGER,
PRIMARY KEY (aid, cid)
);
-- combination of (aid, bid) must come from b
-- combination of (aid, cid) must come from c
CREATE TABLE d (
aid INTEGER REFERENCES a(aid),
bid INTEGER REFERENCES b(bid),
cid INTEGER REFERENCES c(bid),
PRIMARY KEY (aid, bid)
);
INSERT INTO a values(1);
INSERT INTO a values(2);
INSERT INTO b values(1, 1);
INSERT INTO b values(2, 2);
INSERT INTO c values(1, 1);
INSERT INTO c values(2, 2);
INSERT INTO d values(1, 2, 2);
Obviously the above "CREATE TABLE d" coding failed to ensure
combination of (aid, bid) must come from b
combination of (aid, cid) must come from c
Thanks.
In addition to Sticky bit and Vladimir Baranov's answers about using FOREIGN KEY and REFERENCES, for sqlite3 users:
"Foreign key constraints are disabled by default (for backwards compatibility), so must be enabled separately for each database connection." That means, the user has to open a database first, then run command "PRAGMA foreign_keys = ON;".
The pragma will not work if "the version of SQLite you are using does not support foreign keys (either because it is older than 3.6.19 or because it was compiled with SQLITE_OMIT_FOREIGN_KEY or SQLITE_OMIT_TRIGGER defined)".
You cannot define multicolumn foreign key constraints inline. Define them on their own.
CREATE TABLE d
(aid integer,
bid integer,
cid integer,
PRIMARY KEY (aid,
bid),
FOREIGN KEY (aid,
bid) REFERENCES b (aid,
bid),
FOREIGN KEY (aid,
cid) REFERENCES c (aid,
cid));
You didn't specify what RDBMS you are using. In SQL Server you can create a foreign key using more than one column. The key should point to columns that are in a unique or primary key and you do have the required primary keys in tables B and C.
The definitions of tables A, B and C can remain as they are in the question.
The definition of table D would look like this in SQL Server syntax. Here the foreign key constraints are created using separate ALTER TABLE statements. Different DBMS may use somewhat different syntax.
CREATE TABLE [dbo].[d](
[aid] [int] NULL,
[bid] [int] NULL,
[cid] [int] NULL
)
GO
ALTER TABLE [dbo].[d] WITH CHECK ADD CONSTRAINT [FK_d_b] FOREIGN KEY([aid], [bid])
REFERENCES [dbo].[b] ([aid], [bid])
GO
ALTER TABLE [dbo].[d] CHECK CONSTRAINT [FK_d_b]
GO
ALTER TABLE [dbo].[d] WITH CHECK ADD CONSTRAINT [FK_d_c] FOREIGN KEY([aid], [cid])
REFERENCES [dbo].[c] ([aid], [cid])
GO
ALTER TABLE [dbo].[d] CHECK CONSTRAINT [FK_d_c]
GO
It works as expected in my test:
INSERT INTO a values(1);
INSERT INTO a values(2);
INSERT INTO b values(1, 1);
INSERT INTO b values(2, 2);
INSERT INTO c values(1, 1);
INSERT INTO c values(2, 2);
(1 row affected)
(1 row affected)
(1 row affected)
(1 row affected)
(1 row affected)
(1 row affected)
Now let's try to insert into D:
INSERT INTO d values(1, 2, 2);
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_d_b".
The conflict occurred in database "TestDB", table "dbo.b".
The statement has been terminated.
These inserts go through, as expected:
INSERT INTO d values(1, 1, 1);
INSERT INTO d values(2, 2, 2);
(1 row affected)
(1 row affected)
OR you can use this syntax:
CREATE TABLE d2 (
aid INTEGER,
bid INTEGER,
cid INTEGER,
FOREIGN KEY (aid,bid) REFERENCES b (aid,bid),
FOREIGN KEY (aid,cid) REFERENCES c (aid,cid)
);
In this case the foreign keys would get some autogenerated names, but they will work the same:
INSERT INTO d2 values(1, 2, 2);
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the FOREIGN KEY constraint "FK__d2__351DDF8C".
The conflict occurred in database "TestDB", table "dbo.b".
The statement has been terminated.
INSERT INTO d2 values(1, 1, 2);
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted with the FOREIGN KEY constraint "FK__d2__361203C5".
The conflict occurred in database "TestDB", table "dbo.c".
The statement has been terminated.
These inserts work, as expected:
INSERT INTO d2 values(1, 1, 1);
INSERT INTO d2 values(2, 2, 2);
(1 row affected)
(1 row affected)
OR, you can use this syntax to give the constraints some meaningful names:
CREATE TABLE d3 (
aid INTEGER,
bid INTEGER,
cid INTEGER,
CONSTRAINT FK_d3_b FOREIGN KEY (aid,bid) REFERENCES b (aid,bid),
CONSTRAINT FK_d3_c FOREIGN KEY (aid,cid) REFERENCES c (aid,cid)
);

How do I enforce that a coupling table between two subtables reference the same master table?

I have one table person and two tables foo and bar that both reference person with foreign keys named person_id. I need to create a coupling table that links one foo to one bar, but both need to reference the same person.
How can I express this in a relational structure using just declarative constructs? Or do I need to create a trigger to enforce this?
CREATE TABLE person
(id int primary key, name text);
INSERT INTO person
(id, name)
VALUES
(1, 'John'),
(2, 'Jane');
CREATE TABLE foo
(id int primary key, person_id int references person(id) not null, comment text);
INSERT INTO foo
(id, person_id, comment)
VALUES
(1, 1, 'John is great'),
(2, 2, 'Jane is great');
CREATE TABLE bar
(id int primary key, person_id int references person(id) not null, comment text);
INSERT INTO bar
(id, person_id, comment)
VALUES
(1, 1, 'John is super great'),
(2, 2, 'Jane is super great');
CREATE TABLE foo_bar
(id int primary key, foo_id int references foo(id), bar_id int references bar(id));
INSERT INTO foo_bar
(id, foo_id, bar_id)
VALUES
(1, 1, 1),
(2, 1, 2), -- Shouldn't be possible!
(3, 2, 1), -- Shouldn't be possible!
(4, 2, 2);
As this query demonstrates, it's perfectly possible to get results where a row in foo_bar references data for both John and Jane:
select foo.comment, bar.comment from foo_bar
inner join foo ON foo.id = foo_bar.foo_id
inner join bar ON bar.id = foo_bar.bar_id;
Result:
John is great, John is super great
John is great, Jane is super great
Jane is great, John is super great
Jane is great, Jane is super great
SQL Fiddle: http://sqlfiddle.com/#!17/40c78/3
You could create a unique constraint on foo and bar that contains the id as well as person_id. If the foreign key constraints on foo_bar reference these unique constraints, the condition is automatically satisfied.
ALTER TABLE foo ADD CONSTRAINT foo_id_person_unique
UNIQUE (person_id, id);
ALTER TABLE bar ADD CONSTRAINT bar_id_person_unique
UNIQUE (person_id, id);
ALTER TABLE foo_bar ADD person_id integer;
UPDATE foo_bar
SET person_id = foo.person_id
FROM foo
WHERE foo_bar.foo_id = foo_id;
ALTER TABLE foo_bar ALTER person_id SET NOT NULL;
ALTER TABLE foo_bar ADD CONSTRAINT foo_bar_foo_fkey
FOREIGN KEY (person_id, foo_id) REFERENCES foo (person_id, id);
ALTER TABLE foo_bar ADD CONSTRAINT foo_bar_bar_fkey
FOREIGN KEY (person_id, bar_id) REFERENCES bar (person_id, id);
Then drop the original foreign key constraints from foo_bar.
I would not use an artificial primary key for foo_bar, since (foo_id, bar_id) is a natural primary key that would guarantee that no relationship is entered more than once.
You have stumbled upon the main issue with single surrogate keys: when it comes to hierarchies (like foo_bar being child to both foo and bar which are both children to person), the database system cannot enforce consistency.
So work with composite keys instead. Something along the lines of (pseudo code):
CREATE TABLE person (person_nr, name text,
PRIMARY KEY (person_nr));
CREATE TABLE foo (person_nr, foo_nr, comment text,
PRIMARY KEY (person_nr, foo_nr),
FOREIGN KEY person_nr REFERENCES person(person_nr));
CREATE TABLE bar (person_nr, bar_nr, comment text,
PRIMARY KEY (person_nr, bar_nr),
FOREIGN KEY person_nr REFERENCES person(person_nr));
CREATE TABLE foo_bar (person_id, foo_nr, bar_nr,
PRIMARY KEY (person_nr, foo_nr, bar_nr),
FOREIGN KEY (person_nr, foo_nr) REFERENCES foo(person_nr, foo_nr),
FOREIGN KEY (person_nr, bar_nr) REFERENCES bar(person_nr, bar_nr));
Composite keys have the disadvantage of making joins a tad more prone to errors (i.e. you may confuse key parts or miss a part of the key), but they also make the database better by enforcing consistency.

PostgreSQL check constraint for foreign key condition

I have a table of users eg:
create table "user" (
id serial primary key,
name text not null,
superuser boolean not null default false
);
and a table with jobs:
create table job (
id serial primary key,
description text
);
the jobs can be assigned to users, but only for superusers. other users cannot have jobs assigned.
So I have a table whereby I see which job was assigned to which user:
create table user_has_job (
user_id integer references "user"(id),
job_id integer references job(id),
constraint user_has_job_pk PRIMARY KEY (user_id, job_id)
);
But I want to create a check constraint that the user_id references a user that has user.superuser = True.
Is that possible? Or is there another solution?
This would work for INSERTS:
create or replace function is_superuser(int) returns boolean as $$
select exists (
select 1
from "user"
where id = $1
and superuser = true
);
$$ language sql;
And then a check contraint on the user_has_job table:
create table user_has_job (
user_id integer references "user"(id),
job_id integer references job(id),
constraint user_has_job_pk PRIMARY KEY (user_id, job_id),
constraint chk_is_superuser check (is_superuser(user_id))
);
Works for inserts:
postgres=# insert into "user" (name,superuser) values ('name1',false);
INSERT 0 1
postgres=# insert into "user" (name,superuser) values ('name2',true);
INSERT 0 1
postgres=# insert into job (description) values ('test');
INSERT 0 1
postgres=# insert into user_has_job (user_id,job_id) values (1,1);
ERROR: new row for relation "user_has_job" violates check constraint "chk_is_superuser"
DETAIL: Failing row contains (1, 1).
postgres=# insert into user_has_job (user_id,job_id) values (2,1);
INSERT 0 1
However this is possible:
postgres=# update "user" set superuser=false;
UPDATE 2
So if you allow updating users you need to create an update trigger on the users table to prevent that if the user has jobs.
The only way I can think of is to add a unique constraint on (id, superuser) to the users table and reference that from the user_has_job table by "duplicating" the superuser flag there:
create table users (
id serial primary key,
name text not null,
superuser boolean not null default false
);
-- as id is already unique there is no harm adding this additional
-- unique constraint (from a business perspective)
alter table users add constraint uc_users unique (id, superuser);
create table job (
id serial primary key,
description text
);
create table user_has_job (
user_id integer references users (id),
-- we need a column in order to be able to reference the unique constraint in users
-- the check constraint ensures we only reference superuser
superuser boolean not null default true check (superuser),
job_id integer references job(id),
constraint user_has_job_pk PRIMARY KEY (user_id, job_id),
foreign key (user_id, superuser) references users (id, superuser)
);
insert into users
(id, name, superuser)
values
(1, 'arthur', false),
(2, 'ford', true);
insert into job
(id, description)
values
(1, 'foo'),
(2, 'bar');
Due to the default value, you don't have to specify the superuser column when inserting into the user_has_job table. So the following insert works:
insert into user_has_job
(user_id, job_id)
values
(2, 1);
But trying to insert arthur into the table fails:
insert into user_has_job
(user_id, job_id)
values
(1, 1);
This also prevents turning ford into a non-superuser. The following update:
update users
set superuser = false
where id = 2;
fails with the error
ERROR: update or delete on table "users" violates foreign key constraint "user_has_job_user_id_fkey1" on table "user_has_job"
Detail: Key (id, superuser)=(2, t) is still referenced from table "user_has_job".
Create a separate superuser table that inherits from the user table:
CREATE TABLE "user" (
id serial PRIMARY KEY,
name text NOT NULL,
);
CREATE TABLE superuser () INHERITS ("user");
The user_has_job table can then reference the superuser table:
CREATE TABLE user_has_job (
user_id integer REFERENCES superuser (id),
job_id integer REFERENCES job(id),
PRIMARY KEY (user_id, job_id)
);
Move users around between the tables as needed by inserting and deleting:
WITH promoted_user AS (
DELETE FROM "user" WHERE id = 1 RETURNING *
) INSERT INTO superuser (id, name) SELECT id, name FROM promoted_user;
I don't know if this is a good way to do it but it seems to work
INSERT INTO user_has_job (user_id, job_id) VALUES (you_user_id, your_job_id)
WHERE EXIST (
SELECT * FROM user WHERE id=your_user_id AND superuser=true
);

Oracle SQL, trigger inserting row into table X as FK for currently inserted row in table Y

So in Oracle 11g I have the following:
CREATE TABLE OBJECT(
ID NUMBER(8) NOT NULL,
CATEGORY_ENUM_ID NUMBER(8) NOT NULL
);
CREATE SEQUENCE OBJECT_SEQ
START WITH 1
INCREMENT BY 1
NOCACHE
NOCYCLE;
/
CREATE TABLE TREE(
ID NUMBER(8) NOT NULL,
OBJECT_ID NUMBER(8) NOT NULL,
NAME NVARCHAR2(128)
);
CREATE TABLE CATEGORY_ENUM(
ID NUMBER(8) NOT NULL,
NAME NVARCHAR2(64)
);
-- PK's
ALTER TABLE TREE
ADD CONSTRAINT TREE_PK PRIMARY KEY (ID);
ALTER TABLE OBJECT
ADD CONSTRAINT OBJECT_PK PRIMARY KEY (ID);
ALTER TABLE CATEGORY_ENUM
ADD CONSTRAINT CATEGORY_ENUM_PK PRIMARY KEY (ID)
--- FK's
ALTER TABLE TREE
ADD CONSTRAINT TREE_OBJECT_FK FOREIGN KEY (OBJECT_ID)
REFERENCES OBJECT (ID);
ALTER TABLE OBJECT
ADD CONSTRAINT OBJECT_CATEGORY_FK FOREIGN KEY (CATEGORY_ENUM_ID)
REFERENCES CATEGORY_ENUM (ID);
-- Closed dictionary sample data
INSERT INTO CATEGORY_ENUM (ID, NAME) VALUES (1, 'TREE');
INSERT INTO CATEGORY_ENUM (ID, NAME) VALUES (2, 'HERB');
INSERT INTO CATEGORY_ENUM (ID, NAME) VALUES (3, 'SHROOM');
-- Triggers
CREATE OR REPLACE TRIGGER TREE_before_insert
BEFORE INSERT
ON TREE
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
CATEGORY_ID NUMBER;
BEGIN
SELECT ID INTO CATEGORY_ID FROM CATEGORY_ENUM WHERE NAME = 'TREE' AND ROWNUM <= 1;
if :NEW.OBJECT_ID is null then
:NEW.OBJECT_ID := OBJECT_SEQ.nextval;
INSERT INTO OBJECT (ID, CATEGORY_ENUM_ID) VALUES (:NEW.OBJECT_ID, CATEGORY_ID);
end if;
END;
/
If next I run:
INSERT INTO TREE (ID, NAME) VALUES (1, 'Tree1');
INSERT INTO TREE (ID, NAME) VALUES (2, 'Tree2');
I get an error:
...
TRIGGER TREE_BEFORE_INSERT compiled
Error starting at line 91 in command:
INSERT INTO TREE (ID, NAME) VALUES (1, 'Tree1')
Error report:
SQL Error: ORA-02291: integrity constraint (HR.TREE_OBJECT_FK) violated - parent key not found
02291. 00000 - "integrity constraint (%s.%s) violated - parent key not found"
*Cause: A foreign key value has no matching primary key value.
*Action: Delete the foreign key or add a matching primary key.
1 rows inserted.
But if before the above, I insert anything into FEATURES table (like below), it works fine.
INSERT INTO OBJECT (ID, CATEGORY_ENUM_ID) VALUES (0, 1);
INSERT INTO TREE (ID, NAME) VALUES (1, 'Tree1');
INSERT INTO TREE (ID, NAME) VALUES (2, 'Tree2');
So the problem occurs only for the very first insert, the others work fine, IDs assigned from OBJECT_SEQ.nextval are proper.
What am I doing wrong ?
Jah bless ya for help.
EDIT I have removed some unnecessary code, so now it's more clear and shorter.
The problem here is that you have a primary key constraint placed on the "TREE" table and a foreign key constraint placed on the "OBJECT" table. So any new record that you try inserting into the table with foreign key constraint, make sure it is already available in the table that has the primary key mapping.
Example shown below:
**Tables:**
create table tab1
(id number,
name varchar2(100)
)
create table tab2
(id number,
name varchar2(100)
)
**constraints added:**
alter table tab1 add constraint pk_tab1_id primary key (id)
alter table tab2 add constraint fk_tab2_id foreign key(id) references tab1(id)
**Insert statement on the second table:**
insert into tab2(values(1,'abc')
**Error:**
SQL Error: ORA-02291: integrity constraint (PRAVE.fk_tab2_id) violated - parent key not found
02291. 00000 - "integrity constraint (%s.%s) violated - parent key not found"
*Cause: A foreign key value has no matching primary key value.
*Action: Delete the foreign key or add a matching primary key.
**Solution:**
**Insert into the table with PK first as below:**
insert into tab1 values(1,'abc')
1 rows inserted.
insert into tab2 values(1,'abc')
1 rows inserted.