Thread: PL/pgSQL trigger and sequence increment

PL/pgSQL trigger and sequence increment

From
jonesd@xmission.com
Date:
Greetings.  I noticed an interesting behavior when using a PL/pgSQL
trigger.  I'm running PostgreSQL 8.3.  The trigger function checks a
newly inserted or updated row for a type of uniqueness.  Specifically,
each row in the table has a submitter id and an entry timestamp.  No
two rows can have the same submitter id and entry timestamp month
(basically, this means that there can be one entry per submitter per
month).  In other words, the trigger function is along the lines of:

BEGIN
  IF NOT EXISTS (SELECT * FROM table_entry WHERE submitter_id =
new.submitter_id AND date_trunc('month',entry_timestamp) =
date_trunc('month',new.entry_timestamp)) THEN RETURN new;
  ELSE RETURN NULL;
  END IF;
END

Each row in the table also has a SERIAL identifier with a sequence
providing values.  I'd like to provide information to the user
regarding why the INSERT or UPDATE failed, as the examples in the
documentation do via using a RAISE EXCEPTION instead of RETURN NULL
(see
http://www.postgresql.org/docs/8.3/interactive/plpgsql-trigger.html,
which appears to be unchanged in the documentation for 9.0).  However,
if I do so, the sequence increments after the attempted INSERT or
UPDATE, which is not desired (and does not happen if RETURN NULL is
the result of the trigger function).

Any assistance is appreciated - thanks in advance!

---

Dominic Jones, Ph.D.

Re: PL/pgSQL trigger and sequence increment

From
Tom Lane
Date:
jonesd@xmission.com writes:
> Greetings.  I noticed an interesting behavior when using a PL/pgSQL
> trigger.  I'm running PostgreSQL 8.3.  The trigger function checks a
> newly inserted or updated row for a type of uniqueness.  Specifically,
> each row in the table has a submitter id and an entry timestamp.  No
> two rows can have the same submitter id and entry timestamp month
> (basically, this means that there can be one entry per submitter per
> month).  In other words, the trigger function is along the lines of:

> BEGIN
>   IF NOT EXISTS (SELECT * FROM table_entry WHERE submitter_id =
> new.submitter_id AND date_trunc('month',entry_timestamp) =
> date_trunc('month',new.entry_timestamp)) THEN RETURN new;
>   ELSE RETURN NULL;
>   END IF;
> END

Seems like you would be a lot better off enforcing this with a unique
index on (submitter_id, date_trunc('month',entry_timestamp)).  The above
not only doesn't provide any feedback, it's got serious race-condition
problems.

> Each row in the table also has a SERIAL identifier with a sequence
> providing values.  I'd like to provide information to the user
> regarding why the INSERT or UPDATE failed, as the examples in the
> documentation do via using a RAISE EXCEPTION instead of RETURN NULL
> (see
> http://www.postgresql.org/docs/8.3/interactive/plpgsql-trigger.html,
> which appears to be unchanged in the documentation for 9.0).  However,
> if I do so, the sequence increments after the attempted INSERT or
> UPDATE, which is not desired (and does not happen if RETURN NULL is
> the result of the trigger function).

Really?  Frankly, I don't believe it.  Any default value will get filled
in long before triggers run.  In any case, you'd still have issues from
errors occurring later in the transaction.  In general, you *can not*
expect to not have "holes" in the serial number assignment when using a
sequence object.  You'll save yourself a lot of grief if you just accept
that fact, rather than imagining (falsely) that you've found a
workaround to avoid it.

If you really must have gap-free serial numbers, it's possible, but it's
slow, expensive, and doesn't rely on sequence objects.  You can find the
details in the list archives, but basically each insert has to lock the
table against other inserts and then examine it to find the max current
id.

            regards, tom lane

Re: PL/pgSQL trigger and sequence increment

From
jonesd@xmission.com
Date:
> Seems like you would be a lot better off enforcing this with a unique
> index on (submitter_id, date_trunc('month',entry_timestamp)).  The above
> not only doesn't provide any feedback, it's got serious race-condition
> problems.

I'll take a look at using an index to do this.  The trigger is an ugly
solution.

> > Each row in the table also has a SERIAL identifier with a sequence
>   > providing values.  I'd like to provide information to the user
> > regarding why the INSERT or UPDATE failed, as the examples in the
>  > documentation do via using a RAISE EXCEPTION instead of RETURN
> NULL   > (see   >
> http://www.postgresql.org/docs/8.3/interactive/plpgsql-trigger.html,
>  >  which appears to be unchanged in the documentation for 9.0).   >
> However,  if I do so, the sequence increments after the attempted  >
> INSERT or  UPDATE, which is not desired (and does not happen if  >
> RETURN NULL is  the result of the trigger function).

> Really?  Frankly, I don't believe it.  Any default value will get filled
> in long before triggers run.  In any case, you'd still have issues from
> errors occurring later in the transaction.  In general, you *can not*
> expect to not have "holes" in the serial number assignment when using a
> sequence object.  You'll save yourself a lot of grief if you just accept
> that fact, rather than imagining (falsely) that you've found a
> workaround to avoid it.

I double-checked it and got the same behavior each time I did it.
Poking around in the documentation makes me think that the key is when
the trigger fires.  The trigger in question is a BEFORE trigger, so
according to the docs if it returns NULL the INSERT never happens.
Thus, the sequence wouldn't increment - makes sense to me.  It appears
that, if you get an exception instead, the sequence does increment,
which is the part that doesn't make sense.

> If you really must have gap-free serial numbers, it's possible, but it's
> slow, expensive, and doesn't rely on sequence objects.  You can find the
> details in the list archives, but basically each insert has to lock the
> table against other inserts and then examine it to find the max current
> id.

Been there, done that, implemented a solution (which doesn't use
sequences).  I'm not using that solution here - just don't see why a
BEFORE trigger should be incrementing a sequence.


Dominic Jones, Ph.D.

Re: PL/pgSQL trigger and sequence increment

From
jonesd@xmission.com
Date:
> Seems like you would be a lot better off enforcing this with a unique
> index on (submitter_id, date_trunc('month',entry_timestamp)).  The above
> not only doesn't provide any feedback, it's got serious race-condition
> problems.

Unfortunately, it didn't work.

CREATE UNIQUE INDEX one_entry_per_submitter_per_month ON table_entry
(submitter_id , date_trunc('month',entry_timestamp));

runs into

ERROR:  functions in index expression must be marked IMMUTABLE.

If I'm reading this correctly, date_trunc is not IMMUTABLE and thus
not usable in an index.


Dominic Jones, Ph.D.

Re: PL/pgSQL trigger and sequence increment

From
Tom Lane
Date:
jonesd@xmission.com writes:
> CREATE UNIQUE INDEX one_entry_per_submitter_per_month ON table_entry
> (submitter_id , date_trunc('month',entry_timestamp));
> runs into
> ERROR:  functions in index expression must be marked IMMUTABLE.

> If I'm reading this correctly, date_trunc is not IMMUTABLE and thus
> not usable in an index.

It is not immutable because it depends on the timezone setting: the same
timestamptz might be truncated to different absolute time instants
depending on which zone you are in.  IOW, when is midnight of the first
of the month, exactly?

You could work around this with something like

date_trunc('month',entry_timestamp AT TIME ZONE 'UTC')

(feel free to substitute a different zone name reflecting what you want
to have happpen) but I wonder whether this doesn't reflect a gap in your
database specification.

            regards, tom lane