Re: BUG #5042: Update numeric within a rule

From: Robert Haas <robertmhaas(at)gmail(dot)com>
To: Ilian Georgiev <georgiev(dot)ilian(at)gmail(dot)com>
Cc: pgsql-bugs(at)postgresql(dot)org
Subject: Re: BUG #5042: Update numeric within a rule
Date: 2009-09-16 01:11:27
Message-ID: 603c8f070909151811l2563de31jba18bd814faeb1f4@mail.gmail.com
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

On Sun, Sep 6, 2009 at 4:48 PM, Ilian Georgiev <georgiev(dot)ilian(at)gmail(dot)com> wrote:
>
> The following bug has been logged online:
>
> Bug reference:      5042
> Logged by:          Ilian Georgiev
> Email address:      georgiev(dot)ilian(at)gmail(dot)com
> PostgreSQL version: 8.1.10
> Operating system:   Windows XP
> Description:        Update numeric within a rule
> Details:
>
> Hello I have a sutuation where I can do update on numeric column with a
> signle update but when this update statement is in a rule it doesn't wotk
> properly.
> Look :
>
> I have a table with videos :
>
> CREATE TABLE video (
>  video_sid SERIAL PRIMARY KEY,
>  category_sid int NOT NULL REFERENCES category ON UPDATE RESTRICT ON DELETE
> RESTRICT,
>  url varchar(32) NOT NULL,
>  user_sid int NOT NULL REFERENCES "user" ON UPDATE RESTRICT ON DELETE
> RESTRICT,
>        image_sid int REFERENCES image,
>        creation_datetime timestamp NOT NULL DEFAULT NOW(),
>        view_count int NOT NULL DEFAULT 0,
>        comment_count int NOT NULL DEFAULT 0,
>        rating numeric(4,2) NOT NULL DEFAULT 0,
>        rating_percent int NOT NULL DEFAULT 0,
>        votes int NOT NULL DEFAULT 0,
>        is_published boolean NOT NULL DEFAULT false,
>        title varchar(128) NOT NULL,
>        description text
> );
>
> GRANT INSERT, UPDATE, SELECT ON video TO web;
> GRANT SELECT, UPDATE ON video_video_sid_seq TO web;
>
> COMMENT ON TABLE video IS 'Holds video desctiptions';
>
> CREATE OR REPLACE FUNCTION update_rating_percent()
>  RETURNS "trigger" AS
> $BODY$
>        BEGIN
>
>                IF (NEW.rating!=0) THEN
>                        NEW.rating_percent := ((NEW.rating / 5 ) * 100)::int;
>                END IF;
>
>                RETURN NEW;
>        END;
> $BODY$
>  LANGUAGE 'plpgsql' VOLATILE;
>
> GRANT EXECUTE ON FUNCTION update_rating_percent() TO web;
>
> CREATE TRIGGER update_rating_percent_trg
>  BEFORE UPDATE
>  ON video
>  FOR EACH ROW
>  EXECUTE PROCEDURE update_rating_percent();
>
> and table with votes :
>
> CREATE TABLE video_vote (
>  video_sid int NOT NULL REFERENCES video ON UPDATE CASCADE ON DELETE
> CASCADE,
>        ip_address inet NOT NULL,
>        rate int NOT NULL CHECK (rate > 0 AND rate < 6),
>        creation_datetime timestamp NOT NULL DEFAULT NOW()
> );
>
> GRANT INSERT, UPDATE, DELETE, SELECT ON video_vote TO web;
>
> COMMENT ON TABLE video_vote IS 'Votes of every user by IP address';
>
> CREATE UNIQUE INDEX video_vote_ukey ON video_vote (video_sid, ip_address);
>
> with a rule connected to the video table :
>
> CREATE OR REPLACE RULE
>        video_vote_insert_rule
> AS ON INSERT TO
>        video_vote
> DO ALSO
>        UPDATE
>                video
>        SET
>                votes = votes + 1,
>                rating = (( SELECT
>                                                                        SUM(rate)::numeric
>                                                         FROM
>                                                                        video_vote
>                                                         WHERE
>                                                                        video_sid = NEW.video_sid
>                                                        ) / (votes + 1) )::numeric
>        WHERE
>                video_sid = NEW.video_sid;
>
> now when I do simple update on video it gets the right value for scale.But
> when I do insert on video_vote and this do update on video table I got .00
> for scale.

I think what is happening here is that you are dividing two integers
(rate is an integer, therefore sum(rate) is an integer, and votes is
an integer, therefore votes + 1 is an integer), so you're getting an
integer result. You then cast that result to a numeric, but by that
point you've already thrown away the remainder. If you divide by
votes::numeric + 1 instead of votes + 1 you'll probably get a
different answer.

> I even changed my rule to :
>
> CREATE OR REPLACE RULE
>        video_vote_insert_rule
> AS ON INSERT TO
>        video_vote
> DO ALSO
>        UPDATE
>                video
>        SET
>
>                rating = 2.95
>        WHERE
>                video_sid = NEW.video_sid;
>
> The result in rating column was 2.00 .

I find this just about impossible to believe. I just tried it with a
self-contained test case and it worked fine (see below). It's pretty
hard to believe that there could be a bug that makes numeric division
truncate to the nearest integer, but only when used from within a
rule. I think it's more likely that you made a mistake somewhere in
the process of carrying out this experiment.

rhaas=# create table foo (id serial primary key, rating numeric(4,2)
not null default 0);
NOTICE: CREATE TABLE will create implicit sequence "foo_id_seq" for
serial column "foo.id"
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index
"foo_pkey" for table "foo"
CREATE TABLE
rhaas=# create table bar (id serial primary key);
NOTICE: CREATE TABLE will create implicit sequence "bar_id_seq" for
serial column "bar.id"
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index
"bar_pkey" for table "bar"
CREATE TABLE
rhaas=# create rule bar_rule as on insert to bar do update foo set
rating = 2.95;
CREATE RULE
rhaas=# insert into foo values (default, '3.67');
INSERT 0 1
rhaas=# insert into bar values (default);
INSERT 0 1
rhaas=# select * from foo;
id | rating
----+--------
1 | 2.95
(1 row)

> When I do :
>
> update
>  video
> set
>  rating = 2.95
> where
>  video_sid = 1;
>
> Then the result is expected = 2.95

I'm not at all surprised by this result. :-)

By the by, I think that the way you are implementing this is not
MVCC-safe. You probably should store the sum of the ratings and the
count of votes in the video table, and do the division when you select
from that table. I think the way you have it there might be a
possibility of the wrong average rating being stored in the face of
concurrent inserts to video_vote.

...Robert

In response to

Browse pgsql-bugs by date

  From Date Subject
Next Message Tom Lane 2009-09-16 01:50:45 Re: BUG #5055: Invalid page header error
Previous Message Robert Haas 2009-09-16 00:54:41 Re: BUG #5058: [jdbc] Silent failure with executeUpdate()