Simulating UPSERT in presence of UNIQUE constraints - sql

Simulating UPSERT was already discusssed before. In my case though, I have PRIMARY KEY and additional UNIQUE constraint, and I want upsert semantic with respect to primary key - replacing existing row if it exists, while checking the unique constraint.
Here's an attempt using insert-or-replace:
drop table if exists test;
create table test (id INTEGER, name TEXT, s INTEGER,
PRIMARY KEY (id, s),
UNIQUE (name, s));
insert or replace into test values (1, "a", 0);
insert or replace into test values (1, "a", 0);
insert or replace into test values (2, "b", 0);
insert or replace into test values (2, "a", 0);
The last statement is replaces both rows. This is documented behavior of 'insert or replace', but not what I want.
Here is an attempt with "on conflict replace":
drop table if exists test;
create table test (id INTEGER, name TEXT, s INTEGER,
PRIMARY KEY (id, s) on conflict replace,
UNIQUE (name, s));
insert into test values (1, "a", 0);
insert into test values (1, "a", 0);
I get "UNIQUE constraint failed" right away. The problem disappears if don't share column between both primary key and unique constraint:
drop table if exists test;
create table test (id INTEGER, name TEXT,
PRIMARY KEY (id) on conflict replace,
UNIQUE (name));
insert into test values (1, "a");
insert into test values (1, "a");
insert into test values (2, "b");
insert into test values (2, "a");
Here, I get constraint violation on the very last statement, which is precisely right. Sadly, I do need to share a column between constraints.
Is this something I don't understand about SQL, or SQLite issue, and how do I get the desired effect, except for first trying insert and then doing update on failure?

Can you try to apply the ON CONFLICT REPLACE clause to the UNIQUE constraint also?
create table test (id INTEGER, name TEXT,
PRIMARY KEY (id) on conflict replace,
UNIQUE (name) on conflict replace);

SQLite is an embedded database without client/server communication overhead, so it is not necessary to try to do this in a single statement.
To simulate UPSERT, just execute the UPDATE/INSERT statements separately:
c.execute("UPDATE test SET s = ? WHERE id = ? AND name = ?", [0, 1, "a"])
if c.rowcount == 0:
c.execute("INSERT INTO test(s, id, name) VALUES (?, ?, ?)", [0, 1, "a"])
Since SQLite 3.24.0, you can just use UPSERT.

Related

Unique constraint on inserting new row

I wrote a SQL statement within PostgreSQL 12 and I first created an unique constraint like:
CONSTRAINT post_comment_response_approval__tm_response__uidx UNIQUE (post_comment_response_id, team_member_id)
On a SQL query:
INSERT INTO post_comment_response_approval (post_comment_response_id, team_member_id, approved, note)
VALUES (:postCommentResponseId, :workspaceMemberId, :approved, :note)
ON CONFLICT ON CONSTRAINT post_comment_response_approval__tm_response__uidx DO
UPDATE SET approved = :approved, note = :note
Fist, I wanted to use it for the same row whenever ever some action is made, but now I just want to make sure the API shows them if multiple actions have been submitted by the same member.
An example is that someone might suggest a change, then that change is made, then that person who suggested it later approves it. That would generate multiple post_comment_response_approval rows for that approver.
Is there a way to make it happen without removing unique constraint or maybe it should be deleted? I am new with PostgreSQL.
I didn't understand your question in detail. But I think I understood what you need. You can use PostgreSQL partial indexing.
Examples for you:
CREATE TABLE table6 (
id int4 NOT NULL,
id2 int4 NOT null,
approve bool NULL
);
-- creating partial indexing
CREATE UNIQUE INDEX table6_id_idx ON table6 (id,id2) where approve is true;
insert into table6 (id, id2, approve) values (1, 1, false);
-- success insert
insert into table6 (id, id2, approve) values (1, 1, false);
-- success insert
insert into table6 (id, id2, approve) values (1, 1, false);
-- success insert
insert into table6 (id, id2, approve) values (1, 1, true);
-- success insert
insert into table6 (id, id2, approve) values (1, 1, true);
-- error: duplicate key value violates unique constraint "table6_id_idx"
So, you get unique fields by condition.

How can I insert values into a table with a composite primary key?

These are the tables I already have:
CREATE TABLE Gyartok
(
GyID INT IDENTITY(2, 3),
Nev VARCHAR(20),
CONSTRAINT PK_Gyartok PRIMARY KEY (GyID)
)
CREATE TABLE Focicsuka
(
CsID INT IDENTITY(2, 2),
Meret INT,
CONSTRAINT PK_Focicsuka PRIMARY KEY (CsID)
)
CREATE TABLE FcsGyartjaGya
(
GyID INT IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1
FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2
FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya
PRIMARY KEY (GyID, CsID)
)
The problem is that every time I try to add new values to the table (like such)
INSERT INTO FcsGyartjaGya (Ar) VALUES (300);
I get an error saying I didn't initialize the CsID INT column:
Cannot insert the value NULL into column 'CsID', table 'Lab3.dbo.FcsGyartjaGya'; column does not allow nulls. INSERT fails.
I know I must initialize it with something, but I have no idea what do to it with, because IDENTITY(x, y) doesn't work (it's occupied by another column already) and adding another parameter to the code (like such)
INSERT INTO FcsGyartjaGya (Ar, CsID) VALUES (300, 7);
creates another error which says
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Lab3a", table "dbo.Gyartok", column 'GyID'.
It is important to note that I already filled every column with data, so that couldn't be the problem.
As I mention in the comments, your INSERT will work fine, provided the stars align correctly. For your table Gyartok you have GyID as your PRIMARY KEY, which is defined as a IDENTITY(2,3); so the first value generated is 2 and then each row attempted to be INSERTed will increment by 3.
So, if we run the following, we get the IDs 2, 5, 7 and 17. (11 and 14 are skipped as the INSERT failed).
CREATE TABLE Gyartok (
GyID INT IDENTITY(2, 3),
Nev VARCHAR(20),
CONSTRAINT PK_Gyartok PRIMARY KEY (GyID)
);
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES ('asdfjahsbvd'),
('ashjkgdfakd'),
('kldfbhjo');
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES (REPLICATE('A',25)), --Force a truncation error
('ashjkgdfakd');
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES (REPLICATE('A',15));
Let's now add some data for your other table:
CREATE TABLE Focicsuka (
CsID INT IDENTITY(2, 2),
Meret INT,
CONSTRAINT PK_Focicsuka PRIMARY KEY (CsID)
)
INSERT INTO dbo.Focicsuka (Meret)
VALUES(12),
(25);
Now we want to INSERT into the table FcsGyartjaGya, defined as the following:
CREATE TABLE FcsGyartjaGya (
GyID INT IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1 FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2 FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya PRIMARY KEY (GyID, CsID)
)
This has a IDENTITY on GyID, but defined as an IDENTITY(3,2), so the first value is 3 and then incremented by 2.
As this has 2 foreign keys, on GyID and CsID when we INSERT the row the values must appear in the respective tables. As GyID is defined as anIDENTITY(3,2) however, this is where we need to rely on the Stars luck for the INSERT to work. Why? Well 2 + (3*n) and 3+(2*n) can give very different numbers. The first are as you saw at the start of this answer. For the latter, we have numbers like 3, 5, 7, 9, 11. As you can see, only 1 in 3 of these numbers match a number in our original sequence, so luck is what we are going to be relying on.
Let's, therefore, try a single INSERT.
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(2,1);
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Sandbox", table "dbo.Gyartok", column 'GyID'.
Well, that didn't work, but it was expected. 3 isn't a value in the table Gyartok. Let's try again!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(2,2);
It worked! The stars Luck was our side, and the IDENTITY value was a value in the table Gyartok. Let's try a couple of rows this time!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,3),
(4,4);
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Sandbox", table "dbo.Gyartok", column 'GyID'.
No!! Not again. :( That's because the stars didn't align; 7 and 9 aren't in the other table. But wait, 11 was in the sequence, so let's try that:
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,5);
Error, again?! No, it cannot be!!! :( Oh wait, I forgot, the stars were against us before, because that INSERT failed against Gyartok for the value of 11. I need to wait for 17!
--13 fails
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
GO
--15 fails
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
GO
--17 works!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
And now we have another row in our table.
So what is the problem? Your design. GyID is defined as an IDENTITY and a FOREIGN KEY; meaning you are at the "whims" of SQL Server generating a value valid. This is not what you want. Just don't define the column as an IDENTITY and then INSERT the data with all 3 of your columns defined:
CREATE TABLE FcsGyartjaGya (
GyID int,-- IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1 FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2 FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya PRIMARY KEY (GyID, CsID)
)
GO
INSERT INTO dbo.FcsGyartjaGya (GyID, CsID, Ar)
VALUES(2,2,1),
(2,4,2),
(5,4,3),
(8,2,4),
(8,4,5);
And all these rows insert fine.
I think there is a bit confusion, if I understand correctly what You're trying to do, then you have two tables each with their own id, which is based on an identity column, so you get new values in those for free.
Then you are trying to make a relation table with extra data.
Issue 1: You cannot have FcsGyartjaGya.GyID be identity if it refers to Gyartok.GyID because you will want to insert into it and not rely on an auto increment. If it doesn't refer to the same it should have another name or my head will possibly explode :))
Issue 2: When populating a relation table you need to insert it with what pairs you want, there is no way SQL server can know how it should match these identity pairs in the relation table
I think this is what people are aiming at in the comments, for example
to insert a relationship between row with Focicsuka.CsID = 1 to Gyartok.GyID 7 and adding Ar = 300 have to look like
INSERT INTO FCSGYARTJAGYA(GYID, CSID, AR)
VALUES(7, 1, 300)
Unless You've forgotten to mention that you want to put some value for each of some value or based on something which can be scripted, in other words unless You have logics to define the pairs and their values, relationship tables cannot have defaults on their foreign key fields.

Violating foreign key constraint with deferred constraint

I'm trying to set my sql scripts into a transaction to achieve atomicity with my database.
The table structure is (simplified):
CREATE TABLE foo (
id serial NOT NULL,
foo varchar(50) NOT NULL,
CONSTRAINT foo_pk PRIMARY KEY (id)
);
CREATE TABLE access (
id serial NOT NULL,
foo_id int NULL
CONSTRAINT access_pk PRIMARY KEY (id)
);
ALTER TABLE access ADD CONSTRAINT access_foo
FOREIGN KEY (foo_id)
REFERENCES foo (id)
ON DELETE CASCADE
ON UPDATE CASCADE
DEFERRABLE
INITIALLY DEFERRED;
In my code I first declare:
client.query('BEGIN'); (I'm using npm library 'pg') then
insert a row into a table 'foo', then another insert to 'access' with a foo_id from the first insert. After that there is client.query('COMMIT');
All of this is in a try catch, and in the catch is client.query('ROLLBACK'); and the rolling back seems to be working if there is an issue either of the inserts. When everything should be committed I still end up in the catch block for this:
message: "insert or update on table "access" violates foreign key constraint "access_foo""
detail: "Key (foo_id)=(20) is not present in table "foo"."
I thought that deferring constraint would be enough to do this, but I guess I'm wrong. Any help is welcome.
You probably have some issue with the transaction demarcation. I ran a simple test and works wells.
insert into foo (id, foo) values (1, 'Anne');
start transaction;
insert into access (id, foo_id) values (101, 1);
insert into access (id, foo_id) values (107, 7); -- 7 does not exist yet...
insert into foo (id, foo) values (7, 'Ivan'); -- 7 now exists!
commit; -- at this point all is good
See running example at DB Fiddle.

Check if many-to-many relationship exists before insert or delete

I have 3 tables
For example:
Book
id
title
Tag
id
name
BookTag
book_id
tag_id
The goal to disallow having Book without Tag. i.e. when I try insert/delete data I need something to check on database level that Book has at least one Tag through many-to-many. If such validation fails it should throw constaint violation error or some sort of that. How should I implement that? Can it be reached by check constraint or should I create some trigger, if so then how?
please help me. thanks for your help in advance
You can enforce this at the pure database level by adding a foreign key in the book table that points back to a tag (any tag) in the book_tag table. As of now, your database model looks like:
create table book (
id int primary key not null,
title varchar(50)
);
create table tag (
id int primary key not null,
name varchar(50)
);
create table book_tag (
book_id int not null,
book_tag int not null,
primary key (book_id, book_tag)
);
Now, add the extra foreign key that points back to a tag:
alter table book add column a_tag int not null;
alter table book add constraint fk1 foreign key (id, a_tag)
references book_tag (book_id, tag_id) deferrable initially deferred;
Now when you insert a book, it can temporarily not have a tag, but only while the transaction hasn't finished yet. You need to insert a tag before committing. If you don't the constraint will fail, the transaction will rollback, and the insert won't happen.
Note: Please notice that this requires the use of deferrable constraints (look at deferrable initially deferred), something that is part of the SQL Standard but seldomly implemented. Fortunately, PostgreSQL does.
EDIT - Adding an example
Considering the previous modified tables you can try inserting a book without tags (will fail) and with tags (succeeding) as shown below:
insert into tag (id, name) values (10, 'classic');
insert into tag (id, name) values (12, 'action');
insert into tag (id, name) values (13, 'science fiction');
-- begin transaction
insert into book (id, title, a_tag) values (1, 'Moby Dick', 123);
commit; -- fails
-- begin transaction
insert into book (id, title, a_tag) values (2, 'Frankenstein', 456);
insert into book_tag (book_id, book_tag) values (2, 10);
insert into book_tag (book_id, book_tag) values (2, 13);
update book set a_tag = 10;
commit; -- succeeds

Conditional composite key in MySQL?

So I have this table with a composite key, basically 'userID'-'data' must be unique (see my other question SQL table - semi-unique row?)
However, I was wondering if it was possible to make this only come into effect when userID is not zero? By that I mean, 'userID'-'data' must be unique for non-zero userIDs?
Or am I barking up the wrong tree?
Thanks
Mala
SQL constraints apply to every row in the table. You can't make them conditional based on certain data values.
However, if you could use NULL instead of zero, you can get around the unique constraint. A unique constraint allows multiple entries that have NULL. The reason is that uniqueness means no two equal values can exist. Equality means value1 = value2 must be true. But in SQL, NULL = NULL is unknown, not true.
CREATE TABLE MyTable (id SERIAL PRIMARY KEY, userid INT, data VARCHAR(64));
INSERT INTO MyTable (userid, data) VALUES ( 1, 'foo');
INSERT INTO MyTable (userid, data) VALUES ( 1, 'bar');
INSERT INTO MyTable (userid, data) VALUES (NULL, 'baz');
So far so good, now you might think the following statements would violate the unique constraint, but they don't:
INSERT INTO MyTable (userid, data) VALUES ( 1, 'baz');
INSERT INTO MyTable (userid, data) VALUES (NULL, 'foo');
INSERT INTO MyTable (userid, data) VALUES (NULL, 'baz');
INSERT INTO MyTable (userid, data) VALUES (NULL, 'baz');