"polymorphism" for FOREIGN KEY constraints - postgresql

There is this field in a table:
room_id INT NOT NULL CONSTRAINT room_id_ref_room REFERENCES room
I have three 2 tables for two kinds of rooms: standard_room and family_room
How to do something like this:
room_id INT NOT NULL CONSTRAINT room_id_ref_room REFERENCES standard_room or family_room
I mean, room_id should reference either standard_room or family_room.
Is it possible to do so?

Here is the pattern I've been using.
CREATE TABLE room (
room_id serial primary key,
room_type VARCHAR not null,
CHECK CONSTRAINT room_type in ("standard_room","family_room"),
UNIQUE (room_id, room_type)
);
CREATE_TABLE standard_room (
room_id integer primary key,
room_type VARCHAR not null default "standard_room",
FOREIGN KEY (room_id, room_type) REFERENCES room (room_id, room_type),
CHECK CONSTRAINT room_type = "standard_room"
);
CREATE_TABLE family_room (
room_id integer primary key,
room_type VARCHAR not null default "family_room",
FOREIGN KEY (room_id, room_type) REFERENCES room (room_id, room_type),
CHECK CONSTRAINT room_type = "family_room"
);
That is, the 'subclasses' point at the super-class, by way of a type descriminator column (such that the pointed to base class is of the correct type, and that primary key of the super class is the same as the child classes.

Here's the same SQL from the accepted answer that works for PostGres 12.8. There's a few issues not only the CREATE_TABLE syntax mistake:
CREATE TABLE room (
room_id serial primary key,
room_type VARCHAR not null,
CONSTRAINT room_in_scope CHECK (room_type in ('standard_room','family_room')),
CONSTRAINT unique_room_type_combo UNIQUE (room_id, room_type)
);
CREATE TABLE standard_room (
room_id integer primary key,
room_type VARCHAR not null default 'standard_room',
CONSTRAINT roomid_std_roomtype_fk FOREIGN KEY (room_id, room_type) REFERENCES public."room" (room_id, room_type),
CONSTRAINT std_room_constraint CHECK (room_type = 'standard_room')
);
CREATE TABLE family_room (
room_id integer primary key,
room_type VARCHAR not null default 'family_room',
CONSTRAINT roomid_fam_roomtype_fk FOREIGN KEY (room_id, room_type) REFERENCES "room" (room_id, room_type),
CONSTRAINT fam_room_constraint CHECK (room_type = 'family_room')
);
NOTE: The SQL above uses constraints to enforce the child room_type values default to the parent tables' room_type values: 'standard_room' or 'family_room'.
PROBLEM: Since the child tables Primary Key's expect either the standard and family room Primary Key that means you can't insert more than one record in thsee two child tables.
insert into room (room_type) VALUES ('standard_room'); //Works
insert into room (room_type) values ('family_room'); //Works
insert into standard_room (room_id,pictureAttachment) VALUES (1,'Before Paint'); //Works
insert into standard_room (room_id,pictureAttachment) VALUES (1,'After Paint'); //Fails
insert into standard_room (room_id,pictureAttachment) VALUES (1,'With Furniture');
insert into family_room (room_id,pictureAttachment) VALUES (2, 'Beofre Kids'); //Works
insert into family_room (room_id,pictureAttachment) VALUES (2,'With Kids'); //Fails
To make the tables accept > 1 row you have to remove the Primary Keys from the 'standard_room' and 'family_room' tables which is BAD database design.
Despite 26 upvotes I will ping OP about this as I can see the answer was typed free hand.
Alternate Solutions
For smallish tables with less than a handful of variations a simple alterative is a single table with Bool columns for different table Primary Key fields.
Single Table "Room"
Id
IsStandardRoom
IsFamilyRoom
Desc
Dimensions
1
True
False
Double Bed, BIR
3 x 4
2
False
True
3 Set Lounge
5.5 x 7
SELECT * FROM Room WHERE IsStdRoom = true;
At the end of the day, in a relational database it's not very common to be adding Room Types when it involves creating the necessary related database tables using DDL commands (CREATE, ALTER, DROP).
A typical future proof database design allowing for more Tables would look something like this:
Multi Many-To-Many Table "Room"
Id
TableName
TableId
1
Std
8544
2
Fam
236
3
Std
4351
Either Standard or Family:
select * from standard_room sr where sr.room_id in
(select TableId from room where TableName = 'Std');
select * from family_room fr where fr.room_id in
(select id from room where TableName = 'Fam');
Or both:
select * from standard_room sr where sr.room_id in
(select TableId from room where TableName = 'Std')
UNION
select * from family_room fr where fr.room_id in
(select id from room where TableName = 'Fam');
Sample SQL to demo Polymorphic fields:
If you want to have different Data Types in the polymorphic foreign key fields then you can use this solution. Table r1 stores a TEXT column, r2 stores a TEXT[] Array column and r3 a POLYGON column:
CREATE OR REPLACE FUNCTION null_zero(anyelement)
RETURNS INTEGER
LANGUAGE SQL
AS $$
SELECT CASE WHEN $1 IS NULL THEN 0 ELSE 1 END;
$$;
CREATE TABLE r1 (
r1_id SERIAL PRIMARY KEY
, r1_text TEXT
);
INSERT INTO r1 (r1_text)
VALUES ('foo bar'); --TEXT
CREATE TABLE r2 (
r2_id SERIAL PRIMARY KEY
, r2_text_array TEXT[]
);
INSERT INTO r2 (r2_text_array)
VALUES ('{"baz","blurf"}'); --TEXT[] ARRAY
CREATE TABLE r3 (
r3_id SERIAL PRIMARY KEY
, r3_poly POLYGON
);
INSERT INTO r3 (r3_poly)
VALUES ( '((1,2),(3,4),(5,6),(7,8))' ); --POLYGON
CREATE TABLE flex_key_shadow (
flex_key_shadow_id SERIAL PRIMARY KEY
, r1_id INTEGER REFERENCES r1(r1_id)
, r2_id INTEGER REFERENCES r2(r2_id)
, r3_id INTEGER REFERENCES r3(r3_id)
);
ALTER TABLE flex_key_shadow ADD CONSTRAINT only_one_r
CHECK(
null_zero(r1_id)
+ null_zero(r2_id)
+ null_zero(r3_id)
= 1)
;
CREATE VIEW flex_key AS
SELECT
flex_key_shadow_id as Id
, CASE
WHEN r1_id IS NOT NULL THEN 'r1'
WHEN r2_id IS NOT NULL THEN 'r2'
WHEN r3_id IS NOT NULL THEN 'r3'
ELSE 'wtf?!?'
END AS "TableName"
, CASE
WHEN r1_id IS NOT NULL THEN r1_id
WHEN r2_id IS NOT NULL THEN r2_id
WHEN r3_id IS NOT NULL THEN r3_id
ELSE NULL
END AS "TableId"
FROM flex_key_shadow
;
INSERT INTO public.flex_key_shadow (r1_id,r2_id,r3_id) VALUES
(1,NULL,NULL),
(NULL,1,NULL),
(NULL,NULL,1);
SELECT * FROM flex_key;

Related

how to retrieve data from multiple tables (postgresql)

I have 4 different tables that are linked to each other in the following way (I only kept the essential columns in each table to emphasise the relationships between them):
create TABLE public.country (
country_code varchar(2) NOT NULL PRIMARY KEY,
country_name text NOT NULL,
);
create table public.address
(
id integer generated always as identity primary key,
country_code text not null,
CONSTRAINT FK_address_2 FOREIGN KEY (country_code) REFERENCES public.country (country_code)
);
create table public.client_order
(
id integer generated always as identity primary key,
address_id integer null,
CONSTRAINT FK_client_order_1 FOREIGN KEY (address_id) REFERENCES public.address (id)
);
create table public.client_order_line
(
id integer generated always as identity primary key,
client_order_id integer not null,
product_id integer not null,
client_order_status_id integer not null default 0,
quantity integer not null,
CONSTRAINT FK_client_order_line_0 FOREIGN KEY (client_order_id) REFERENCES public.client_order (id)
);
I want to get the data in the following way: for each client order line to show the product_id, quantity and country_name(corresponding to that client order line).
I tried this so far:
SELECT country_name FROM public.country WHERE country_code = (
SELECT country_code FROM public.address WHERE id = (
SELECT address_id FROM public.client_order WHERE id= 5
)
)
to get the country name given a client_order_id from client_order_line table. I don't know how to change this to get all the information mentioned above, from client_order_line table which looks like this:
id client_order_id. product_id. status. quantity
1 1 122 0 1000
2 2 122 0 3000
3 2 125 0 3000
4 3 445 0 2000
Thanks a lot!
You need a few join-s.
select col.client_order_id,
col.product_id,
col.client_order_status_id as status,
col.quantity,
c.country_name
from client_order_line col
left join client_order co on col.client_order_id = co.id
left join address a on co.address_id = a.id
left join country c on a.country_code = c.country_code
order by col.client_order_id;
Alternatively you can use your select query as a scalar subquery expression.

Altering Column Type in SQL (PGadmin) not working

I am new to DB design and have created my tables in Postgres. However, I need to change the datatype of one of my tables from date to integer'. However, when I add the code in to do this, I get the following error:
ERROR: cannot cast type date to integer
LINE 13: ALTER COLUMN year TYPE INT USING year::integer;
It was recommended that I add the USING line to override this error, but it did not solve my problem.
Below is the entire code, any advice would be great appreciated.
BEGIN;
CREATE TABLE IF NOT EXISTS public."Actor"
(
actor1_name "char",
actor_id numeric,
actor_type "char",
PRIMARY KEY (actor_id)
);
CREATE TABLE IF NOT EXISTS public."Country"
(
country_name "char",
country_id numeric,
PRIMARY KEY (country_id)
);
CREATE TABLE IF NOT EXISTS public."Event"
(
event_id numeric,
event_type_descr "char",
event_date date,
**year date,**
fatalities numeric,
event_type "char",
PRIMARY KEY (event_id)
);
**ALTER TABLE "Event"
ALTER COLUMN year TYPE INT USING year::integer;**
CREATE TABLE IF NOT EXISTS public."Location"
(
location_name "char",
longitude numeric,
latitude numeric,
location_id numeric,
PRIMARY KEY (location_id)
);
CREATE TABLE IF NOT EXISTS public."Region"
(
region_name "char",
region_id numeric,
PRIMARY KEY (region_id)
);
CREATE TABLE IF NOT EXISTS public."Event_Actor"
(
"Event_event_id" numeric,
"Actor_actor_id" numeric
);
ALTER TABLE public."Location"
ADD FOREIGN KEY (location_id)
REFERENCES public."Country" (country_id)
NOT VALID;
ALTER TABLE public."Country"
ADD FOREIGN KEY (country_id)
REFERENCES public."Region" (region_id)
NOT VALID;
ALTER TABLE public."Event_Actor"
ADD FOREIGN KEY ("Event_event_id")
REFERENCES public."Event" (event_id)
NOT VALID;
ALTER TABLE public."Event_Actor"
ADD FOREIGN KEY ("Actor_actor_id")
REFERENCES public."Actor" (actor_id)
NOT VALID;
ALTER TABLE public."Event"
ADD FOREIGN KEY (event_id)
REFERENCES public."Location" (location_id)
NOT VALID;
END;
Assuming from the column names that you want the year part of the date as integer (you should clearly express that in a question!), you can use extract().
ALTER TABLE "Event"
ALTER COLUMN year
TYPE integer
USING extract(YEAR FROM year);
And as a side note: Avoid case sensitive object names like "Event". They only make things harder but have no benefit. If you need "pretty" labels, that's a job for the presentation layer, not the database anyway.

PostgreSQL: Foreign key between composite type and independent columns

Minimal definitions:
CREATE TYPE GlobalId AS (
id1 BigInt,
id2 SmallInt
);
CREATE TABLE table1 (
id1 BigSerial NOT NULL,
id2 SmallInt NOT NULL,
PRIMARY KEY (id1, id2)
);
CREATE TABLE table2 (
global_id GlobalId NOT NULL,
FOREIGN KEY (global_id) REFERENCES table1 (id1, id2)
);
In short, I use a composite type for table2 (and many other tables), but for the primary table (table1), I don't directly use the composite type because composite types don't support the use of Serial.
The above produces the following error due to the ostensible mismatch between global_id and id1, id2: number of referencing and referenced columns for foreign key disagree.
Alternatively, if I define the foreign key as FOREIGN KEY (global_id.id1, global_id.id2) REFERENCES table1 (id1, id2), I get a syntax error on using an accessor on global_id.
Any ideas on how to define this foreign key relationship? Alternatively, if there's a way for table1 to use the GlobalId composite type while still getting serial/sequence behavior for id1, that works also.
You can define table1 using your composite type and fill the value using a BEFORE trigger:
CREATE TABLE table1 (id globalid PRIMARY KEY);
CREATE SEQUENCE s OWNED BY table1.id;
CREATE FUNCTION ins_trig() RETURNS trigger LANGUAGE plpgsql AS
$$BEGIN
NEW.id = (nextval('s'), (NEW.id).id2);
RETURN NEW;
END;$$;
CREATE TRIGGER ins_trig BEFORE INSERT ON table1 FOR EACH ROW
EXECUTE PROCEDURE ins_trig();
INSERT INTO table1 VALUES (ROW(NULL, 42));
SELECT * FROM table1;
id
--------
(1,42)
(1 row)

Cannot add Foreign Key on tables in DashDB / DB2 on Bluemix

When I create a table in DashDB (DB2) on Bluemix like this:
CREATE TABLE DEPARTMENT (
depname CHAR (10) UNIQUE NOT NULL ,
phone INTEGER
) ;
ALTER TABLE DEPARTMENT ADD CONSTRAINT DEPARTMENT_PK PRIMARY KEY ( depname ) ;
CREATE TABLE EMPLOYEE (
"EmpNr" NUMERIC (3) UNIQUE NOT NULL ,
empname CHAR (20) ,
depname CHAR (10) ,
EMPLOYEE2_title CHAR (20)
);
ALTER TABLE EMPLOYEE ADD CONSTRAINT EMPLOYEE_PK PRIMARY KEY ( "EmpNr" ) ;
ALTER TABLE EMPLOYEE ADD CONSTRAINT EMPLOYEE_DEPARTMENT_FK FOREIGN KEY (depname ) REFERENCES DEPARTMENT ( depname ) ;
Bluemix disallows adding a Foreign Key constraint for this table type.
When you look at the documentation for dashDB (not DB2) you will notice that foreign keys can be created. However, a table by default is created column-organized. Only non-enforced referential constraints are supported. In your example you would need to add NOT ENFORCED to your statement:
ALTER TABLE EMPLOYEE
ADD CONSTRAINT EMPLOYEE_DEPARTMENT_FK FOREIGN KEY (depname )
REFERENCES DEPARTMENT ( depname ) NOT ENFORCED;
By default on CREATE a DashDB table on Bluemix is 'organized by column'...
https://www-01.ibm.com/support/knowledgecenter/SSEPGG_10.5.0/com.ibm.db2.luw.admin.dbobj.doc/doc/c0060592.html
This will also disallow adding Foreign Key constraints for this table type.
To add FKs add ORGANIZE BY ROW to your CREATE TABLE statement:
CREATE TABLE DEPARTMENT (
depname CHAR (10) UNIQUE NOT NULL ,
phone INTEGER
) ORGANIZE BY ROW;
ALTER TABLE DEPARTMENT
ADD CONSTRAINT DEPARTMENT_PK PRIMARY KEY ( depname ) ;
CREATE TABLE EMPLOYEE (
"EmpNr" NUMERIC (3) UNIQUE NOT NULL ,
empname CHAR (20) ,
depname CHAR (10) ,
EMPLOYEE2_title CHAR (20)
) ORGANIZE BY ROW;
ALTER TABLE EMPLOYEE
ADD CONSTRAINT EMPLOYEE_PK PRIMARY KEY ( "EmpNr" ) ;
ALTER TABLE EMPLOYEE
ADD CONSTRAINT EMPLOYEE_DEPARTMENT_FK FOREIGN KEY (depname )
REFERENCES DEPARTMENT ( depname ) ;

How to add a foreign key constraint to same table using ALTER TABLE in PostgreSQL

To create table I use:
CREATE TABLE category
(
cat_id serial NOT NULL,
cat_name character varying NOT NULL,
parent_id integer NOT NULL,
CONSTRAINT cat_id PRIMARY KEY (cat_id)
)
WITH (
OIDS=FALSE
);
ALTER TABLE category
OWNER TO pgsql;
parent_id is a id to another category. Now I have a problem: how to cascade delete record with its children? I need to set parent_id as foreign key to cat_id.
I try this:
ALTER TABLE category
ADD CONSTRAINT cat_cat_id_fkey FOREIGN KEY (parent_id)
REFERENCES category (cat_id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
But it falls with:
ERROR: insert or update on table "category" violates foreign key constraint "cat_cat_id_fkey"
DETAIL: Key (parent_id)=(0) is not present in table "category".
The problem you have - what would be the parent_id of a category at the top of the hierarchy?
If it will be null - it will break the NOT NULL constratint.
If it will be some arbitrary number like 0 - it will break the foreign key (like in your example).
The common solution - drop the NOT NULL constratint on the parent_id and set parent_id to null for top categories.
-- create some fake data for testing
--
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
CREATE TABLE category
(
cat_id serial NOT NULL,
cat_name character varying NOT NULL,
parent_id integer NOT NULL,
CONSTRAINT cat_id PRIMARY KEY (cat_id)
);
INSERT INTO category(cat_name,parent_id)
SELECT 'Name_' || gs::text
, gs % 3
FROM generate_series(0,9) gs
;
-- find the records with the non-existing parents
SELECT ca.parent_id , COUNT(*)
FROM category ca
WHERE NOT EXISTS (
SELECT *
FROM category nx
WHERE nx.cat_id = ca.parent_id
)
GROUP BY ca.parent_id
;
-- if all is well: proceed
-- make parent pointer nullable
ALTER TABLE category
ALTER COLUMN parent_id DROP NOT NULL
;
-- set non-existing parent pointers to NULL
UPDATE category ca
SET parent_id = NULL
WHERE NOT EXISTS (
SELECT *
FROM category nx
WHERE nx.cat_id = ca.parent_id
)
;
-- Finally, add the FK constraint
ALTER TABLE category
ADD CONSTRAINT cat_cat_id_fkey FOREIGN KEY (parent_id)
REFERENCES category (cat_id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
;
This is quite simple.
Here the foreign key parent_id refers to cat_id.
Here a record with parent_id=0 exists but not a record with cat_id=0.