Postgresql Upsert based on conditition - sql

I have the following tables
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (),
...
CREATE TABLE tags (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (),
user_id UUID NOT NULL references users (id),
tag VARCHAR(200) NOT NULL,
...
}
I would like to form a query that inserts a tag based on the following constraints:
For a given user_id in the tags table, all entries must have unique tags
Different user_ids can have the same tag. For example:
The following should be valid in the tag table
id
user_id
tag
some-tag-uuid-1
some-user-uuid-1
foo
some-tag-uuid-2
some-user-uuid-1
bar
some-tag-uuid-3
some-user-uuid-2
foo
Note the differences in user_id .
The following should NOT be valid in the tag table
id
user_id
tag
some-tag-uuid-1
some-user-uuid-1
foo
some-tag-uuid-2
some-user-uuid-1
foo
If an entry exists, I should return the existing tag id. If not, we insert the new tag
and return the new tag's id.
What I currently have
As of now, the only query I can come up with is split into two parts and the app handles the intermediate logic.
For a given tag to insert e.g.
{id: 'some-tag-uuid-1', user_id: 'some-user-uuid-1', tag: 'busy'};
SELECT id FROM tag WHERE user_id = 'some-user-uuid-1' AND tag = 'busy'
From the resulting rows, I then check if it exists, if so, I return the existing id, if not I insert the new id in the tag table returning the new id.
I'm not sure if this approach is the best approach, and would like a single more performant query (if possible)

As stated by #SebDieBln :
You add a unique constraint in the tags table definition : CONSTRAINT unique_constraint UNIQUE (user_id, tag)
You add ON CONFLICT DO NOTHING in the INSERT statement
You add the RETURNING clause in the INSERT statement in order to get the new tag when inserted
But when the tag value already exists for the user_id, the returned value is NULL, so you need to catch the tag input value instead.
Finaly you can do everything within a sql function :
CREATE OR REPLACE FUNCTION test (IN _user_id UUID , INOUT _tag VARCHAR(200), OUT _rank INTEGER)
RETURNS record LANGUAGE sql AS
$$
WITH cte AS (INSERT INTO tags (user_id, tag) VALUES (_user_id, _tag) ON CONFLICT DO NOTHING RETURNING tag)
SELECT tag, 1 FROM cte
UNION
SELECT _tag, 2
ORDER BY 2
LIMIT 1 ;
$$
And you call the sql function to get the expected behavior :
SELECT _tag FROM test('some-user-uuid-1', 'busy')
see the test result in dbfiddle.

Related

Copy data from one table to another existing table combined by calculated variable and specific columns

The scenario is as described in the following steps (this is only an example to illustrate the problem)
There are two tables in the database (user_1, post_1) - no real relation between them
user_1 contains the following fields:
id VARCHAR,
name VARCHAR,
address TEXT,
phone_number VARCHAR,
PRIMARY KEY (id)
post_1 contains the following fields:
id VARCHAR,
user_id VARCHAR,
title VARCHAR,
body TEXT,
PRIMARY KEY (id)
Suppose I added the following data to the two tables above:
INSERT INTO user_1(id, name, address, phone_number) VALUES ('first_u', 'avi', 'some address', '05488789906');
INSERT INTO post_1(id, user_id, title, body) VALUES ('first_p', 'first_u', 'new post', 'This is a good one!');
Now I've created new tables with few changes and with a link between them:
user_2 contains the following fields:
id uuid DEFAULT uuid_generate_v4(),
name VARCHAR,
address TEXT,
phone_number VARCHAR,
PRIMARY KEY (id)
post_2 contains the following fields:
id uuid DEFAULT uuid_generate_v4(),
user_id uuid,
title VARCHAR,
body TEXT,
PRIMARY KEY (id),
CONSTRAINT fk_user FOREIGN KEY(user_id) REFERENCES user_2(id)
Now the problem is with the copy of the data to the two new tables with the real link (foreign key) between them.
I will explain - it is divided into few parts:
a. Count the number of rows in the user_1 table (I've created a function for that):
CREATE OR REPLACE FUNCTION rowsnumber() RETURNS INTEGER AS $$
DECLARE
numberOfRows integer;
BEGIN
SELECT COUNT(*) INTO numberOfRows FROM (
SELECT *
FROM user_1
) t;
RETURN numberOfRows;
END;
$$ LANGUAGE plpgsql;
b. Go through the function result in the for loop and do the following:
(b.1) Extract the row number x according to the current index in the for loop (can achieve this by using limit and offset) from user_1 table.
(b.2) Insert the row data into the user_2 table.
(b.3) Extract the newly created uuid and save it in a parameter.
(b.4) Retrieve the appropriate row from the post_1 table according to the id column of the current row stored in the user_id column of the post_1 table.
(b.5) Insert the appropriate data of the row that we extracted in stage (b.4) into the post_2 table BUT with the user_id that we extracted in step (b.3) so that there is a real relationship between the post_2 table and the user_2 table.
I would greatly appreciate any help - if someone could write the query I would need to run to solve this problem.
Many thanks.
After several attempts I was able to implement the function that would answer the question - according to the steps I described above.
CREATE OR REPLACE FUNCTION insertToTables() RETURNS VOID AS $$
DECLARE
numOfRowsInTable integer := rowsnumber();
newRow record;
userUUIDRow record;
postRow record;
BEGIN
for r in 0..numOfRowsInTable-1
loop
SELECT * INTO newRow FROM user_1 LIMIT 1 OFFSET r;
INSERT INTO user_2(name, address, phone_number) SELECT newRow.name,newRow.address, newRow.phone_number ;
SELECT * INTO userUUIDRow FROM user_2 ORDER BY created_at DESC LIMIT 1;
SELECT * INTO postRow FROM post_1 WHERE user_id = newRow.id;
INSERT INTO post_2(user_id, title, body) SELECT userUUIDRow.id, postRow.title, postRow.body;
end loop;
END;
$$ LANGUAGE plpgsql;

PostgreSQL- insert result of query into exisiting table, auto-increment id

I have created an empty table with the following SQL statement. My understanding (based on this tutorial: https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-serial/) was that SERIAL PRIMARY KEY will automatically provide an auto-incremented id for every new row:
CREATE TABLE "shema".my_table
(
id SERIAL PRIMARY KEY,
transaction text NOT NULL,
service_privider text NOT NULL,
customer_id text NOT NULL,
value numeric NOT NULL
)
WITH (
OIDS = FALSE
);
ALTER TABLE "shema".my_table
OWNER to admin;
Now I am querying another tables and would like to save the result of that query into my_table. The result of the query outputs following schema:
transaction
service_provider
customer_id
value
meaning the schema of my_table minus id. when I try to execute:
INSERT into my table
Select {here is the query}
Then I am getting an error that column "id" is of type integer but expression is of type text. I interpret it that the sql query is looking for id column and cannot find it. How can I insert data into my_table without explicitly stating id number but have this id auto-generated for every row?
Always mention the columns you want to INSERT:
INSERT INTO schemaname.my_table("transaction", service_privider, customer_id, value)
SELECT ?, ?, ?, ?;
If you don't, your code will break now or somewhere in the future.
By the way, transaction is a reserved word, try to use a better column name.

How to efficiently insert ENUM value into table?

Consider the following schema:
CREATE TABLE IF NOT EXISTS snippet_types (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER NOT NULL PRIMARY KEY,
title TEXT,
content TEXT,
type INTEGER NOT NULL,
FOREIGN KEY(type) REFERENCES snippet_types(id)
);
This schema assumes a one-to-many relationship between tables and allows efficiently maintaining a set of ENUMs in the snippet_types table. Efficiency comes from the fact that we don't need to store the whole string describing snippet type in the snippets table, but this decision also leads us to some inconvenience: upon inserting we need to retrieve snippet id from snippet_types and this leads to one more select and check before inserting:
SELECT id FROM snippet_types WHERE name = "foo";
-- ...check that > 0 rows returned...
INSERT INTO snippets (title, content, type) values ("bar", "buz", id);
We could also combine this insert and select into one select like that:
INSERT INTO snippets (title, content, type)
SELECT ("bar", "buz", id) FROM snippet_types WHERE name = "foo"
However, if "foo" type is missing in snippet_types then 0 rows would have been inserted and no error returned and I don't see a possibility to get a number of rows sqlite actually inserted.
How can I insert ENUM-containing tuple in one query?

Populate virtual SQLite FTS5 (full text search) table from content table

I've followed https://kimsereylam.com/sqlite/2020/03/06/full-text-search-with-sqlite.html to set up SQLite's virtual table extension FTS5 for full text search on an external content table.
While the blog shows how to set up triggers to keep the virtual FTS table updated with the data:
CREATE TABLE user (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
short_description TEXT
)
CREATE VIRTUAL TABLE user_fts USING fts5(
username,
short_description,
email UNINDEXED,
content='user',
content_rowid='id'
)
CREATE TRIGGER user_ai AFTER INSERT ON user
BEGIN
INSERT INTO user_fts (rowid, username, short_description)
VALUES (new.id, new.username, new.short_description);
END;
...
I am failing to populate the FTS table from all previous data in an analogous fashion.
I'll stick to the example from the blog:
INSERT INTO user_fts (rowid, username, short_description) SELECT (id, username, short_description) FROM user;
However, sqlite (3.37.2) fails with row value misused.
Please explain how id, content_rowid, rowid and new.id are related and how to modify the query to update the FTS table properly.
INSERT INTO user_fts (rowid, username, short_description) SELECT id, username, short_description FROM user;
(no parentheses) works.
rowid is a unique 64 bit unsigned integer row id.
If the table contains an integer primary key (as id in user), they are the same (alias). I.e. user.rowid == user.id = user_fts.rowid.
Doc: https://www.sqlite.org/lang_createtable.html#rowid
The new refers to the element being inserted.
Doc: https://www.sqlite.org/lang_createtrigger.html
content_rowid links the virtual FTS table to the external data table row id column (it defaults to rowid).
Doc: https://www.sqlite.org/fts5.html#external_content_tables

Tackling nested inserts using functions

Hi people i need some help deciding on the best way to do an insert into table ‘shop’ which has a serial id field. I also need to insert into tables ‘shopbranch’ and ‘shopproperties’ which both references shop.id.
In a nutshell I need to insert one shop record. Then two records for each table of the following tables, shopproperty and shopbranch, whose shopid (FK) references the just created shop.id field
I saw somewhere that i could wrap the ‘shop’ insert, inside a function called lets say ‘insert_shop’ which does the 'shop' insert and returns its id using a select statement
Then inside another function which inserts shoproperty and shopbranch records i could do one call to insert_shop function to return the shop id which can be used to be passed in as the shop id for the records.
Can you let me know if I’m looking at this in the correct way as I’m a newbie.
One way to approach this is to create a view on your three tables that shows all columns from all three tables that can be inserted or updated. If you then create an INSTEAD OF INSERT trigger on the view then you can manipulate the view contents as if it were a table. You can do the same with UPDATE and even combine the two into an INSTEAD OF INSERT OR UPDATE trigger. The function that your trigger calls then has three INSERT statements that redirect the insert on the view to the underlying tables:
CREATE TABLE shop (
id serial PRIMARY KEY,
nm text,
...
);
CREATE TABLE shopbranch (
id serial PRIMARY KEY,
shop integer NOT NULL REFERENCES shop,
branchcode text,
loc text,
...
);
CREATE TABLE shopproperties (
id serial PRIMARY KEY,
shop integer NOT NULL REFERENCES shop,
prop1 text,
prop2 text,
...
);
CREATE VIEW shopdetails AS
SELECT s.*, b.*, p.*
FROM shop s, shopbranch b, shopproperties p,
WHERE b.shop = s.id AND p.shop = s.id;
CREATE FUNCTION shopdetails_insert() RETURNS trigger AS $$
DECLARE
shopid integer;
BEGIN
INSERT INTO shop (nm, ...) VALUES (NEW.nm, ...) RETURNING id INTO shopid;
IF NOT FOUND
RETURN NULL;
END;
INSERT INTO shopbranch (shop, branchcode, loc, ...) VALUES (shopid, NEW.branchcode, NEW.loc, ...);
INSERT INTO shopproperties(shop, prop1, prop2, ...) VALUES (shopid, NEW.prop1, NEW.prop2, ...);
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER shopdetails_trigger_insert
INSTEAD OF INSERT
FOR EACH ROW EXECUTE PROCEDURE shopdetails_insert();
You could of course play with the view and show only those columns from the three tables that can be inserted or updated (such as excluding primary and foreign keys).