How to model polymorphic data in PostgreSQL - sql

We have some data that we'd like to model in the following way. We have two fields:
to
from
and each of those fields have the following data:
some enum
some foreign key (one of 4 possible tables)
Does PostgreSQL provide a way to store this polymorphic enum + dynamic foreign key?

You can use json (or jsonb or array) for store 2 fields in "from" and "to" columns.
Then you can use generated columns to store one fk for each table.
Something like this:
create table table1 (
id serial primary key,
name text
);
insert into table1 values (1,'one'),(2,'two');
create table table2 (
id serial primary key,
name text
);
insert into table2 values (1,'one'),(2,'two');
create table the_table(
"from" jsonb,
from1 bigint generated always as (case when "from"->>'enum' = '1' then ("from"->>'data')::bigint else null end) stored,
from2 bigint generated always as (case when "from"->>'enum' = '2' then ("from"->>'data')::bigint else null end) stored,
foreign key (from1) references table1 (id),
foreign key (from2) references table2 (id)
);
insert into the_table("from") values ('{"enum":1, "data":2}');

Related

Add primary key to an existing table

How to add a primary key to an existing table in SQLite? I know that I have to generate a new table and copy everything from the old table. However, it keeps giving me an error saying datatype mismatch due to one table having a primary key and the other one not. I did run the same commands without including primary key and it works.
CREATE TABLE temp_table
(
id INTEGER PRIMARY KEY,
age INTEGER,
address TEXT
)
INSERT INTO temp_table
SELECT *
FROM original_table
Since I am importing the data from a CSV file I do not know how to add the PRIMARY KEY in the first place. In case anyone knows a solution for that it would also work.
Assuming that the original table has 2 columns: age and address, you can either list the column names of the new table without the primary key:
INSERT INTO temp_table(age, address)
SELECT age, address FROM original_table
or, include the primary key and pass null for its value:
INSERT INTO temp_table(id, age, address)
SELECT null, age, address FROM original_table
or:
INSERT INTO temp_table
SELECT null, age, address FROM original_table
In all cases the id will be filled by SQLite, because it is defined as INTEGER PRIMARY KEY and it will be automatically autoincremented starting from 1.
If there is another column in the original table with unique integer values, you can pass that column to fill the new id:
INSERT INTO temp_table(id, age, address)
SELECT col, age, address FROM original_table
Change col to the actual name of the column.

Using column name as part of foreign key relationship

As part of the application I am building I want to store the values of all dropdowns in a single table, where all table/column combinations that represent a dropdown will be able to reference it.
Something like the below:
CREATE TABLE dropdown_def
(
id int,
table_id int,
field_name TEXT,
value TEXT,
PRIMARY KEY (id),
UNIQUE (table_id, field_name, value)
);
INSERT INTO test1.dropdowns (id, table_id, field_name, value)
VALUES (1, 1, 'status', 'active');
CREATE TABLE clients
(
id int,
table_id int,
status TEXT,
FOREIGN KEY (table_id, 'status', status)
REFERENCES dropdowns (table_id, field_name, value)
);
I tried the above but with no surprise it seems you cannot use the actual column name as part of the foreign key constraint.
Is there any other way of using the column identifier in a FK?
You can have a constant stored computed column for the FK.
CREATE TABLE clients
(
id int,
table_id int,
status TEXT,
refcolname text GENERATED ALWAYS AS ('status') STORED,
FOREIGN KEY (table_id, refcolname, status)
REFERENCES dropdowns (table_id, field_name, value)
);
Really I would advice against the design like that. Why to create a united table of lookups just to get troubles referencing its parts. Use the EAV pattern only if it is absolutely inevitable, for example a set of lookup tables is to be defined by a user at runtime.

No auto increment in my foreign key

I'm working in my school project and I want to make the id subject of any student incremented automatically as foreign key. I am showing you the example below as prototype, there are two tables, when I'm trying to insert data into the second table, I get an error (necessary to insert another field id of the table)
CREATE DATABASE database1;
USE database1;
CREATE TABLE table1
(
IdTable1 INT NOT NULL IDENTITY,
NOM VARCHAR(30),
PRIMARY KEY(IDMEDO)
);
--auto increment is working here
INSERT INTO table1
VALUES ('data1Table1'), ('data2Table1'), ('data3Table1');
--auto increment is working here just with the primary key
CREATE TABLE table2
(
IdTable2 INT not null IDENTITY,
IdTable1 INT,
dataTable2 varchar(30),
primary key(IdTable2),
constraint fk_tbl1 foreign key(IdTable1) references Table1
);
--necessary to add id field
INSERT INTO table2
VALUES ('data1Table2'), ('data2Table2'), ('data3Table2');
You should always (as a "best practice") define the columns you want to insert data into - that way, you can specify those that you have to provide values for, and let SQL Server handle the others. And for the foreign key, you have to explicitly provide a value - there's no "auto-magic" (or "auto-incrementing") way of associating a child row with its parent table - you have to provide the value in your INSERT statement.
So change your code to:
-- explicitly *specify* the NOM column here!
INSERT INTO table1 (NOM)
VALUES ('data1Table1'), ('data2Table1'), ('data3Table1');
-- again: explicitly *specify* the columns you want to insert into!
INSERT INTO table2 (IdTable1, dataTable2)
VALUES (1, 'data1Table2'), (2, 'data2Table2'), (3, 'data3Table2');
You need to insert the value of the foreign key wrt to table 1 now if you enter a value that's not in table 1 column which is acting as a foreign key then also it will not accept
Simply
Primary key and foreign key should match
Foreign key can't be null if table 1 doesn't contain a primary key with value null

SQL Oracle - Data transfer between relational and object tables

I need solution for data transfer between two tables in Oracle database. Source is relational type and target is object type of table. Here is example:
Source table (existing data):
CREATE TABLE "HISTORY" (
"ID" NUMBER,
"ID_PROCESS" NUMBER,
"ID_UNIT" NUMBER,
"PASS_DATE" DATE DEFAULT SYSDATE
);
CREATE UNIQUE INDEX "HISTORY_PK" ON "HISTORY" ("ID");
ALTER TABLE "HISTORY" ADD CONSTRAINT "HISTORY_PROCESS_FK1"
FOREIGN KEY ("ID_PROCESS") REFERENCES "PROCESS" ("ID");
ALTER TABLE "HISTORY" ADD CONSTRAINT "HISTORY_PROCESS_FK2"
FOREIGN KEY ("ID_UNIT") REFERENCES "UNITS" ("ID");
Target table (empty):
CREATE OR REPLACE TYPE t_history AS OBJECT (
"ID" NUMBER,
"ID_PROCESS" REF t_process,
"ID_UNIT" REF t_unit,
"PASS_DATE" DATE
);
CREATE TABLE o_history OF t_history (
"ID" PRIMARY KEY);
Assuming you've already created types and object tables for the process and unit values, you can do:
INSERT INTO o_history
SELECT t_history(h.id, REF(p), REF(u), h.pass_date)
FROM history h
JOIN o_process p ON p.id_process = h.id_process
JOIN o_unit u ON u.id_unit = h.id_unit;
This gets the ID and pass date from the history table, and gets the references from the other tables for the matching values.
SQL Fiddle demo.

How do I create a composite foreign key going backwards down a relationship?

Sorry about the title, couldn't think of a better way to write it.
Here's my problem...
I have 2 tables in my database [Drawings] and [Revisions];
[Drawings] 1-----* [Revisions]
ProjectId(pk) ProjectId(pk)(fk)
DrawingNo(pk) DrawingNo(pk)(fk)
RevisionNo(pk)
LatestRevision
There is a foreign key in [revisions] referencing [drawings] on [ProjectId] and [DrawingNo].
I need to implement a way of enforcing that the drawings latest revision number equals a corresponding revision number in the revisions table:
... WHERE [Drawings].[LatestRevision] NOT IN (
SELECT [RevisionNo]
FROM [Revisions]
WHERE [Drawings].[ProjectId] = [Revisions].[ProjectId]
AND [Drawings].[DrawingNo] = [Revisions].[DrawingNo])
How would I put something like this into a foreign key?
I need this to work on sql server 2008 express onwards.
Thanks in advance for any help!
Schema:
TABLE Drawings
( ProjectId varchar,
DrawingNo varchar,
LatestRevisions varchar,
...other columns
PRIMARY KEY(ProjectId, DrawingNo)
)
TABLE Revisions
( ProjectId varchar,
DrawingNo varchar,
RevisionNo varchar,
...other columns
PRIMARY KEY(ProjectId, DrawingNo, RevisionNo)
FOREIGN KEY(ProjectId, DrawingNo) REFERENCES (Drawings(ProjectId, DrawingNo))
)
Drawing 'A' can have revision '1', and Drawing 'B' can have a different revision '1',
Revision number by itself is not unique
I will take the schema as follows:
TABLE drawings
( projectid integer,
drawingno integer,
latestRevision integer,
primary key (projectid, drawingno)
)
TABLE revisions
( revisionno integer primary key,
projectid integer,
drawingno integer,
foreign key (projectid, drawingno)
references (drawings (projectid, drawingno))
)
In this case, I would issue:
ALTER TABLE drawings
ADD FOREIGN KEY (latestRevision)
REFERENCES (revisions(revisionNo))
This would mean that every revisions.revisionNo is unique and the column drawings.latestRevision is a foreign key that references the primary key of revisions table, that is, revisionNo.
Please let me know if there are any changes to the schema you have.
Also, the foreign key is enforced only if it is referencing a primary key of another table. If revisions.revisionno is not a primary key or if the primary key constraint is disabled on this column, then the ALTER TABLE .. ADD FOREIGN KEY statement is bound to return an error.
The following structures replaces your tables with views and looks similar to what you describe, except it's instead maintained behind the scenes rather than being an explicit foreign key. I don't know what operations you'd want to support on Revisions, at the moment I only support INSERT:
create table dbo._Drawings (
ProjectId int not null,
DrawingId int not null,
constraint PK_Drawings PRIMARY KEY (ProjectID,DrawingID)
)
go
create table dbo._Revisions (
ProjectID int not null,
DrawingID int not null,
RevisionNo int not null,
_PreviousRevision as CASE WHEN RevisionNo > 1 THEN RevisionNo - 1 END persisted,
_NextRevision int null,
constraint PK_Revisions PRIMARY KEY (ProjectID,DrawingID,RevisionNo),
constraint FK_Revisions_Drawings FOREIGN KEY (ProjectID,DrawingID)
references _Drawings (ProjectID,DrawingID),
constraint CK_RevisionNos CHECK (RevisionNo >= 1),
constraint UK_Revisions_Previous UNIQUE (ProjectID,DrawingID,_PreviousRevision),
constraint UK_Revisions_Next UNIQUE (ProjectID,DrawingID,_NextRevision),
constraint FK_Revisions_Previous FOREIGN KEY (ProjectID,DrawingID,_PreviousRevision)
references _Revisions (ProjectID,DrawingID,RevisionNo),
constraint FK_Revisions_Next FOREIGN KEY (ProjectID,DrawingID,_NextRevision)
references _Revisions (ProjectID,DrawingID,RevisionNo)
)
The above two tables are the "backing store" for the data. The _Revisions table ensures that the revision sequences are strictly monotonically increasing from 1. Each row maintains a foreign key to its immediate preceding and succeeding revisions, except the first and last, for which NULLs substitute (but the unique constraints ensure only one of each exists for each ProjectID,DrawingID combination.
create view dbo.Drawings
with schemabinding
as
select
d.ProjectID,
d.DrawingID,
r.RevisionNo as LatestRevision
from
dbo._Drawings d
left join
dbo._Revisions r
on
d.ProjectId = r.ProjectID and
d.DrawingId = r.DrawingID and
r._NextRevision is null
The above view mimics your asked for Drawings table and would be used for any actual data access. If you wanted to enforce an invariant that each drawing must have at least one revision, you could switch the left join to an inner join and make this an indexed view. You'd need to add a trigger to support INSERTs, in much the same way as the below does for Revisions, which then populates both tables.
create view dbo.Revisions
with schemabinding
as
select
ProjectID,
DrawingID,
RevisionNo
from
dbo._Revisions
This view creates the impression that Revisions is as simple as in your query
create trigger T_Revisions_I
on dbo.Revisions
instead of insert
as
;with SplitData as (
select ProjectID,DrawingID,RevisionNo,RevisionNo-1 as Prev, Seq
from inserted cross join (select 1 union all select 2) t(Seq)
)
merge into dbo._Revisions r
using SplitData s
on
r.ProjectID = s.ProjectID and
r.DrawingID = s.DrawingID and
(
(s.Seq = 1 and r.RevisionNo = s.Prev) or
(s.Seq = 2 and r.RevisionNo = s.RevisionNo)
)
when matched and s.Seq = 1
then update set _NextRevision = s.RevisionNo
when not matched and s.Seq = 2
then insert (ProjectID,DrawingID,RevisionNo) values (s.ProjectID,s.DrawingID,s.RevisionNo)
;
And finally, this trigger is responsible for maintaining the _Revisions structure in the way that the constraints I created above require. The trick is that we use a MERGE statement so that at the same time as we insert the new row, we also update the previous row so that it's _NextRevision column is no longer null and references the row that we're inserting.
More triggers can be added to support more advanced usage.