Thread: Proposal / proof of concept: Triggers on VIEWs

Proposal / proof of concept: Triggers on VIEWs

From
Dean Rasheed
Date:
I've been thinking about the idea of allowing triggers on views as a
way of implementing updateable views. This would not be a replacement
for rules in all situations, but rather a complementary feature.
It would also complement the SQL standard feature allowing updates to
simple views, which is fine except that in practice I tend to have
more complex views, that would need custom code to update, and often
this is very difficult to achieve with rules.

The way this is done in some other databases is "INSTEAD OF"
triggers. These triggers are neither BEFORE nor AFTER, but
INSTEAD. And they are also typically ROW-level triggers, eg:

CREATE TRIGGER my_trig INSTEAD OF UPDATE ON my_view
  FOR EACH ROW EXECUTE PROCEDURE my_trig_fn();

The trigger fires for each row that needs to be updated in the view,
and has access to OLD and NEW tuples containing all the columns from
the view. The trigger function takes full responsibility for updating
the base tables, which works OK provided that the view exposes
sufficient key columns to allow the relevant tuples in the base tables
to be identified.

I've used this feature in Oracle in the past, and found it very
useful. What I'm proposing is something similar to that. The main
restrictions placed on INSTEAD OF triggers in Oracle are:

1). They're only allowed on views not tables.
2). They may only be ROW-level triggers.
3). They don't support WHEN conditions.
4). They can't be used with for UPDATE OF <column list>.

1 and 2 could possibly be relaxed, but IMO they are sensible
restrictions at least in the first pass. 3 and 4 ensure that there is
an *unconditional* action to take instead of the VIEW update.

One thing that I think I would do differently from Oracle is the
following: in Oracle the return value of an INSTEAD OF trigger is
ignored, and it always just assumes that the trigger performed the
required update. In PostgreSQL, I think it might be more consistent
with the existing triggers to have the trigger return the OLD tuple
for DELETEs and the (possibly modified) NEW tuple for INSERT and
UPDATE. This would allow RETURNING clauses to show what was actually
added to the view by the trigger. A return value of NULL would
indicate that the trigger did nothing.



I've been thinking about how I would implement this, and attached is a
proof-of-concept patch. This patch doesn't actually have any trigger
definition or execution code (although I think writing that part
should be fairly mechanical). It's purpose is to get an idea of the
necessary changes to the rewriter, planner and executor. The patch
just raises NOTICEs in the executor at the point where the INSTEAD OF
triggers would be fired, showing what data would be available to those
triggers.

It's quite a small patch, which I hope isn't a sign that I've vastly
oversimplified this or overlooked something crucial. It works
basically as follows:

- In the rewriter, if the target of an UPDATE or DELETE is a VIEW,
then instead of simply replacing that view with its subquery, it now
adds the subquery to the end of the original query's rtable list, and
leaves the original RTE for the VIEW in place as the query's
resultRelation and for any returningList Vars. All other parts of the
query, including the jointree fromlist are modified to refer to the
VIEW's subquery, not the original VIEW RTE. For an INSERT, the
rewriter does nothing, leaving the VIEW RTE as the query
resultRelation.

- The planner largely ignores the VIEW relation RTE, since it does not
appear in the jointree fromlist. It is only used as the query target,
giving a plan with a ModifyTable node at the top and a subquery from
the view's base tables, joined to any other tables in the query,
together with any view conditions combined with any user conditions.

- In the executor, nodeModifyTable needs a few changes to be able to
handle a VIEW as the target relation. BEFORE and AFTER ROW triggers
are disallowed on views (I don't see a need for them if we have
INSTEAD OF triggers, and it's not clear how they would work anyway, in
the absence of a real table CTID). BEFORE and AFTER STATEMENT triggers
on the other hand, ought to work as-is. The INSTEAD OF triggers would
fire for each tuple coming from the subquery instead of the normal
heap_update/insert/delete().


Does this sound like a useful feature? Is this a sane approach to
implementing it? If not, has anyone else given any thought as to how
it might be implemented?

If this approach is valid, I believe that I should have time to put
together a more complete patch for the next commitfest.

Regards,
Dean

---

P.S. Here's some test output (note: the table is never actually updated
in this test, the NOTICEs just show what triggers would have done):

CREATE TABLE foo(a int PRIMARY KEY, b int, c int);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index
"foo_pkey" for table "foo"
CREATE TABLE
INSERT INTO foo
  SELECT g, g*10, g*100 FROM generate_series(1,10000) AS g;
INSERT 0 10000
CREATE VIEW foo_v AS
  SELECT b AS bb, c AS cc, a AS aa FROM foo WHERE a%2 = 0;
CREATE VIEW
INSERT INTO foo_v (aa,bb,cc)
  SELECT g, g*10, g*100 FROM generate_series(10001,10005) AS g;
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100010,
cc=1000100, aa=10001)
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100020,
cc=1000200, aa=10002)
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100030,
cc=1000300, aa=10003)
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100040,
cc=1000400, aa=10004)
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100050,
cc=1000500, aa=10005)
INSERT 0 5
UPDATE foo_v SET cc=0 WHERE aa < 10;
NOTICE:  Trigger would UDPATE view "foo_v" OLD=(20,200,2) NEW=(bb=20,
cc=0, aa=2)
NOTICE:  Trigger would UDPATE view "foo_v" OLD=(40,400,4) NEW=(bb=40,
cc=0, aa=4)
NOTICE:  Trigger would UDPATE view "foo_v" OLD=(60,600,6) NEW=(bb=60,
cc=0, aa=6)
NOTICE:  Trigger would UDPATE view "foo_v" OLD=(80,800,8) NEW=(bb=80,
cc=0, aa=8)
UPDATE 4
EXPLAIN UPDATE foo_v SET cc=0 WHERE aa < 10;
                                   QUERY PLAN
---------------------------------------------------------------------------------
 Update  (cost=70.93..188.18 rows=18 width=18)
   ->  Bitmap Heap Scan on foo  (cost=70.93..188.18 rows=18 width=18)
         Recheck Cond: (a < 10)
         Filter: ((a % 2) = 0)
         ->  Bitmap Index Scan on foo_pkey  (cost=0.00..70.93 rows=3557 width=0)
               Index Cond: (a < 10)
(6 rows)

DELETE FROM foo_v WHERE aa = 50;
NOTICE:  Trigger would DELETE from view "foo_v" OLD=(500,5000,50)
DELETE 1
EXPLAIN DELETE FROM foo_v WHERE aa = 50;
                                QUERY PLAN
---------------------------------------------------------------------------
 Delete  (cost=0.00..8.27 rows=1 width=18)
   ->  Index Scan using foo_pkey on foo  (cost=0.00..8.27 rows=1 width=18)
         Index Cond: (a = 50)
         Filter: ((a % 2) = 0)
(4 rows)

INSERT INTO foo_v (aa,bb,cc)
  SELECT g, g*10, g*100 FROM generate_series(10001,10005) AS g
  RETURNING aa,bb,cc;
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100010,
cc=1000100, aa=10001)
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100020,
cc=1000200, aa=10002)
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100030,
cc=1000300, aa=10003)
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100040,
cc=1000400, aa=10004)
NOTICE:  Trigger would INSERT into view "foo_v" NEW=(bb=100050,
cc=1000500, aa=10005)
  aa   |   bb   |   cc
-------+--------+---------
 10001 | 100010 | 1000100
 10002 | 100020 | 1000200
 10003 | 100030 | 1000300
 10004 | 100040 | 1000400
 10005 | 100050 | 1000500
(5 rows)

INSERT 0 5
CREATE VIEW foo_vv AS
  SELECT cc AS c, aa AS a, bb AS b FROM foo_v WHERE aa%3 = 0;
CREATE VIEW
SELECT COUNT(*) FROM foo_vv;
 count
-------
  1666
(1 row)

SELECT * FROM foo_vv ORDER BY a LIMIT 5;
  c   | a  |  b
------+----+-----
  600 |  6 |  60
 1200 | 12 | 120
 1800 | 18 | 180
 2400 | 24 | 240
 3000 | 30 | 300
(5 rows)

UPDATE foo_vv SET b=a*11 WHERE a=6;
NOTICE:  Trigger would UDPATE view "foo_vv" OLD=(600,6,60) NEW=(c=600,
a=6, b=66)
UPDATE 1
EXPLAIN UPDATE foo_vv SET b=a*11 WHERE a=6;
                                QUERY PLAN
---------------------------------------------------------------------------
 Update  (cost=0.00..8.28 rows=1 width=18)
   ->  Index Scan using foo_pkey on foo  (cost=0.00..8.28 rows=1 width=18)
         Index Cond: (a = 6)
         Filter: (((a % 2) = 0) AND ((a % 3) = 0))
(4 rows)

CREATE VIEW foo_vvv AS
  SELECT a, b, (SELECT 1 FROM foo_vv AS vv2 WHERE vv2.a = vv1.b/12) AS c
    FROM foo_vv AS vv1;
CREATE VIEW
DELETE FROM foo_vvv WHERE a=12;
NOTICE:  Trigger would DELETE from view "foo_vvv" OLD=(12,120,)
DELETE 1
EXPLAIN DELETE FROM foo_vvv WHERE a=12;
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Delete  (cost=0.00..16.56 rows=1 width=14)
   ->  Index Scan using foo_pkey on foo  (cost=0.00..16.56 rows=1 width=14)
         Index Cond: (a = 12)
         Filter: (((a % 2) = 0) AND ((a % 3) = 0))
         SubPlan 1
           ->  Index Scan using foo_pkey on foo  (cost=0.00..8.28
rows=1 width=0)
                 Index Cond: (a = (public.foo.b / 12))
                 Filter: (((a % 2) = 0) AND ((a % 3) = 0))
(8 rows)

UPDATE foo_vvv SET c=NULL WHERE a=36;
NOTICE:  Trigger would UDPATE view "foo_vvv" OLD=(36,360,1) NEW=(a=36,
b=360, c=null)
UPDATE 1
EXPLAIN UPDATE foo_vvv SET c=NULL WHERE a=36;
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Update  (cost=0.00..16.56 rows=1 width=14)
   ->  Index Scan using foo_pkey on foo  (cost=0.00..16.56 rows=1 width=14)
         Index Cond: (a = 36)
         Filter: (((a % 2) = 0) AND ((a % 3) = 0))
         SubPlan 1
           ->  Index Scan using foo_pkey on foo  (cost=0.00..8.28
rows=1 width=0)
                 Index Cond: (a = (public.foo.b / 12))
                 Filter: (((a % 2) = 0) AND ((a % 3) = 0))
(8 rows)

INSERT INTO foo_vvv VALUES (1,2,3);
NOTICE:  Trigger would INSERT into view "foo_vvv" NEW=(a=1, b=2, c=3)
INSERT 0 1
EXPLAIN INSERT INTO foo_vvv VALUES (1,2,3);
                   QUERY PLAN
------------------------------------------------
 Insert  (cost=0.00..0.01 rows=1 width=0)
   ->  Result  (cost=0.00..0.01 rows=1 width=0)
(2 rows)

CREATE RULE foo_vv_rule AS ON UPDATE TO foo_vv
  DO INSTEAD SELECT 'foo_vv_rule doing nothing';
CREATE RULE
UPDATE foo_vv SET b=a*11 WHERE a=6;
         ?column?
---------------------------
 foo_vv_rule doing nothing
(1 row)

UPDATE 0
EXPLAIN UPDATE foo_vv SET b=a*11 WHERE a=6;
                             QUERY PLAN
--------------------------------------------------------------------
 Index Scan using foo_pkey on foo  (cost=0.00..8.28 rows=1 width=0)
   Index Cond: (a = 6)
   Filter: (((a % 2) = 0) AND ((a % 3) = 0))
(3 rows)

UPDATE foo_vvv SET b=a*11 WHERE a=6;
NOTICE:  Trigger would UDPATE view "foo_vvv" OLD=(6,60,) NEW=(a=6, b=66, c=null)
UPDATE 1
EXPLAIN UPDATE foo_vvv SET b=a*11 WHERE a=6;
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Update  (cost=0.00..24.84 rows=1 width=14)
   ->  Index Scan using foo_pkey on foo  (cost=0.00..24.84 rows=1 width=14)
         Index Cond: (a = 6)
         Filter: (((a % 2) = 0) AND ((a % 3) = 0))
         SubPlan 1
           ->  Index Scan using foo_pkey on foo  (cost=0.00..8.28
rows=1 width=0)
                 Index Cond: (a = (public.foo.b / 12))
                 Filter: (((a % 2) = 0) AND ((a % 3) = 0))
         SubPlan 2
           ->  Index Scan using foo_pkey on foo  (cost=0.00..8.28
rows=1 width=0)
                 Index Cond: (a = (public.foo.b / 12))
                 Filter: (((a % 2) = 0) AND ((a % 3) = 0))
(12 rows)

CREATE RULE foo_vvv_rule AS ON UPDATE TO foo_vvv
  DO INSTEAD UPDATE foo_v SET bb=NEW.b, cc=NEW.c WHERE aa=OLD.a;
CREATE RULE
UPDATE foo_vvv SET b=a*11 WHERE a >= 30 AND a <= 36;
NOTICE:  Trigger would UDPATE view "foo_v" OLD=(300,3000,30)
NEW=(bb=330, cc=null, aa=30)
NOTICE:  Trigger would UDPATE view "foo_v" OLD=(360,3600,36)
NEW=(bb=396, cc=1, aa=36)
UPDATE 2
EXPLAIN UPDATE foo_vvv SET b=a*11 WHERE a >= 30 AND a <= 36;
                                     QUERY PLAN
------------------------------------------------------------------------------------
 Update  (cost=4.78..79.30 rows=1 width=32)
   ->  Nested Loop  (cost=4.78..79.30 rows=1 width=32)
         ->  Bitmap Heap Scan on foo  (cost=4.78..62.73 rows=1 width=14)
               Recheck Cond: ((a >= 30) AND (a <= 36))
               Filter: (((a % 2) = 0) AND ((a % 3) = 0))
               ->  Bitmap Index Scan on foo_pkey  (cost=0.00..4.78
rows=53 width=0)
                     Index Cond: ((a >= 30) AND (a <= 36))
         ->  Index Scan using foo_pkey on foo  (cost=0.00..8.27 rows=1 width=18)
               Index Cond: (a = public.foo.a)
               Filter: ((a % 2) = 0)
         SubPlan 1
           ->  Index Scan using foo_pkey on foo  (cost=0.00..8.28
rows=1 width=0)
                 Index Cond: (a = (public.foo.b / 12))
                 Filter: (((a % 2) = 0) AND ((a % 3) = 0))
(14 rows)

CREATE OR REPLACE RULE foo_vvv_rule AS ON UPDATE TO foo_vvv
  DO INSTEAD UPDATE foo_v SET bb=NEW.b, cc=NEW.c WHERE aa=OLD.a
             RETURNING NEW.a, NEW.b, NEW.c;
CREATE RULE
UPDATE foo_vvv SET b=a*11 WHERE a >= 30 AND a <= 36 RETURNING a, b, c;
NOTICE:  Trigger would UDPATE view "foo_v" OLD=(300,3000,30)
NEW=(bb=330, cc=null, aa=30)
NOTICE:  Trigger would UDPATE view "foo_v" OLD=(360,3600,36)
NEW=(bb=396, cc=1, aa=36)
 a  |  b  | c
----+-----+---
 30 | 330 |
 36 | 396 | 1
(2 rows)

UPDATE 2
EXPLAIN UPDATE foo_vvv SET b=a*11 WHERE a >= 30 AND a <= 36 RETURNING a, b, c;
                                     QUERY PLAN
------------------------------------------------------------------------------------
 Update  (cost=4.78..79.30 rows=1 width=32)
   ->  Nested Loop  (cost=4.78..79.30 rows=1 width=32)
         ->  Bitmap Heap Scan on foo  (cost=4.78..62.73 rows=1 width=14)
               Recheck Cond: ((a >= 30) AND (a <= 36))
               Filter: (((a % 2) = 0) AND ((a % 3) = 0))
               ->  Bitmap Index Scan on foo_pkey  (cost=0.00..4.78
rows=53 width=0)
                     Index Cond: ((a >= 30) AND (a <= 36))
         ->  Index Scan using foo_pkey on foo  (cost=0.00..8.27 rows=1 width=18)
               Index Cond: (a = public.foo.a)
               Filter: ((a % 2) = 0)
         SubPlan 1
           ->  Index Scan using foo_pkey on foo  (cost=0.00..8.28
rows=1 width=0)
                 Index Cond: (a = (public.foo.b / 12))
                 Filter: (((a % 2) = 0) AND ((a % 3) = 0))
   SubPlan 2
     ->  Index Scan using foo_pkey on foo  (cost=0.00..8.28 rows=1 width=0)
           Index Cond: (a = (public.foo.b / 12))
           Filter: (((a % 2) = 0) AND ((a % 3) = 0))
(18 rows)

Attachment

Re: Proposal / proof of concept: Triggers on VIEWs

From
Marko Tiikkaja
Date:
On 8/4/10 2:39 PM +0300, Dean Rasheed wrote:
> Does this sound like a useful feature? Is this a sane approach to
> implementing it? If not, has anyone else given any thought as to how
> it might be implemented?

I didn't look at the patch, but so far, I've identified three problems 
with the existing view system:
    1) You can't re-evaluate the UPDATE expression like an UPDATE on a       table does.  Consider for example  UPDATE
fooSET a=a+1;  If the       tuples change before we get to them, we lose data because we       simply can't re-evaluate
"a+1"in the trigger.
 
    2) You can't set the number of affected rows.
    3) You can't set the RETURNING results.  You suggested that       RETURNING for DELETE would return the OLD value,
butthat seems       broken because that's not necessarily what was deleted.  I didn't       understand what you
suggestionfor UPDATE was; how does PG know       that if the view doesn't have a primary key?
 

I think these are the main three problems that prevent people from 
actually using views, and I think these should be focused on when adding 
triggers on VIEWS.  I would love to see the feature though.

Any thoughts?


Regards,
Marko Tiikkaja


Re: Proposal / proof of concept: Triggers on VIEWs

From
Dean Rasheed
Date:
On 4 August 2010 13:22, Marko Tiikkaja <marko.tiikkaja@cs.helsinki.fi> wrote:
> On 8/4/10 2:39 PM +0300, Dean Rasheed wrote:
>>
>> Does this sound like a useful feature? Is this a sane approach to
>> implementing it? If not, has anyone else given any thought as to how
>> it might be implemented?
>
> I didn't look at the patch, but so far, I've identified three problems with
> the existing view system:
>
>    1) You can't re-evaluate the UPDATE expression like an UPDATE on a
>       table does.  Consider for example  UPDATE foo SET a=a+1;  If the
>       tuples change before we get to them, we lose data because we
>       simply can't re-evaluate "a+1" in the trigger.
>

Is this the same problem the writeable CTE patch ran into?
The way I've done this, the OLD values passed to the trigger all come
from a snapshot established at the start of the query, so you're
right, the trigger won't see values changed after the query started,
unless it re-queries for them. I don't see an easy way round that.


>    2) You can't set the number of affected rows.
>

Yeah, the assumption is that the number of affected rows is the number
of rows in the view that matched the user's WHERE clause. You could
return fewer affected rows by having the trigger return NULL for some
of them, but you can't say that you've affected more than that. So
even if the trigger updates 10 rows in the base tables for a given row
in the view, that still only counts as 1 row affected in the view by
the original query.


>    3) You can't set the RETURNING results.  You suggested that
>       RETURNING for DELETE would return the OLD value, but that seems
>       broken because that's not necessarily what was deleted.

Well that's what happens for a table. Alternatively the trigger could
modify OLD, and then have RETURNING return that, but that's not what
happens in a BEFORE DELETE trigger on a table.


>  I didn't
>       understand what you suggestion for UPDATE was; how does PG know
>       that if the view doesn't have a primary key?

For INSERT and UPDATE the trigger would compute and make the necessary
changes to the base tables, and then return the new contents of the
view's row in a modified copy of NEW, if necessary for RETURNING. This
might include re-computed derived values for example.

If the view doesn't have a PK, or any other way of uniquely
identifying rows then its probably hopeless. That's not a case that
this patch is targeted for.

Regards,
Dean


>
> I think these are the main three problems that prevent people from actually
> using views, and I think these should be focused on when adding triggers on
> VIEWS.  I would love to see the feature though.
>
> Any thoughts?
>
>
> Regards,
> Marko Tiikkaja
>


Re: Proposal / proof of concept: Triggers on VIEWs

From
Marko Tiikkaja
Date:
On 8/4/10 4:31 PM +0300, Dean Rasheed wrote:
>>     1) You can't re-evaluate the UPDATE expression like an UPDATE on a
>>        table does.  Consider for example  UPDATE foo SET a=a+1;  If the
>>        tuples change before we get to them, we lose data because we
>>        simply can't re-evaluate "a+1" in the trigger.
>>
>
> Is this the same problem the writeable CTE patch ran into?

No, that was something different.

> Yeah, the assumption is that the number of affected rows is the number
> of rows in the view that matched the user's WHERE clause. You could
> return fewer affected rows by having the trigger return NULL for some
> of them, but you can't say that you've affected more than that. So
> even if the trigger updates 10 rows in the base tables for a given row
> in the view, that still only counts as 1 row affected in the view by
> the original query.

I think that's fine.

>>     3) You can't set the RETURNING results.  You suggested that
>>        RETURNING for DELETE would return the OLD value, but that seems
>>        broken because that's not necessarily what was deleted.
>
> Well that's what happens for a table. Alternatively the trigger could
> modify OLD, and then have RETURNING return that, but that's not what
> happens in a BEFORE DELETE trigger on a table.

I'm not sure I understand.  RETURNING in DELETE on a table fetches the 
old value after it was DELETEd, so it really is what the tuple was 
before the DLETE, not what is seen by the snapshot.  In a BEFORE DELETE 
trigger, the row is always locked so it can't change after the trigger 
is fired.

> For INSERT and UPDATE the trigger would compute and make the necessary
> changes to the base tables, and then return the new contents of the
> view's row in a modified copy of NEW, if necessary for RETURNING. This
> might include re-computed derived values for example.

I see.


Regards,
Marko Tiikkaja


Re: Proposal / proof of concept: Triggers on VIEWs

From
Dean Rasheed
Date:
On 4 August 2010 14:43, Marko Tiikkaja <marko.tiikkaja@cs.helsinki.fi> wrote:
>>>    3) You can't set the RETURNING results.  You suggested that
>>>       RETURNING for DELETE would return the OLD value, but that seems
>>>       broken because that's not necessarily what was deleted.
>>
>> Well that's what happens for a table. Alternatively the trigger could
>> modify OLD, and then have RETURNING return that, but that's not what
>> happens in a BEFORE DELETE trigger on a table.
>
> I'm not sure I understand.  RETURNING in DELETE on a table fetches the old
> value after it was DELETEd, so it really is what the tuple was before the
> DLETE, not what is seen by the snapshot.  In a BEFORE DELETE trigger, the
> row is always locked so it can't change after the trigger is fired.
>

Ah, I think I mis-understood. If I understand what you're saying
correctly, you're worried that the row might have been modified in the
same query, prior to being deleted, and you want RETURNING to return
the updated value, as it was when it was deleted.

So yes, you're right, that really is different from a table. I guess
it would have to be handled by the trigger returning a modified copy
of OLD for RETURNING to use.

Regards,
Dean


Re: Proposal / proof of concept: Triggers on VIEWs

From
Marko Tiikkaja
Date:
On 8/4/10 5:03 PM +0300, Dean Rasheed wrote:
> On 4 August 2010 14:43, Marko Tiikkaja<marko.tiikkaja@cs.helsinki.fi>  wrote:
>> I'm not sure I understand.  RETURNING in DELETE on a table fetches the old
>> value after it was DELETEd, so it really is what the tuple was before the
>> DLETE, not what is seen by the snapshot.  In a BEFORE DELETE trigger, the
>> row is always locked so it can't change after the trigger is fired.
>>
>
> Ah, I think I mis-understood. If I understand what you're saying
> correctly, you're worried that the row might have been modified in the
> same query, prior to being deleted, and you want RETURNING to return
> the updated value, as it was when it was deleted.

I'm mainly concerned about concurrently running transactions.


Regards,
Marko Tiikkaja


Re: Proposal / proof of concept: Triggers on VIEWs

From
Dean Rasheed
Date:
On 4 August 2010 15:08, Marko Tiikkaja <marko.tiikkaja@cs.helsinki.fi> wrote:
> On 8/4/10 5:03 PM +0300, Dean Rasheed wrote:
>>
>> On 4 August 2010 14:43, Marko Tiikkaja<marko.tiikkaja@cs.helsinki.fi>
>>  wrote:
>>>
>>> I'm not sure I understand.  RETURNING in DELETE on a table fetches the
>>> old
>>> value after it was DELETEd, so it really is what the tuple was before the
>>> DLETE, not what is seen by the snapshot.  In a BEFORE DELETE trigger, the
>>> row is always locked so it can't change after the trigger is fired.
>>>
>>
>> Ah, I think I mis-understood. If I understand what you're saying
>> correctly, you're worried that the row might have been modified in the
>> same query, prior to being deleted, and you want RETURNING to return
>> the updated value, as it was when it was deleted.
>
> I'm mainly concerned about concurrently running transactions.
>

Sorry for the delay replying.

Once again, I think I mis-understood your point. I think that the
database can't really lock anything before firing the trigger because
the view might contain grouping/aggregates or even not be based on any
real tables at all, so it would be impossible to work out what to
lock. Thus it would be up to the trigger function to get this right.
In the simplest case, for a DELETE, this might look something like:

CREATE OR REPLACE FUNCTION instead_of_delete_trig_fn()
RETURNS trigger AS
$$
BEGIN DELETE FROM base_table WHERE pk = OLD.pk; IF NOT FOUND THEN RETURN NULL; END IF;
 RETURN OLD;
END;
$$
LANGUAGE plpgsql;

If 2 users try to delete the same row, the second would block until
the first user's transaction finished, and if the first user
committed, the second user's trigger would return NULL, which the
database would signal as no rows deleted.

Regards,
Dean


Re: Proposal / proof of concept: Triggers on VIEWs

From
Naveed Alam
Date:
aoa how can i connect my postgres database to manifold?<br /><br />Regards?<br /><br /><br /><br /><div
class="gmail_quote">OnFri, Aug 6, 2010 at 12:49 PM, Dean Rasheed <span dir="ltr"><<a
href="mailto:dean.a.rasheed@gmail.com">dean.a.rasheed@gmail.com</a>></span>wrote:<br /><blockquote
class="gmail_quote"style="margin: 0pt 0pt 0pt 0.8ex; border-left: 1px solid rgb(204, 204, 204); padding-left: 1ex;">On
4August 2010 15:08, Marko Tiikkaja <<a
href="mailto:marko.tiikkaja@cs.helsinki.fi">marko.tiikkaja@cs.helsinki.fi</a>>wrote:<br /> > On 8/4/10 5:03 PM
+0300,Dean Rasheed wrote:<br /> >><br /> >> On 4 August 2010 14:43, Marko Tiikkaja<<a
href="mailto:marko.tiikkaja@cs.helsinki.fi">marko.tiikkaja@cs.helsinki.fi</a>><br/> >>  wrote:<br />
>>><br/> >>> I'm not sure I understand.  RETURNING in DELETE on a table fetches the<br />
>>>old<br /> >>> value after it was DELETEd, so it really is what the tuple was before the<br />
>>>DLETE, not what is seen by the snapshot.  In a BEFORE DELETE trigger, the<br /> >>> row is always
lockedso it can't change after the trigger is fired.<br /> >>><br /> >><br /> >> Ah, I think I
mis-understood.If I understand what you're saying<br /> >> correctly, you're worried that the row might have been
modifiedin the<br /> >> same query, prior to being deleted, and you want RETURNING to return<br /> >> the
updatedvalue, as it was when it was deleted.<br /> ><br /> > I'm mainly concerned about concurrently running
transactions.<br/> ><br /><br /> Sorry for the delay replying.<br /><br /> Once again, I think I mis-understood your
point.I think that the<br /> database can't really lock anything before firing the trigger because<br /> the view might
containgrouping/aggregates or even not be based on any<br /> real tables at all, so it would be impossible to work out
whatto<br /> lock. Thus it would be up to the trigger function to get this right.<br /> In the simplest case, for a
DELETE,this might look something like:<br /><br /> CREATE OR REPLACE FUNCTION instead_of_delete_trig_fn()<br /> RETURNS
triggerAS<br /> $$<br /> BEGIN<br />  DELETE FROM base_table WHERE pk = OLD.pk;<br />  IF NOT FOUND THEN RETURN NULL;
ENDIF;<br /><br />  RETURN OLD;<br /> END;<br /> $$<br /> LANGUAGE plpgsql;<br /><br /> If 2 users try to delete the
samerow, the second would block until<br /> the first user's transaction finished, and if the first user<br />
committed,the second user's trigger would return NULL, which the<br /> database would signal as no rows deleted.<br
/><br/> Regards,<br /> Dean<br /><font color="#888888"><br /> --<br /> Sent via pgsql-hackers mailing list (<a
href="mailto:pgsql-hackers@postgresql.org">pgsql-hackers@postgresql.org</a>)<br/> To make changes to your
subscription:<br/><a href="http://www.postgresql.org/mailpref/pgsql-hackers"
target="_blank">http://www.postgresql.org/mailpref/pgsql-hackers</a><br/></font></blockquote></div><br /> 

Re: Proposal / proof of concept: Triggers on VIEWs

From
Marko Tiikkaja
Date:
On 8/6/2010 10:49 AM, Dean Rasheed wrote:
> On 4 August 2010 15:08, Marko Tiikkaja<marko.tiikkaja@cs.helsinki.fi>  wrote:
>> I'm mainly concerned about concurrently running transactions.
>
> Once again, I think I mis-understood your point. I think that the
> database can't really lock anything before firing the trigger because
> the view might contain grouping/aggregates or even not be based on any
> real tables at all, so it would be impossible to work out what to
> lock.

Right.

> Thus it would be up to the trigger function to get this right.
> In the simplest case, for a DELETE, this might look something like:
>
> CREATE OR REPLACE FUNCTION instead_of_delete_trig_fn()
> RETURNS trigger AS
> $$
> BEGIN
>    DELETE FROM base_table WHERE pk = OLD.pk;
>    IF NOT FOUND THEN RETURN NULL; END IF;
>
>    RETURN OLD;
> END;
> $$
> LANGUAGE plpgsql;
>
> If 2 users try to delete the same row, the second would block until
> the first user's transaction finished, and if the first user
> committed, the second user's trigger would return NULL, which the
> database would signal as no rows deleted.

The problem is that this isn't even nearly sufficient.  I gave this some 
more thought while I was away, and it seems that I missed at least one 
more important thing: the WHERE clause.  Imagine this query:

DELETE FROM view WHERE pk = 1 AND f1 > 0;

Now the trigger function gets called if the row where pk = 1, as seen by 
the query's snapshot, has f1 > 0.  But if a concurrent transaction sets 
f1 to 0 before the triggers gets to the row, you end up deleting a row 
that doesn't match the WHERE clause.  I have a few ideas on how this 
could be tackled, but I think we need to split these two threads.  I 
still think that having triggers on views without addressing these 
concurrency concerns is not a good idea, though.


Regards,
Marko Tiikkaja


Re: Proposal / proof of concept: Triggers on VIEWs

From
Dean Rasheed
Date:
On 7 August 2010 10:56, Marko Tiikkaja <marko.tiikkaja@cs.helsinki.fi> wrote:
> The problem is that this isn't even nearly sufficient.  I gave this some
> more thought while I was away, and it seems that I missed at least one more
> important thing: the WHERE clause.  Imagine this query:
>
> DELETE FROM view WHERE pk = 1 AND f1 > 0;
>
> Now the trigger function gets called if the row where pk = 1, as seen by the
> query's snapshot, has f1 > 0.  But if a concurrent transaction sets f1 to 0
> before the triggers gets to the row, you end up deleting a row that doesn't
> match the WHERE clause.

I've been playing with this in Oracle and I can confirm that it behaves
exactly as my code would do. So in this example, the trigger deletes
from the underlying table even after the row has been changed so that
it no longer satisfies the original WHERE clause. The case I find
worse is that if 2 concurrent transactions do UPDATE view SET f1=f1+1,
the value will only be incremented once.

For the record, here is the Oracle code in all its gory detail:

CREATE TABLE foo(a int, b int);
CREATE VIEW foo_v AS SELECT * FROM foo;

CREATE TRIGGER del_trig INSTEAD OF DELETE ON foo_v
FOR EACH ROW
BEGIN DELETE FROM foo WHERE a=:OLD.a;
END;
/

CREATE TRIGGER mod_trig INSTEAD OF UPDATE ON foo_v
FOR EACH ROW
BEGIN UPDATE foo SET a=:NEW.a, b=:NEW.b WHERE a=:OLD.a;
END;
/

INSERT INTO foo VALUES(1,1);
COMMIT;

So in the first case, 2 concurrent transactions T1 and T2 are started
and do the following:

1. T1 does UPDATE foo_v SET b=0 WHERE a=1;  -- Trigger fires and updates foo
2. T2 does DELETE FROM foo_v WHERE a=1 AND b=1;  -- This matches 1 row in the VIEW (T1 not committed yet)  -- Trigger
firesand does DELETE FROM foo WHERE a=1  -- T2 waits for T1 
3. T1 commits
4. T2 resumes and deletes the row from foo, since it satisfies a=1

Arguably, this is a deficiency in the trigger function rather than the
trigger firing code. It could be fixed by having the trigger re-check
all the columns in the table against OLD, but that would be pretty
cumbersome for very wide tables, and none of the documented
examples I've seen take that approach.

The second case is as follows:

INSERT INTO foo VALUES(1,1);
COMMIT;

Then 2 concurrent transactions T1 and T2 are started and do the
following:

1. T1 does UPDATE foo_v SET b=b+1 WHERE a=1;  -- Trigger fires and does  UPDATE foo SET b=2 WHERE a=1;
2. T2 does UDPATE foo_v SET b=b+1 WHERE a=1;  -- Trigger fires and does  UPDATE foo SET b=2 WHERE a=1;  -- T2 waits for
T1
3. T1 commits
4. T2 resumes and sets b to 2 as requested inside the trigger

So the net result is b=2 rather than b=3 - pretty-much the textbook
concurrency example. Surprisingly I don't see too many people
complaining about this in Oracle, because to me it seems pretty bad.

In PostgreSQL we could fix it by declaring the VIEW query as FOR
UPDATE, but that's no good if for example the VIEW was based on an
aggregate. Oracle has the same limitations on FOR UPDATE, but also
AFAICS it doesn't allow VIEWs to be created using SELECT FOR UPDATE
at all.

For those migrating code from Oracle, providing this feature as-is
might be valuable, since presumably they are not too concerned about
these concurrency issues. Ideally we'd want to do better though.

Thoughts?

Regards,
Dean



> I have a few ideas on how this could be tackled,
> but I think we need to split these two threads.  I still think that having
> triggers on views without addressing these concurrency concerns is not a
> good idea, though.
>
>
> Regards,
> Marko Tiikkaja
>


Re: Proposal / proof of concept: Triggers on VIEWs

From
Marko Tiikkaja
Date:
On 8/8/2010 12:49 PM, Dean Rasheed wrote:
> On 7 August 2010 10:56, Marko Tiikkaja<marko.tiikkaja@cs.helsinki.fi>  wrote:
>> The problem is that this isn't even nearly sufficient.  I gave this some
>> more thought while I was away, and it seems that I missed at least one more
>> important thing: the WHERE clause.  Imagine this query:
>>
>> DELETE FROM view WHERE pk = 1 AND f1>  0;
>>
>> Now the trigger function gets called if the row where pk = 1, as seen by the
>> query's snapshot, has f1>  0.  But if a concurrent transaction sets f1 to 0
>> before the triggers gets to the row, you end up deleting a row that doesn't
>> match the WHERE clause.
>
> I've been playing with this in Oracle and I can confirm that it behaves
> exactly as my code would do. So in this example, the trigger deletes
> from the underlying table even after the row has been changed so that
> it no longer satisfies the original WHERE clause. The case I find
> worse is that if 2 concurrent transactions do UPDATE view SET f1=f1+1,
> the value will only be incremented once.

Wow.  I'm surprised to hear this.

> In PostgreSQL we could fix it by declaring the VIEW query as FOR
> UPDATE, but that's no good if for example the VIEW was based on an
> aggregate. Oracle has the same limitations on FOR UPDATE, but also
> AFAICS it doesn't allow VIEWs to be created using SELECT FOR UPDATE
> at all.

Also making all SELECTs on the view FOR UPDATE is not a very good idea..

> For those migrating code from Oracle, providing this feature as-is
> might be valuable, since presumably they are not too concerned about
> these concurrency issues. Ideally we'd want to do better though.

Yes, you might be right.  This feature on its on can greatly simplify 
what people now have to do to get triggers on views.


Regards,
Marko Tiikkaja


Re: Proposal / proof of concept: Triggers on VIEWs

From
Marko Tiikkaja
Date:
On 2010-08-08 1:45 PM +0300, I wrote:
> On 8/8/2010 12:49 PM, Dean Rasheed wrote:
>> For those migrating code from Oracle, providing this feature as-is
>> might be valuable, since presumably they are not too concerned about
>> these concurrency issues. Ideally we'd want to do better though.
>
> Yes, you might be right.  This feature on its on can greatly simplify
> what people now have to do to get triggers on views.

This weekend I had the luxury of working with our VIEW system, and it 
seems like I greatly underestimated the power of this feature, even if 
the concurrency issues I've mentioned in this thread would be left 
unaddressed.

I haven't looked at the patch yet (I plan to do so), but +100 for this 
feature.

Should we expect a new patch any time soon?


Regards,
Marko Tiikkaja


Re: Proposal / proof of concept: Triggers on VIEWs

From
Dean Rasheed
Date:
On 14 August 2010 13:12, Marko Tiikkaja <marko.tiikkaja@cs.helsinki.fi> wrote:
> On 2010-08-08 1:45 PM +0300, I wrote:
>>
>> On 8/8/2010 12:49 PM, Dean Rasheed wrote:
>>>
>>> For those migrating code from Oracle, providing this feature as-is
>>> might be valuable, since presumably they are not too concerned about
>>> these concurrency issues. Ideally we'd want to do better though.
>>
>> Yes, you might be right.  This feature on its on can greatly simplify
>> what people now have to do to get triggers on views.
>
> This weekend I had the luxury of working with our VIEW system, and it seems
> like I greatly underestimated the power of this feature, even if the
> concurrency issues I've mentioned in this thread would be left unaddressed.
>
> I haven't looked at the patch yet (I plan to do so), but +100 for this
> feature.
>

Thanks for your feedback. I'd be interested in any ideas on the
concurrency issues.

> Should we expect a new patch any time soon?
>

I hope to look at this again tomorrow.
I'll try to post an updated patch then, with some real trigger code,
but without addressing any of the concurrency issues.

Regards,
Dean


Re: Proposal / proof of concept: Triggers on VIEWs

From
Dean Rasheed
Date:
On 14 August 2010 23:22, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
> I'll try to post an updated patch then, with some real trigger code,
>

I've moved this to a new thread, with a WIP patch that allow 3 types
of triggers to be added to VIEWs:
http://archives.postgresql.org/pgsql-hackers/2010-08/msg01030.php

Comments welcome.

Cheers,
Dean


Re: Proposal / proof of concept: Triggers on VIEWs

From
David Fetter
Date:
On Mon, Aug 16, 2010 at 09:05:12AM +0100, Dean Rasheed wrote:
> On 14 August 2010 23:22, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
> > I'll try to post an updated patch then, with some real trigger
> > code,
> 
> I've moved this to a new thread, with a WIP patch that allow 3 types
> of triggers to be added to VIEWs:
> http://archives.postgresql.org/pgsql-hackers/2010-08/msg01030.php
> 
> Comments welcome.

Please add this to the next commitfest :)

https://commitfest.postgresql.org/action/commitfest_view?id=7

Cheers,
David.
-- 
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778  AIM: dfetter666  Yahoo!: dfetter
Skype: davidfetter      XMPP: david.fetter@gmail.com
iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate


Re: Proposal / proof of concept: Triggers on VIEWs

From
Dean Rasheed
Date:
On 16 August 2010 14:45, David Fetter <david@fetter.org> wrote:
> Please add this to the next commitfest :)
>

Done.

Thanks,
Dean


> https://commitfest.postgresql.org/action/commitfest_view?id=7
>
> Cheers,
> David.
> --
> David Fetter <david@fetter.org> http://fetter.org/
> Phone: +1 415 235 3778  AIM: dfetter666  Yahoo!: dfetter
> Skype: davidfetter      XMPP: david.fetter@gmail.com
> iCal: webcal://www.tripit.com/feed/ical/people/david74/tripit.ics
>
> Remember to vote!
> Consider donating to Postgres: http://www.postgresql.org/about/donate
>