Empty Updates, ON UPDATE triggers and Rules

Started by Josh Trutwinover 16 years ago11 messagesgeneral
Jump to latest
#1Josh Trutwin
josh@trutwins.homeip.net

Hello,

I have a simple table that has a trigger to set a last_modified column
using the following:

CREATE OR REPLACE FUNCTION set_last_modified ()
RETURNS TRIGGER
AS $$
BEGIN
NEW.last_modified = NOW();
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;

CREATE TRIGGER trigger_test_upd_set_last_mod
BEFORE UPDATE ON test_upd
FOR EACH ROW EXECUTE PROCEDURE set_last_modified();

The table data:

select * from test_upd;

id | foo | bar | last_modified
----+-----+-----+----------------------------
1 | foo | 1 | 2009-08-06 11:37:09.15584
2 | foo | 2 | 2009-08-06 11:37:12.740515
3 | baz | 3 | 2009-08-06 11:37:19.730894

If I run the following query:

UPDATE test_up SET foo = 'foo', bar = 1 WHERE id = 1;

The set_last_modified() trigger is run even though the data didn't
actually change. Perhaps due to an application program which doesn't
know the contents before running the UPDATE.

New Data (notice last_modified changed for row 1):

select * from test_upd;

id | foo | bar | last_modified
----+-----+-----+----------------------------
2 | foo | 2 | 2009-08-06 11:37:12.740515
3 | baz | 3 | 2009-08-06 11:37:19.730894
1 | foo | 1 | 2009-08-06 11:37:43.045065

Doing some research on this I found this post:
http://www.depesz.com/index.php/2007/09/08/avoiding-empty-updates/

Which has a Rule:

CREATE RULE no_unchanging_updates AS
ON UPDATE TO test_upd
WHERE ROW(OLD.*) IS NOT DISTINCT FROM ROW(NEW.*)
DO INSTEAD NOTHING;

This worked great - re-ran the update query and no change to
last_modified column for row id 1. BUT, one major issue with this -
if I inspect the table with \d it appears the rule above was expanded
to this:

Rules:
no_unchanging_updates AS
ON UPDATE TO test_upd
WHERE NOT (old.id IS DISTINCT FROM new.id OR old.foo IS DISTINCT
FROM new.foo OR old.bar IS DISTINCT FROM new.bar OR
old.last_modified IS DISTINCT FROM new.last_modified) DO INSTEAD
NOTHING

Now if I add a column using:

ALTER TABLE test_upd ADD COLUMN baz TEXT;

The rule above is not updated to include the new column and running
an empty update query involving baz causes the trigger to change
last_modified.

Do I have to DROP/recreate the Rule everytime I ALTER the table or is
there a better way?

I have an application where it's possible for end users to easily
add / remove columns from their "plugin" application so I was hoping
to not have to add rule rebuilding to these operations if possible.
I noticed if I attempt to DROP column bar that I have to add CASCADE
so the rule is deleted so I'll likely have to deal with it anyway.

Postgresql 8.3.7

Thank you,

Josh

#2Merlin Moncure
mmoncure@gmail.com
In reply to: Josh Trutwin (#1)
Re: Empty Updates, ON UPDATE triggers and Rules

On Thu, Aug 6, 2009 at 12:53 PM, Josh Trutwin<josh@trutwins.homeip.net> wrote:

Hello,

I have a simple table that has a trigger to set a last_modified column
using the following:

CREATE OR REPLACE FUNCTION set_last_modified ()
RETURNS TRIGGER
AS $$
  BEGIN
     NEW.last_modified = NOW();
     RETURN NEW;
  END;
$$ LANGUAGE PLPGSQL;

CREATE TRIGGER trigger_test_upd_set_last_mod
BEFORE UPDATE ON test_upd
FOR EACH ROW EXECUTE PROCEDURE set_last_modified();

The table data:

select * from test_upd;

 id | foo | bar |       last_modified
----+-----+-----+----------------------------
 1 | foo |   1 | 2009-08-06 11:37:09.15584
 2 | foo |   2 | 2009-08-06 11:37:12.740515
 3 | baz |   3 | 2009-08-06 11:37:19.730894

If I run the following query:

UPDATE test_up SET foo = 'foo', bar = 1 WHERE id = 1;

The set_last_modified() trigger is run even though the data didn't
actually change.  Perhaps due to an application program which doesn't
know the contents before running the UPDATE.

Triggers are supposed to fire regardless if new == old. In fact it's
common practice to do something like:
update foo set x = x; to get trigger to fire.

CREATE RULE no_unchanging_updates AS
 ON UPDATE TO test_upd
 WHERE ROW(OLD.*) IS NOT DISTINCT FROM ROW(NEW.*)
 DO INSTEAD NOTHING;

in 8.3 you can also do:
WHERE old::text = new.text

in 8.4 you can (and should) do:
WHERE old = new

This worked great - re-ran the update query and no change to
last_modified column for row id 1.  BUT, one major issue with this -
if I inspect the table with \d it appears the rule above was expanded
to this:

Rules:
   no_unchanging_updates AS
   ON UPDATE TO test_upd
  WHERE NOT (old.id IS DISTINCT FROM new.id OR old.foo IS DISTINCT
  FROM new.foo OR old.bar IS DISTINCT FROM new.bar OR
  old.last_modified IS DISTINCT FROM new.last_modified) DO INSTEAD
  NOTHING

'*' is expanded during the creation of the rule. There's nothing you
can do about this for rules, however for functions '*' is preserved
because the function is recompiled from source when necessary. So,
from this we conclude:

*) '*' is dangerous except in functions
*) use functions instead of rules where possible

how about:

CREATE OR REPLACE FUNCTION set_last_modified ()
RETURNS TRIGGER
AS $$
BEGIN
IF NEW != OLD THEN -- 8.4 syntax
NEW.last_modified = NOW();
END IF;

RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;

merlin

#3Jeff Davis
pgsql@j-davis.com
In reply to: Josh Trutwin (#1)
Re: Empty Updates, ON UPDATE triggers and Rules

On Thu, 2009-08-06 at 11:53 -0500, Josh Trutwin wrote:

The set_last_modified() trigger is run even though the data didn't
actually change. Perhaps due to an application program which doesn't
know the contents before running the UPDATE.

The following doc explains the standard way to accomplish this:

http://www.postgresql.org/docs/8.4/static/functions-trigger.html

The document says that in most cases, you would want the above trigger
to fire last. However, I think your situation is different: you probably
want that trigger to fire before your "last updated" trigger.

Rules happen at a much earlier stage. Expressions haven't been evaluated
yet and triggers haven't been fired, etc., so the rule won't really know
whether the new row and old row are really equal or not. A rule will
only work in simple cases, which may or may not be acceptable for you.

Regards,
Jeff Davis

#4Jeff Davis
pgsql@j-davis.com
In reply to: Merlin Moncure (#2)
Re: Empty Updates, ON UPDATE triggers and Rules

On Thu, 2009-08-06 at 13:15 -0400, Merlin Moncure wrote:

in 8.4 you can (and should) do:
WHERE old = new

I couldn't get that to work in a rule.

IF NEW != OLD THEN -- 8.4 syntax

Does this work correctly in the case of NULLs? It looks like it does,
but that seems strange, because ROW(1, NULL) = ROW(1, NULL) evaluates to
NULL. Where is this documented?

Regards,
Jeff Davis

#5Pavel Stehule
pavel.stehule@gmail.com
In reply to: Jeff Davis (#4)
Re: Empty Updates, ON UPDATE triggers and Rules

2009/8/6 Jeff Davis <pgsql@j-davis.com>:

On Thu, 2009-08-06 at 13:15 -0400, Merlin Moncure wrote:

in 8.4 you can (and should) do:
WHERE old = new

I couldn't get that to work in a rule.

  IF NEW != OLD THEN  -- 8.4 syntax

Does this work correctly in the case of NULLs? It looks like it does,
but that seems strange, because ROW(1, NULL) = ROW(1, NULL) evaluates to
NULL. Where is this documented?

it's not safe, I thing so correct test is

IF NEW IS NOT DISTINCT FROM OLD THEN ...

regards
Pavel Stehule

Show quoted text

Regards,
       Jeff Davis

--
Sent via pgsql-general mailing list (pgsql-general@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-general

#6Merlin Moncure
mmoncure@gmail.com
In reply to: Jeff Davis (#4)
Re: Empty Updates, ON UPDATE triggers and Rules

On Thu, Aug 6, 2009 at 1:38 PM, Jeff Davis<pgsql@j-davis.com> wrote:

On Thu, 2009-08-06 at 13:15 -0400, Merlin Moncure wrote:

in 8.4 you can (and should) do:
WHERE old = new

I couldn't get that to work in a rule.

it should, maybe try old::foo = new::foo

  IF NEW != OLD THEN  -- 8.4 syntax

Does this work correctly in the case of NULLs? It looks like it does,
but that seems strange, because ROW(1, NULL) = ROW(1, NULL) evaluates to
NULL. Where is this documented?

If you think that's weird, check out:

postgres=# select (50, 0)::foo > (50, null)::foo;
?column?
----------
f
(1 row)

postgres=# select (50, 0)::foo < (50, null)::foo;
?column?
----------
t
(1 row)

I think maybe Pavel is right and is distinct from is safer, but I'd
argue against any change that disallowed comparisons of composites
with nulls in them.

merlin

#7Josh Trutwin
josh@trutwins.homeip.net
In reply to: Merlin Moncure (#2)
Re: Empty Updates, ON UPDATE triggers and Rules

On Thu, 6 Aug 2009 13:15:57 -0400
Merlin Moncure <mmoncure@gmail.com> wrote:

CREATE OR REPLACE FUNCTION set_last_modified ()
RETURNS TRIGGER
AS $$
BEGIN
IF NEW != OLD THEN -- 8.4 syntax
NEW.last_modified = NOW();
END IF;

RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;

Thanks - I'll try this. Since using 8.3 sounds like I need to
replace above with:

IF old::text != new::text

?

I'll give it a go anyway....

Josh

#8Josh Trutwin
josh@trutwins.homeip.net
In reply to: Merlin Moncure (#2)
Re: Empty Updates, ON UPDATE triggers and Rules

On Thu, 6 Aug 2009 13:15:57 -0400
Merlin Moncure <mmoncure@gmail.com> wrote:

CREATE OR REPLACE FUNCTION set_last_modified ()
RETURNS TRIGGER
AS $$
BEGIN
IF NEW != OLD THEN -- 8.4 syntax
NEW.last_modified = NOW();
END IF;

RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;

Interestingly, this syntax is accepted in 8.3.7, but SELECT queries
fail:

CREATE TRIGGER trigger_test_upd_set_last_mod
BEFORE UPDATE ON test_upd
FOR EACH ROW EXECUTE PROCEDURE set_last_modified();

Then:

UPDATE test_upd SET foo = 'foo' WHERE id = 1;
ERROR: operator does not exist: test_upd <> test_upd
LINE 1: SELECT $1 != $2
^
HINT: No operator matches the given name and argument type(s). You
might need to add explicit type casts. QUERY: SELECT $1 != $2
CONTEXT: PL/pgSQL function "set_last_modified_test" line 2 at IF

This seems to be working fine on 8.3 though:

IF old::text != new::text THEN

Are there any solutions pre 8.3? We still have some 8.1 installs....

Thanks!

Josh

#9Michael Glaesemann
grzm@seespotcode.net
In reply to: Josh Trutwin (#8)
Re: Empty Updates, ON UPDATE triggers and Rules

On Aug 6, 2009, at 15:31 , Josh Trutwin wrote:

Interestingly, this syntax is accepted in 8.3.7, but SELECT queries
fail:

CREATE TRIGGER trigger_test_upd_set_last_mod
BEFORE UPDATE ON test_upd
FOR EACH ROW EXECUTE PROCEDURE set_last_modified();

Then:

UPDATE test_upd SET foo = 'foo' WHERE id = 1;
ERROR: operator does not exist: test_upd <> test_upd
LINE 1: SELECT $1 != $2
^
HINT: No operator matches the given name and argument type(s). You
might need to add explicit type casts. QUERY: SELECT $1 != $2
CONTEXT: PL/pgSQL function "set_last_modified_test" line 2 at IF

That's not a SELECT query per se: AIUI it's how the evaluation of the
NEW != OLD expression is evaluated within the PL/pgSQL function as
part of the IF statement (note the "line 2 at IF" context line). It's
just saying the <> operator doesn't exist for the test_upd rowtype.

Michael Glaesemann
grzm seespotcode net

#10Merlin Moncure
mmoncure@gmail.com
In reply to: Josh Trutwin (#8)
Re: Empty Updates, ON UPDATE triggers and Rules

On Thu, Aug 6, 2009 at 3:31 PM, Josh Trutwin<josh@trutwins.homeip.net> wrote:

On Thu, 6 Aug 2009 13:15:57 -0400
Merlin Moncure <mmoncure@gmail.com> wrote:

CREATE OR REPLACE FUNCTION set_last_modified ()
RETURNS TRIGGER
AS $$
BEGIN
  IF NEW != OLD THEN  -- 8.4 syntax
    NEW.last_modified = NOW();
  END IF;

  RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;

Interestingly, this syntax is accepted in 8.3.7, but SELECT queries
fail:

CREATE TRIGGER trigger_test_upd_set_last_mod
BEFORE UPDATE ON test_upd
FOR EACH ROW EXECUTE PROCEDURE set_last_modified();

Then:

UPDATE test_upd SET foo = 'foo' WHERE id = 1;
ERROR:  operator does not exist: test_upd <> test_upd
LINE 1: SELECT   $1  !=  $2
                    ^
HINT:  No operator matches the given name and argument type(s). You
might need to add explicit type casts. QUERY:  SELECT   $1  !=  $2
CONTEXT:  PL/pgSQL function "set_last_modified_test" line 2 at IF

This seems to be working fine on 8.3 though:

  IF old::text != new::text THEN

Are there any solutions pre 8.3?  We still have some 8.1 installs....

yes, there is a similar, more circuitous way, that should work for 8.1
IIRC you have to calll record_out to get the text for the record (the
cast is just shorthand for that).

merlin

#11Josh Trutwin
josh@trutwins.homeip.net
In reply to: Michael Glaesemann (#9)
Re: Empty Updates, ON UPDATE triggers and Rules

On Thu, 6 Aug 2009 16:58:02 -0400
Michael Glaesemann <grzm@seespotcode.net> wrote:

That's not a SELECT query per se: AIUI it's how the evaluation of
the NEW != OLD expression is evaluated within the PL/pgSQL function
as part of the IF statement (note the "line 2 at IF" context line).
It's just saying the <> operator doesn't exist for the test_upd
rowtype.

Oops - that was a typo - meant to say UPDATE.

Thx,

Josh