HiveBrain v1.2.0
Get Started
← Back to all entries
patternsqlModerate

PostgreSQL use NEW in query for INSTEAD OF trigger

Submitted by: @import:stackexchange-dba··
0
Viewed 0 times
postgresqltriggernewqueryinsteadforuse

Problem

I'm having trouble getting an INSTEAD OF trigger to work correctly, and I think I've misunderstood how to use NEW. Consider the following simplified scenario:

CREATE TABLE Product (
  product_id SERIAL PRIMARY KEY,
  product_name VARCHAR
);
CREATE TABLE Purchase (
  purchase_id SERIAL PRIMARY KEY,
  product_id INT REFERENCES Product,
  when_bought DATE
);

CREATE VIEW PurchaseView AS
SELECT purchase_id, product_name, when_bought
FROM Purchase LEFT JOIN Product USING (product_id);


I'd like to be able to create INSTEAD OF triggers to allow me to insert directly into PurchaseView, e.g.:

INSERT INTO Product(product_name) VALUES ('foo');
INSERT INTO PurchaseView(product_name, when_bought) VALUES ('foo', NOW());


What I had in mind was something along the lines of:

CREATE OR REPLACE FUNCTION insert_purchaseview_func()
  RETURNS trigger AS
$BODY$
BEGIN
  INSERT INTO Purchase(product_id, when_bought)
  SELECT product_id, when_bought
  FROM NEW 
  LEFT JOIN Product USING (product_name)
  RETURNING * INTO NEW;
END;
$BODY$
LANGUAGE plpgsql;

CREATE TRIGGER insert_productview_trig
  INSTEAD OF INSERT
  ON PurchaseView
  FOR EACH ROW
  EXECUTE PROCEDURE insert_purchaseview_func();


However the above trigger function gives errors (relation "new" does not exist) when executed. I know I can write queries that explicitly use attributes from NEW in the WHERE and SELECT clauses, but sometimes it would be convenient to be able to include NEW in a join. Is there a way to do this?

Current (unsatisfactory) solution

The closest I can get to what I want is

```
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 * INTO tmp;
RETURN

Solution

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
  LANGUAGE plpgsql 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$;

CREATE TRIGGER insert_productview_trig
INSTEAD OF INSERT ON purchaseview
FOR EACH ROW EXECUTE FUNCTION 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.
  1. Skip row if product is not found



To skip the row instead (and possibly raise a WARNING / EXCEPTION):

CREATE OR REPLACE FUNCTION insert_purchaseview_func()
  RETURNS trigger
  LANGUAGE plpgsql 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$;


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.
  1. For multiple lookups



CREATE OR REPLACE FUNCTION insert_purchaseview_func()
  RETURNS trigger
  LANGUAGE plpgsql 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$;


The LEFT JOINs 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.

fiddle

Code Snippets

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');
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
  RETURNS trigger
  LANGUAGE plpgsql 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$;

CREATE TRIGGER insert_productview_trig
INSTEAD OF INSERT ON purchaseview
FOR EACH ROW EXECUTE FUNCTION insert_purchaseview_func();
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
  RETURNS trigger
  LANGUAGE plpgsql 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$;
CREATE OR REPLACE FUNCTION insert_purchaseview_func()
  RETURNS trigger
  LANGUAGE plpgsql 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$;

Context

StackExchange Database Administrators Q#144197, answer score: 14

Revisions (0)

No revisions yet.