Database Administrators Stack Exchange is a question and answer site for database professionals who wish to improve their database skills and learn from others in the community. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

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

This is a little unsatisfactory for several reasons:

  1. I need to explicitly write all the attributes of NEW in CTE WITH query, which for large views (especially those whose attributes are automatically determined with SELECT *) gets unwieldy;

  2. The returned result doesn't have the SERIAL type product_id updated, so you don't get the expected result for

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

share|improve this question
    
SELECT NEW.product_name and so on. Then you don't join, the FROM clause will have only the other table, the join condition goes into WHERE. – dezso Jul 18 '16 at 15:55
    
@dezso That's my point - my actual tables and views are substantially more complicated, so I'm looking to avoid explicitly referring to individual columns (e.g. with NATURAL JOIN). Also not sure how best to deal with LEFT JOINS as in the example I've given. – beldaz Jul 18 '16 at 20:14
1  
NEW is not a table.. It's a PurchaseView%ROWTYPE .. you can't perform table operations on it, but you CAN perform row oprations on it (which is what dezso is suggesting) – Joishi Bodio Jul 18 '16 at 20:31
1  
I cannot confirm now if it works, but you could try NEW.* – dezso Jul 18 '16 at 21:07
2  
@beldaz Yes, it sucks - but that's one of the downsides of using an INSTEAD OF trigger for a VIEW - especially if the VIEW is large. If your VIEW is defined as SELECT * FROM TABLE JOIN TABLE JOIN TABLE .. then you should probably redefine your VIEW so that it doesn't use SELECT *.. – Joishi Bodio Jul 18 '16 at 21:16
up vote 1 down vote accepted

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.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.