Primary key collision in scope of one trasaction - sql

I have a postgresql database, which heavily relies on events from the outside, e.g. administrator changing / adding some fields or records might trigger a change in overall fields structure in other tables.
There lies the problem, however, as sometimes the fields changed by the trigger function are primary key fields. There is a table, which uses two foreign keys ids as the primary key, as in example below:
# | PK id1 | PK id2 | data |
0 | 1 | 1 | ab |
1 | 1 | 2 | cd |
2 | 1 | 3 | ef |
However, within one transaction (if I may call it such, since, in fact, it is a plpgsql function), the structure might be changed to:
# | PK id1 | PK id2 | data |
0 | 1 | 3 | ab |
1 | 1 | 2 | cd |
2 | 1 | 1 | ef |
Which, as you might have noticed, changed the 0th record's second primary key to 3, and the 2nd's to 1, which is the opposite of what they were before.
It is 100% certain that after the function has taken its effect there will be no collisions whatsoever, but I'm wondering, how can this be implemented?
I could, in fact, use a synthetic primary key as a BIGSERIAL, yet there is still a need for those two ids to be UNIQUE constained, so it wouldn't do the trick, unfortunately.

You can declare a constraint as deferrable, for example a primary key:
CREATE TABLE elbat (id int,
nmuloc int,
PRIMARY KEY (id)
DEFERRABLE);
You can then use SET CONSTRAINTS in a transaction to set deferrable constraints as deferred. That means that they can be violated temporarily during the transaction but must be fulfilled at the transaction's COMMIT.
Let's assume we have some data in our example table:
INSERT INTO elbat (id,
nmuloc)
VALUES (1,
1),
(2,
2);
We can now switch the IDs like this:
BEGIN TRANSACTION;
SET CONSTRAINTS ALL DEFERRED;
UPDATE elbat
SET id = 2
WHERE nmuloc = 1;
SELECT *
FROM elbat;
UPDATE elbat
SET id = 1
WHERE nmuloc = 2;
COMMIT;
There's no error even though the IDs are both 2 after the first UPDATE.
db<>fiddle
More on that can be found in the documentation, e.g. in CREATE TABLE (or ALTER TABLE) and SET CONSTRAINTS.

Related

Is there a way to generate composite key efficiently in a PostgreSQL database?

Suppose I have a student table with id as primary key, which holds information about all students.
id | name
---+------
1 | aaron
2 | bob
In addition there is a table where id and tid form a composite key, which holds the scores for each test.
id | tid | score
---| --- | -----
Note: Different students have different tests with different numbers and no correlation. tid does not represent a specific test at all, but for a student the test serial number. id=1 and id=2, if tid=1, does not mean it is the same test.
There are two ways to generate tid, one is globally unique and increases by 1 for each record inserted, e.g.
id | tid | score
-- | --- | -----
1 | 1 | 99
1 | 2 | 98
2 | 3 | 97
2 | 4 | 96
The other is unique within a specific id, and different ids can have the same tid take value, for example
id | tid | score
-- | --- | -----
1 | 1 | 99
1 | 2 | 98
2 | 1 | 97
2 | 2 | 96
In the previous way, a student with id=2 could probably guess how many tests roughly the whole school went through in between based on his tid change. Since the tid of each student changes globally, this is something I don't want. Of course I could consider using a non-repeating use of random numbers or scheme. But I would prefer a slightly more compact incremental integer to describe it.
For the latter, is there a more efficient and simple way to implement it?
Option 1.
create table student(id integer PRIMARY KEY, name varchar);
create table test(tid integer PRIMARY KEY, name varchar, test_date date);
create table test_score(sid integer, tid integer references test, score integer, PRIMARY KEY(sid, tid);
UPDATE
Option 2.
Create an BEFORE INSERT trigger that uses a function that does roughly:
CREATE OR REPLACE FUNCTION tid_incr()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
max_ct integer;
BEGIN
SELECT INTO max_ct max(tid) FROM score WHERE id = NEW.id;
NEW.tid = max_ct + 1;
RETURN NEW;
END;
$function$
The correct design is option 2, because tid should refer (ie be a foreign key) to the test (ie a collection of questions) being taken (whether the test is custom for each student or not).
If students can take the same test more than once, add a date column (or timestamp if multiple attempts may be made of the same day) to distinguish the results of repeat attempts at the same test.

Error "duplicate key value violates unique constraint" while updating multiple rows

I created a table in PostgreSQL and Oracle as
CREATE TABLE temp(
seqnr smallint NOT NULL,
defn_id int not null,
attr_id int not null,
input CHAR(50) NOT NULL,
CONSTRAINT pk_id PRIMARY KEY (defn_id, attr_id, seqnr)
);
This temp table has primary key as (defn_id,attr_id,seqnr) as a whole!
Then I inserted the record in the temp table as
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (1,100,100,'test1');
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (2,100,100,'test2');
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (3,100,100,'test3');
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (4,100,100,'test4');
INSERT INTO temp(seqnr,defn_id,attr_id,input)
VALUES (5,100,100,'test5');
in both Oracle and Postgres!
The table now contains:
seqnr | defn_id | attr_id | input
1 | 100 | 100 | test1
2 | 100 | 100 | test2
3 | 100 | 100 | test3
4 | 100 | 100 | test4
5 | 100 | 100 | test5
When I run the command:
UPDATE temp SET seqnr=seqnr+1
WHERE defn_id = 100 AND attr_id = 100 AND seqnr >= 1;
In case of ORACLE it is Updating 5 Rows and the O/p is
seqnr | defn_id | attr_id | input
2 | 100 | 100 | test1
3 | 100 | 100 | test2
4 | 100 | 100 | test3
5 | 100 | 100 | test4
6 | 100 | 100 | test5
But in case of PostgreSQL it is giving an error!
DETAIL: Key (defn_id, attr_id, seqnr)=(100, 100, 2) already exists.
Why does this happen and how can I replicate the same result in Postgres as Oracle?
Or how can the same result be achieved in Postgres without any errors?
UNIQUE an PRIMARY KEY constraints are checked immediately (for each row) unless they are defined DEFERRABLE - which is the solution you demand.
ALTER TABLE temp
DROP CONSTRAINT pk_id
, ADD CONSTRAINT pk_id PRIMARY KEY (defn_id, attr_id, seqnr) DEFERRABLE
;
Then your UPDATE just works.
db<>fiddle here
This comes at a cost, though. The manual:
Note that deferrable constraints cannot be used as conflict
arbitrators in an INSERT statement that includes an ON CONFLICT DO UPDATE clause.
And for FOREIGN KEY constraints:
The referenced columns must be the columns of a non-deferrable unique
or primary key constraint in the referenced table.
And:
When a UNIQUE or PRIMARY KEY constraint is not deferrable,
PostgreSQL checks for uniqueness immediately whenever a row is
inserted or modified. The SQL standard says that uniqueness should be
enforced only at the end of the statement; this makes a difference
when, for example, a single command updates multiple key values. To
obtain standard-compliant behavior, declare the constraint as
DEFERRABLE but not deferred (i.e., INITIALLY IMMEDIATE). Be aware
that this can be significantly slower than immediate uniqueness
checking.
See:
Constraint defined DEFERRABLE INITIALLY IMMEDIATE is still DEFERRED?
I would avoid a DEFERRABLE PK if at all possible. Maybe you can work around the demonstrated problem? This usually works:
UPDATE temp t
SET seqnr = t.seqnr + 1
FROM (
SELECT defn_id, attr_id, seqnr
FROM temp
WHERE defn_id = 100 AND attr_id = 100 AND seqnr >= 1
ORDER BY defn_id, attr_id, seqnr DESC
) o
WHERE (t.defn_id, t.attr_id, t.seqnr)
= (o.defn_id, o.attr_id, o.seqnr);
db<>fiddle here
But there are no guarantees as ORDER BY is not specified for UPDATE in Postgres.
Related:
UPDATE with ORDER BY

Audited table and foreign key

I have a database with multiples tables that must be audited.
As an example, I have a table of objects defined with an unique ID, a name and a description.
The name will always be the same. It is not possible to update it. "ObjectA" will always be "ObjectA".
As you see the name is not unique in the database but only in the logic.
The rows "from", "to" and "creator_id" are used to audit the changes. "from" is the date of the change, "to" is the date when a new row has been added and is null when it is the latest row. "creator_id" is the ID of the user that made the change.
+----+----------+--------------+----------------------+----------------------+------------+
| id | name | description | from | to | creator_id |
+----+----------+--------------+----------------------+----------------------+------------+
| 1 | ObjectA | My object | 2021-05-30T00:05:00Z | 2021-05-31T05:04:36Z | 18 |
| 2 | ObjectB | My desc | 2021-05-30T02:07:25Z | null | 15 |
| 3 | ObjectA | Super object | 2021-05-31T05:04:36Z | null | 20 |
+----+----------+--------------+----------------------+----------------------+------------+
Now I have another table that must have a foreign key to this object table based on the "unique" object name.
+----+---------+-------------+
| id | foo | object_name |
+----+---------+-------------+
| 1 | blabla | ObjectA |
| 2 | wawawa | ObjectB |
+----+---------+-------------+
How can I create this link between those 2 tables ?
I already tried to create another table with a uuid and add a column "unique_identifier" in the object table. The foreign key will be then linked to this uuid table and not the object table. The issue is that I have multiple tables with this problem and I will have to create the double number of table.
It is also possible to use the object ID as the FK instead of the name but it would mean that I must update every table with that FK with the new ID when updating an object.
By the SQL standard, a foreign key must reference either the primary key or a unique key of the parent table. If the primary key has multiple columns, the foreign key must have the same number and order of columns. Therefore the foreign key references a unique row in the parent table; there can be no duplicates.
Another solution is to use trigger, you can check the existence of the object in objects table before you insert into another table.
Update : Adding code
Prepare the tables and create trigger: (I have only included 3 columns in Objects table for simplicity. In trigger, I am just printing the error in else part, you could raise error suing RAISEERROR function to return the error to client)
Create table AuditObjects(id int identity (1,1),ObjectName varchar(20), ObjectDescription varchar(100) )
Insert into AuditObjects values('ObjectA','description ObjectA Test')
Insert into AuditObjects values('ObjectB','description ObjectB Test')
Insert into AuditObjects values('ObjectC','description ObjectC Test')
Insert into AuditObjects values('ObjectB','description ObjectB Test')
Insert into AuditObjects values('ObjectB','description ObjectB Test')
Insert into AuditObjects values('ObjectA','description ObjectA Test')
Create table ObjectTab2 (id int identity (1,1),foo varchar(200), ObjectName varchar(20))
go
CREATE TRIGGER t_CheckObject ON ObjectTab2 INSTEAD OF INSERT
AS BEGIN
Declare #errormsg varchar(200), #ObjectName varchar(20)
select #ObjectName = objectname from INSERTED
if exists(select 1 from AuditObjects where objectname = #ObjectName)
Begin
INSERT INTO ObjectTab2 (foo, Objectname)
Select foo, Objectname
from INSERTED
End
Else
Begin
Select #errormsg = 'Object '+objectname+ ' does not exists in AuditObjects table'
from Inserted
print(#errormsg)
End
END;
Now if you try to insert a row in ObjectTab2 with object name as "ObjectC", insert will be allowed as "objectC" is present in audit table.
Insert into ObjectTab2 values('blabla', 'ObjectC')
Select * from ObjectTab2
id foo ObjectName
----------- ------ --------------------
1 blabla ObjectC
However, if you try to enter "ObjectD", it will not make an insert and give error msg in output.
Insert into ObjectTab2 values('Inserting ObjectD', 'ObjectD')
Object ObjectD does not exists in AuditObjects table
Well its not what you asked for but give you the same functionality and results.
Can you not still go ahead with linking the two tables based on 'object name'. The only difference would be - when you join the two tables, you would get multiple records from table1 (the first table you were referencing). You may then add filter condition based on from and to, as per your requirements.
Post Edit -
What I meant is, you can still achieve the desired results without introducing Foreign Key in this scenario -
Let's call your tables - Table1 and Table2
--Below will give you all records from Table1
SELECT T2.*, T1.description, T1.creator_id, T1.from, T1.to
FROM TABLE2 T2
INNER JOIN TABLE1 T1 ON T2.OBJECT_NAME = T1.NAME;
--Below will give you ONLY those records from Table1 whose TO is null
SELECT T2.*, T1.description, T1.creator_id, T1.from, T1.to
FROM TABLE2 T2
INNER JOIN TABLE1 T1 ON T2.OBJECT_NAME = T1.NAME
WHERE T1.TO IS NULL;
I decided to go with an additional table to have this final design:
Table "Object"
+-------+--------------------------------------+---------+--------------+----------------------+----------------------+------------+
| id PK | identifier FK | name | description | from | to | creator_id |
+-------+--------------------------------------+---------+--------------+----------------------+----------------------+------------+
| 1 | 123e4567-e89b-12d3-a456-426614174000 | ObjectA | My object | 2021-05-30T00:05:00Z | 2021-05-31T05:04:36Z | 18 |
| 2 | 123e4567-e89b-12d3-a456-524887451057 | ObjectB | My desc | 2021-05-30T02:07:25Z | null | 15 |
| 3 | 123e4567-e89b-12d3-a456-426614174000 | ObjectA | Super object | 2021-05-31T05:04:36Z | null | 20 |
+-------+--------------------------------------+---------+--------------+----------------------+----------------------+------------+
Table "Object_identifier"
+--------------------------------------+
| identifier PK |
+--------------------------------------+
| 123e4567-e89b-12d3-a456-426614174000 |
| 123e4567-e89b-12d3-a456-524887451057 |
+--------------------------------------+
Table "foo"
+-------+--------+--------------------------------------+
| id PK | foo | object_identifier FK |
+-------+--------+--------------------------------------+
| 1 | blabla | 123e4567-e89b-12d3-a456-426614174000 |
| 2 | wawawa | 123e4567-e89b-12d3-a456-524887451057 |
+-------+--------+--------------------------------------+

Uniqueness constraint on cross between two rows

I'm creating a (postgres) table that has:
CREATE TABLE workers (id INT PRIMARY KEY, deleted_at DATE, account_id INT)
I'd like to have a uniqueness constraint only across workers that have not been deleted. Is there a good way to achieve this in sql? As an example:
id | date | account_id
1 | NULL | 1
# valid, was deleted
2 | yesterday | 1
# invalid, dup account
# 3 | NULL | 1
You want what Postgres calls a "partial index" (and other databases call a filtered index):
create unique index idx_workers_account_id on workers(account_id)
where deleted_at is null;
Here is the documentation on this feature.

Inserting a row at the specific place in SQLite database

I was creating the database in SQLite Manager & by mistake I forgot to mention a row.
Now, I want to add a row in the middle manually & below it the rest of the Auto-increment keys should be increased by automatically by 1 . I hope my problem is clear.
Thanks.
You shouldn't care about key values, just append your row at the end.
If you really need to do so, you could probably just update the keys with something like this. If you want to insert the new row at key 87
Make room for the key
update mytable
set key = key + 1
where key >= 87
Insert your row
insert into mytable ...
And finally update the key for the new row
update mytable
set key = 87
where key = NEW_ROW_KEY
I would just update IDs, incrementing them, then insert record setting ID manually:
CREATE TABLE cats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR
);
INSERT INTO cats (name) VALUES ('John');
INSERT INTO cats (name) VALUES ('Mark');
SELECT * FROM cats;
| 1 | John |
| 2 | Mark |
UPDATE cats SET ID = ID + 1 WHERE ID >= 2; -- "2" is the ID of forgotten record.
SELECT * FROM cats;
| 1 | John |
| 3 | Mark |
INSERT INTO cats (id, name) VALUES (2, 'SlowCat'); -- "2" is the ID of forgotten record.
SELECT * FROM cats;
| 1 | John |
| 2 | SlowCat |
| 3 | Mark |
Next record, inserted using AUTOINCREMENT functionality, will have next-to-last ID (4 in our case).