PostgreSQL use NEW in query for INSTEAD OF trigger
NEW
is a record, not a table. Basics:
- Use NEW in FROM clause in Postgres trigger?
Slightly modified setup
CREATE TABLE product (
product_id serial PRIMARY KEY,
product_name text UNIQUE NOT NULL -- must be UNIQUE
);
CREATE TABLE purchase (
purchase_id serial PRIMARY KEY,
product_id int REFERENCES product,
when_bought date
);
CREATE VIEW purchaseview AS
SELECT pu.purchase_id, pr.product_name, pu.when_bought
FROM purchase pu
LEFT JOIN product pr USING (product_id);
INSERT INTO product(product_name) VALUES ('foo');
product_name
has to be UNIQUE
, or the lookup on this column could find multiple rows, which would lead to all kinds of confusion.
1. Simple solution
For your simple example, only looking up the single column product_id
, a lowly correlated subquery is simplest and fastest:
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO purchase(product_id, when_bought)
SELECT (SELECT product_id FROM product WHERE product_name = NEW.product_name), NEW.when_bought
RETURNING purchase_id
INTO NEW.purchase_id; -- generated serial ID for RETURNING - if needed
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insert_productview_trig
INSTEAD OF INSERT ON purchaseview
FOR EACH ROW EXECUTE PROCEDURE insert_purchaseview_func();
No additional variables. No CTE (would only add cost and noise). Columns from NEW
are spelled out once only (your point 1).
The appended RETURNING purchase_id INTO NEW.purchase_id
takes care of your point 2: Now, the returned row includes the newly generated purchase_id
.
If the product is not found (NEW.product_name
does not exist in table product
), the purchase is still inserted and product_id
is NULL
. This may or may not be desirable.
2.
To skip the row instead (and possibly raise a WARNING
/ EXCEPTION
):
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO purchase AS pu
(product_id, when_bought)
SELECT pr.product_id, NEW.when_bought
FROM product pr
WHERE pr.product_name = NEW.product_name
RETURNING pu.purchase_id
INTO NEW.purchase_id; -- generated serial ID for RETURNING - if needed
IF NOT FOUND THEN -- insert was canceled for missing product
RAISE WARNING 'product_name % not found! Skipping INSERT.', quote_literal(NEW.product_name);
END IF;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
This piggybacks NEW
columns to SELECT .. FROM product
. If the product is found, everything proceeds normally. If not, no row is returned from the SELECT
and no INSERT
happens. The special PL/pgSQL variable FOUND
is only true if the last SQL query processed at least one row.
Could be EXCEPTION
instead of WARNING
to raise an error and roll back the transaction. But I'd rather declare purchase.product_id NOT NULL
and insert unconditionally (query 1 or similar), to the same effect: raises an exception if product_id
is NULL
. Simpler, cheaper.
3. For multiple lookups
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO purchase AS pu
(product_id, when_bought) -- more columns?
SELECT pr.product_id, i.when_bought -- more columns?
FROM (SELECT NEW.*) i -- see below
LEFT JOIN product pr USING (product_name)
-- LEFT JOIN tbl2 t2 USING (t2_name) -- more lookups?
RETURNING pu.purchase_id -- more columns?
INTO NEW.purchase_id; -- more columns?
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
The LEFT JOIN
s make the INSERT
unconditional again. Use JOIN
instead to skip if one is not found.
FROM (SELECT NEW.*) i
transforms the record NEW
into a derived table with a single row, which can be used like any table in the FROM
clause - what you were looking for, initially.
db<>fiddle here
As suggested in the comments, it looks like the closest I can do to what I want is (fixing up my original approach in the question):
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$BODY$
DECLARE
tmp RECORD;
BEGIN
WITH input (product_name, when_bought) as (
values (NEW.product_name, NEW.when_bought)
)
INSERT INTO Purchase(product_id, when_bought)
SELECT product_id, when_bought
FROM input
LEFT JOIN Product USING (product_name)
RETURNING purchase_id INTO tmp;
NEW.purchase_id = tmp.purchase_id;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
This does at least make the RETURNING
clause work correctly. It looks like the attributes of NEW
must be explicitly declared. The following:
-- Using NEW.* in CTE doesn't work
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
RETURNS trigger AS
$BODY$
DECLARE
tmp RECORD;
BEGIN
WITH input as (
values (NEW.*)
)
INSERT INTO Purchase(product_id, when_bought)
SELECT product_id, when_bought
FROM input
LEFT JOIN Product USING (product_name)
RETURNING purchase_id INTO tmp;
NEW.purchase_id = tmp.purchase_id;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
results in ERROR: column "product_name" specified in USING clause does not exist in left table
when the trigger is fired.