Returning a column from an INSERTED record via an updateable view - sql

I have a view I need to allow my users to update and insert on. Importantly, when they Insert, they need to be able to return the new value from the Inserted row, however right now they get NULL. It must be a view, as in the SELECT of the view, it needs to be able to return values that are the result of a join.
My underlying table definitions:
CREATE TABLE my_assets (
asset_id bigserial not null primary key,
asset_price NUMERIC(32,10) -- This will vary constantly via an independent process
);
CREATE TABLE my_transactions (
id bigserial not null primary key,
asset_id bigint not null REFERENCES my_assets(asset_id),
some_text varchar(100)
);
INSERT INTO my_assets(asset_price) SELECT 100 as asset_price;
My view that shows the result of the table:
CREATE VIEW my_transactions_view AS
SELECT tx.id, tx.asset_id, tx.some_text, a.asset_price
FROM my_transactions tx
JOIN my_assets a ON tx.asset_id = a.asset_id
My trigger that allows insertion into my_transactions_view:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO my_transactions(asset_id, some_text)
SELECT NEW.asset_id, NEW.some_text;
RETURN NEW;
END
$BODY$
LANGUAGE 'plpgsql';
CREATE TRIGGER trig_my_transactions_view INSTEAD OF INSERT on my_transactions_view
FOR EACH ROW EXECUTE PROCEDURE trigfx_insert_to_my_transactions_view();
All good so far. However, the problem arises from trying to run the below SQL:
INSERT INTO my_transactions_view(asset_id, some_text)
SELECT 1 as asset_id, 'Hello World' as some_text
RETURNING id, asset_id, some_text;
The returned table returns NULL for ID, but I want to return the newly updated ID from the my_transactions table:
|---------------------|------------------|------------------|
| ID | asset_id | some_text |
|---------------------|------------------|------------------|
| null | 1 | Hello World |
|---------------------|------------------|------------------|
Running a subsequent SELECT * FROM my_transactions_view DOES produce the updated result:
|------------------|------------------|------------------|------------------|
| ID | asset_id | some_text | asset_price |
|------------------|------------------|------------------|------------------|
| 1 | 1 | Hello World | 100.0000000 |
|------------------|------------------|------------------|------------------|
but I need it produced during the RETURNING of the INSERT statement.
Thank you!!!

You can populate the new record with the generated ID:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO my_transactions(asset_id, some_text)
values (NEW.asset_id, NEW.some_text);
new.id := lastval(); --<< this gets the generated id from the transactions table
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
Online example
Alternatively you could use currval(pg_get_serial_sequence('my_transactions','id')) instead of lastval()

Turns out we can avoid the extra function call via SELECT INTO from a CTE:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
WITH ins_q as (INSERT INTO my_transactions(asset_id, some_text)
values (NEW.asset_id, NEW.some_text)
RETURNING id, asset_id, some_text)
SELECT ins_q.id, ins_q.asset_id, ins_q.some_text
INTO NEW.id, NEW.asset_id, NEW.some_text
FROM ins_q;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
See online example here.
I was running into initialisation errors (lastval is not yet defined in this session) with the new.id := lastval(); approach.

Related

Copy value from one column into another during insert using postgreSQL

If I have a table like this:
CREATE TABLE mytable
(
id SERIAL,
content TEXT,
copyofid INTEGER
);
Is there a way to copy id into copyofid in a single insert statement?
I tried:
INSERT INTO mytable(content, copyofid) VALUES("test", id);
But that doesn't seem to work.
You can find the sequence behind your serial column using pg_get_serial_sequence() and access it using currval() to get what serial column just got as a result of your INSERT.
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid INTEGER
);
--this works for a single-record insert
INSERT INTO mytable
(content, copyofid)
VALUES
('test', currval(pg_get_serial_sequence('mytable','id')));
--inserting more, you'll have to handle both columns relying on the sequence
INSERT INTO mytable
( id,
content,
copyofid)
VALUES
( nextval(pg_get_serial_sequence('mytable','id')),
'test3',
currval(pg_get_serial_sequence('mytable','id'))),
( nextval(pg_get_serial_sequence('mytable','id')),
'test4',
currval(pg_get_serial_sequence('mytable','id')));
table mytable;
-- id | content | copyofid
------+---------+----------
-- 1 | test | 1
-- 2 | test3 | 2
-- 3 | test4 | 3
--(3 rows)
Fiddle
Edouard makes makes a fair point that if you can specify the conditions when you want this behaviour, you can add them to the definition:
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid integer
generated always as (
case when content ilike '%requires copying ID%' then id end)
stored
);
insert into mytable (content) values ('abc') returning *;
-- id | content | copyofid
------+---------+----------
-- 1 | abc |
--(1 row)
insert into mytable (content) values ('abc, but requires copying ID') returning *;
-- id | content | copyofid
------+------------------------------+----------
-- 2 | abc, but requires copying ID | 2
--(1 row)
If they vary between inserts
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid integer
generated always as (
case when should_copy_id then id end)
stored,
should_copy_id boolean default false
);
insert into mytable (content) values ('efg') returning *;
-- id | content | copyofid | should_copy_id
------+---------+----------+----------------
-- 1 | efg | | f
--(1 row)
insert into mytable (content,should_copy_id) values ('klm','today'::date<>'2022-10-28'::date) returning *;
-- id | content | copyofid | should_copy_id
------+---------+----------+----------------
-- 2 | klm | 2 | t
--(1 row)
The trigger will be better if
the check is fairly complex - generated columns are pretty limited in terms of the definition complexity. For example, you can't use mutable functions in them - not even STABLE are accepted
you want to save the logic and change it later without having to drop the column each time, then re-add it with a new definition (only way to alter a generated column definition)
as a part of the insert you'll want to do more than just copy the id column
The solution is to create a trigger function which is fired before inserting a new row in table mytable and which copy NEW.id into NEW.copyofid if a condition is true :
CREATE OR REPLACE FUNCTION before_insert_mytable() RETURN trigger LANGUAGE plpgsql AS $$
BEGIN
IF condition
THEN NEW.copyofid = NEW.id ;
END IF ;
RETURN NEW ;
END ; $$
CREATE OR REPLACE TRIGGER before_insert_mytable BEFORE INSERT ON mytable
FOR EACH ROW EXECUTE FUNCTION before_insert_mytable () ;
The condition can also be stated directly in the WHEN clause of the trigger instead of in the function :
CREATE OR REPLACE FUNCTION before_insert_mytable() RETURN trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.copyofid = NEW.id ;
RETURN NEW ;
END ; $$
CREATE OR REPLACE TRIGGER before_insert_mytable BEFORE INSERT ON mytable
WHEN condition
FOR EACH ROW EXECUTE FUNCTION before_insert_mytable () ;
see the manual

Postgresql unique constraint for colums of multiple related tables

I have the following tables:
server (server_id as PK)
server_id | server_name
----------+---------------
3251623 | ServerOfDoom
5578921 | BestServerEU
server_groups
Each server can have multiple server groups, every group name within a server must be unique.
id | server_id | group_name
----+-----------+---------------
1 | 3251623 | mods
2 | 5578921 | admins
3 | 5578921 | anothergroup
group_tags
Each group can have multiple tags, a tag can be connected to multiple groups. Each tag name must be unique.
id | tag_name
----+--------------
1 | mods
2 | admins
3 | ag
group_tag_relations
group_id | tag_id
----------+--------
2 | 1
3 | 3
1 | 2
Problem:
I need group tag names and server group names to be unique within one server. So a tag can be connected to multiple groups, but only to one group per server. Or the tag "foo" cannot be connected to the group "bar" of server z because server z already has a group "foo". The same goes for the other way around, so if the group "foo" had the tag "hey" you shouldn't be able to add a group "hey" to the same server.
Basically names should not appear multiple times on a server, no matter if tag or group name.
How would I implement such a Constraint?
EDIT
So basically I'm trying to convert this JSON format into SQL tables:
{
"5578921": {
"Server Name": "Server1",
...
"Groups": {
"Game1": {
"Tags": [
"g1",
"gameone"
], ...
},
"Game2": {
"Tags": [
"g2",
"gametwo"
], ...
}
}
},
"3251623": ...
}
The id's 5578921 and 3251623 should just represent Discord server id's, so every id is for one server my bot is on. The Json file is all the information of my bot for each server, but it's not as reliable and scalable as a database, so I wanted to convert it.
So the upper tables are what I came up with:
A One-To-Many relation between server and server_groups and a Many-To-Many relation between server_groups and group_tags (so instead of storing duplicate tags I can just assign them to different groups).
I just want to make sure there are no duplicate names on one server and asking how to do so for my current tables.
As mentioned before, a stored procedure or function with triggers is probably the way to go.
The code could look something like this:
CREATE FUNCTION public."CHECK_TAG_AND_GROUP_NAME_UNIQUE_PER_SERVER"()
RETURNS trigger
LANGUAGE 'plpgsql'
NOT LEAKPROOF
AS $BODY$
DECLARE
countServerGroupsWithNamePerServer integer;
countGroupTagsWithNamePerServer integer;
BEGIN
-- Count occurrences of name in server_groups table
SELECT COUNT(*)
FROM server_groups
INTO countServerGroupsWithNamePerServer
WHERE "name" = NEW.name
AND "server_id" = NEW.server_id;
-- Check if one exists. If it does, throw error
IF countServerGroupsWithNamePerServer > 0 THEN
RAISE 'Name already exists as a group server name %', NEW.name;
END IF;
-- Count occurrences of name in group_tags table
SELECT COUNT(*)
FROM group_tags
INTO countGroupTagsWithNamePerServer
WHERE "name" = NEW.name
AND "server_id" = NEW.server_id;
-- Check if one exists. If it does, throw error
IF countGroupTagsWithNamePerServer > 0 THEN
RAISE 'Name already exists as a group_tag name %', NEW.name;
END IF;
-- If no error is thrown, insert the new record
RETURN NEW;
END;
$BODY$;
and then you attach the function as a BEFORE INSERT trigger to each of the two tables group_tags and server_groups:
CREATE TRIGGER "BEFORE_INSERT_CHECK_TAG_NAME_UNIQUE_PER_SERVER"
BEFORE INSERT
ON public.group_tags
FOR EACH ROW
EXECUTE PROCEDURE public."CHECK_TAG_AND_GROUP_NAME_UNIQUE_PER_SERVER"();
CREATE TRIGGER "BEFORE_INSERT_CHECK_TAG_NAME_UNIQUE_PER_SERVER"
BEFORE INSERT
ON public.server_groups
FOR EACH ROW
EXECUTE PROCEDURE public."CHECK_TAG_AND_GROUP_NAME_UNIQUE_PER_SERVER"();
Please notice for this example I also added a foreign key column server_id to the group_tags table. Otherwise we are not sure to which server the tag belongs. This is just a rough guide though, please feel free to change it up as much as you want.
After hours of suffering I finally got what I wanted:
Get all Tags of a server
CREATE OR REPLACE FUNCTION get_server_tags(serverid BIGINT)
RETURNS TABLE(group_name VARCHAR(100), tag_name VARCHAR(100), group_id BIGINT, tag_id BIGINT)
AS
$$
SELECT group_name, tag_name, group_id, tag_id FROM group_tag_relations
JOIN server_groups
ON server_groups.server_id = serverid
AND server_groups.id = group_tag_relations.group_id
JOIN group_tags
ON group_tags.id = group_tag_relations.tag_id
$$
language sql
stable;
Get all Groups of a server
CREATE OR REPLACE FUNCTION get_server_group(serverid BIGINT, groupname VARCHAR(100))
RETURNS TABLE(group_name VARCHAR(100), group_id BIGINT)
AS
$$
SELECT group_name, id
FROM server_groups
WHERE server_id = serverid
AND lower(group_name) = lower(groupname);
$$
language sql
stable;
Get a Group by Name (Calling both Functions above)
CREATE OR REPLACE FUNCTION get_group_by_name(serverid BIGINT, groupname VARCHAR(100))
RETURNS TABLE(group_name VARCHAR(100), group_id BIGINT)
AS
$$
BEGIN
RETURN QUERY SELECT get_server_group.group_name, get_server_group.group_id
FROM get_server_group(serverid, groupname);
IF NOT found THEN
RETURN QUERY SELECT get_server_tags.group_name, get_server_tags.group_id
FROM get_server_tags(serverid)
WHERE lower(tag_name) = lower(groupname);
END IF;
END;
$$
language plpgsql
stable;
Update Trigger for server_groups table, checking wether the name is already taken on a server
CREATE OR REPLACE FUNCTION group_name_update()
RETURNS TRIGGER
AS
$$
BEGIN
PERFORM get_group_by_name(OLD.server_id, NEW.group_name);
IF lower(OLD.group_name) = lower(NEW.group_name) THEN
RETURN NEW;
ELSIF found THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$
language plpgsql
volatile;
CREATE TRIGGER group_name_update_trigger
BEFORE UPDATE ON server_groups
FOR EACH ROW EXECUTE PROCEDURE group_name_update();
Insert Trigger for server_groups table, checking wether the name is already taken on a server
CREATE OR REPLACE FUNCTION group_name_insert()
RETURNS TRIGGER
AS
$$
BEGIN
PERFORM get_group_by_name(NEW.server_id, NEW.group_name);
IF found THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$
language plpgsql
volatile;
CREATE TRIGGER group_name_insert_trigger
BEFORE INSERT ON server_groups
FOR EACH ROW EXECUTE PROCEDURE group_name_insert();
Insert Trigger for group_tag_relation table, checking wether the tag is already taken on a server (as the tags wont get updated there's no need for an update trigger)
CREATE OR REPLACE FUNCTION group_tag_relation_insert()
RETURNS TRIGGER
AS
$$
BEGIN
PERFORM get_group_by_name((SELECT server_id FROM server_groups WHERE id = NEW.group_id), (SELECT tag_name FROM group_tags WHERE id = tag_id));
IF found THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$
language plpgsql
volatile;
CREATE TRIGGER group_tag_relation_insert_trigger
BEFORE INSERT ON group_tag_relations
FOR EACH ROW EXECUTE PROCEDURE group_tag_relation_insert();

How to return a custom record using return next

When I try to return a table, my results are showing up like this
generateClassRecord
--------------------
(james, 5)
(bob, 10)
rather than like this
+-------+----------+
| name | new_rank |
+-------+----------+
| james | 5 |
| bob | 10 |
| cole | 54 |
+-------+----------+
I am assuming I am not correctly returning the rows properly. Could someone advice me how I can return my data. I tried using return query but when I evoke my function it tells me the types do not match.
CREATE TABLE employees (
id integer primary key
name text,
rank integer
);
create type empRecord as (
name text,
new_rank integer
);
INSERT INTO employees (id, name, rank) VALUES (1, 'james', 5), (2, 'bob', 10), (3, 'Cole', '54');
CREATE OR REPLACE FUNCTION
generateEmpRecord()
RETURNS empRecord
as $$
DECLARE
r empRecord;
BEGIN
FOR r IN
SELECT name, rank
FROM employees;
LOOP
return next r;
END LOOP;
END;
$$ language plpgsql
Your function should be declared as returns setof emprecord as you are returning multiple rows.
But the whole example can be simplified by using returns table - then you don't need the extra "return type". You also don't need PL/pgSQL for this. A (usually more efficient) language sql function is enough:
CREATE OR REPLACE FUNCTION generateemprecord()
RETURNS table(name text, new_rank int)
as $$
SELECT name, rank
FROM employees;
$$
language sql;
Online example

How to return values from INSERT other that the row that was inserted

I have a large number of rows that i want to insert simultaneously into a PostgreSQL database. I need to track what id is assigned for each row that is inserted. For example say we have the table:
CREATE TABLE example
(
id serial,
name text,
CONSTRAINT example_pkey PRIMARY KEY (id),
);
Now i have some data with ids that i dont want inserted (as the serial id column will assign a new id), but i need to keep track of the mapping between the old id and new id:
old id | name
-------------
-1 | foo
-2 | bar
-3 | baz
So i wrote this query
WITH data(oldid,name) AS ( VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
INSERT INTO example(name)
SELECT name FROM data d
RETURNING id, d.oldid
Expecting to get something back like:
id | oldid
-----------
1 | -1
2 | -2
3 | -3
However this doesn't work, as i don't believe you can return a column that wasn't inserted. Is there any alternative way to do this?
I ended up creating a function that wrapped the inserting of a single row:
CREATE OR REPLACE FUNCTION add_example(
in_name text)
RETURNS integer AS
$BODY$
DECLARE
new_id integer;
BEGIN
INSERT INTO example(name)
VALUES (in_name) RETURNING id INTO new_id;
RETURN new_id;
END;
$BODY$
LANGUAGE plpgsql;
Then i can do:
WITH data(oldid, name) AS (VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
SELECT oldid, add_example(name) AS id
FROM data
Which returns what i expect. I'd like to see if this can be done without the function though.
CREATE SEQUENCE data_id_seq;
CREATE TABLE DATA (
id integer default nextval('data_id_seq') NOT NULL PRIMARY KEY,
oldid integer,
name text,
);
INSERT INTO DATA(oldid,name) values (-1,'foo'),(-2,'bar'),(-3,'baz') returning id,oldid;
The optional RETURNING clause causes INSERT to compute and return
value(s) based on each row actually inserted
from https://www.postgresql.org/docs/current/static/sql-insert.html
so column parasite is unavoidable for such solution:
alter table example add column old bigint;
WITH d(oldid,name) AS ( VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
INSERT INTO example(name,old)
SELECT "name", oldid FROM d
RETURNING id, old

Trigger to update current date in Postgres 9

I have two tables called sale and customer. I want to create a trigger that updates the column last_purchase on customer table on each new insert in the sale table.
Table customer: customer_id, name, last_sale, ...
Table sale: sale_id, customer_id, date, ...
CREATE TRIGGER update_last_sale BEFORE INSERT ON sale FOR EACH ROW EXECUTE...
I have started writing but I don't know how to do it.
Could someone help me?
CREATE FUNCTION update_customer_last_sale() RETURNS TRIGGER AS $$
BEGIN
UPDATE customer SET last_sale=now() WHERE cutomer_id=NEW.customer_id;
RETURN NEW;
END; $$
LANGUAGE plpgsql;
then
CREATE TRIGGER update_last_sale
BEFORE INSERT ON sale
FOR EACH ROW EXECUTE update_customer_last_sale;
NEW is the row which is about to be inserted in the sale table. (For an update row, it would be NEW for how the row will look after the update, and OLD for how the row looks before the update).
Basically, I don't think it is a good idea to store redundant data. The last_sale column in customers is just an aggregate of max(sales.sale_date).
It even gets worse if we use now() to touch customers.last_date. What would happen if we would need to re-insert some historical records (eg to recompute last year's taxes). That's what you get when you store redundant data....
-- modelled after Erwin's version
SET search_path='tmp';
-- DROP TABLE customers CASCADE;
CREATE TABLE customers
( id INTEGER NOT NULL PRIMARY KEY
, name VARCHAR
, last_sale DATE
);
-- DROP TABLE sales CASCADE;
CREATE TABLE sales
( id INTEGER NOT NULL PRIMARY KEY
, customer_id INTEGER REFERENCES customers(id)
, saledate DATE NOT NULL
);
CREATE OR REPLACE FUNCTION update_customer_last_sale() RETURNS TRIGGER AS $meat$
BEGIN
UPDATE customers cu
-- SET last_sale = now() WHERE id=NEW.customer_id
SET last_sale = (
SELECT MAX(saledate) FROM sales sa
WHERE sa.customer_id=cu.id
)
WHERE cu.id=NEW.customer_id
;
RETURN NEW;
END; $meat$
LANGUAGE plpgsql;
CREATE TRIGGER update_last_sale
AFTER INSERT ON sales
FOR EACH ROW
EXECUTE PROCEDURE update_customer_last_sale();
INSERT INTO customers(id,name,last_sale) VALUES(1, 'Dick', NULL),(2, 'Sue', NULL),(3, 'Bill', NULL);
INSERT INTO sales(id,customer_id,saledate) VALUES (1,1,'1900-01-01'),(2,1,'1950-01-01'),(3,2,'2011-12-15');
SELECT * FROM customers;
SELECT * FROM sales;
The results:
id | name | last_sale
----+------+------------
3 | Bill |
1 | Dick | 1950-01-01
2 | Sue | 2011-12-15
(3 rows)
id | customer_id | saledate
----+-------------+------------
1 | 1 | 1900-01-01
2 | 1 | 1950-01-01
3 | 2 | 2011-12-15
(3 rows)
I think you want the rule here.
CREATE RULE therule AS ON INSERT TO sale DO ALSO
(UPDATE customer SET customer.last_sale = now()
WHERE customer.customer_id=NEW.customer_id);
EDIT: but see the discussion in comments.