]> granicus.if.org Git - postgresql/commitdiff
Support automatically-updatable views.
authorTom Lane <tgl@sss.pgh.pa.us>
Sat, 8 Dec 2012 23:25:48 +0000 (18:25 -0500)
committerTom Lane <tgl@sss.pgh.pa.us>
Sat, 8 Dec 2012 23:26:21 +0000 (18:26 -0500)
This patch makes "simple" views automatically updatable, without the need
to create either INSTEAD OF triggers or INSTEAD rules.  "Simple" views
are those classified as updatable according to SQL-92 rules.  The rewriter
transforms INSERT/UPDATE/DELETE commands on such views directly into an
equivalent command on the underlying table, which will generally have
noticeably better performance than is possible with either triggers or
user-written rules.  A view that has INSTEAD OF triggers or INSTEAD rules
continues to operate the same as before.

For the moment, security_barrier views are not considered simple.
Also, we do not support WITH CHECK OPTION.  These features may be
added in future.

Dean Rasheed, reviewed by Amit Kapila

20 files changed:
doc/src/sgml/intro.sgml
doc/src/sgml/ref/alter_table.sgml
doc/src/sgml/ref/alter_view.sgml
doc/src/sgml/ref/create_rule.sgml
doc/src/sgml/ref/create_view.sgml
doc/src/sgml/rules.sgml
src/backend/catalog/information_schema.sql
src/backend/executor/execMain.c
src/backend/rewrite/rewriteHandler.c
src/backend/utils/adt/misc.c
src/include/catalog/catversion.h
src/include/catalog/pg_proc.h
src/include/rewrite/rewriteHandler.h
src/include/utils/builtins.h
src/test/regress/expected/triggers.out
src/test/regress/expected/updatable_views.out [new file with mode: 0644]
src/test/regress/parallel_schedule
src/test/regress/serial_schedule
src/test/regress/sql/triggers.sql
src/test/regress/sql/updatable_views.sql [new file with mode: 0644]

index 4d3f93f31741578d05627d7defe66806592bcd70..f0dba6f56fb487f2cc4ba110c3c8318bc3aadabe 100644 (file)
      <simpara>triggers</simpara>
     </listitem>
     <listitem>
-     <simpara>views</simpara>
+     <simpara>updatable views</simpara>
     </listitem>
     <listitem>
      <simpara>transactional integrity</simpara>
index 356419e2d08d11ade4fb9fbc3ad0e41966523646..5437626c3fe0e91dda236a9ba445d5630916f624 100644 (file)
@@ -147,11 +147,9 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     <listitem>
      <para>
       These forms set or remove the default value for a column.
-      The default values only apply to subsequent <command>INSERT</command>
-      commands; they do not cause rows already in the table to change.
-      Defaults can also be created for views, in which case they are
-      inserted into <command>INSERT</> statements on the view before
-      the view's <literal>ON INSERT</literal> rule is applied.
+      Default values only apply in subsequent <command>INSERT</command>
+      or <command>UPDATE</> commands; they do not cause rows already in the
+      table to change.
      </para>
     </listitem>
    </varlistentry>
index 521f05b84a1c983a69defed6d1f96f7b82474756..0e2b140241e3ada358795f6bd2a1f9520d0b8088 100644 (file)
@@ -80,10 +80,11 @@ ALTER VIEW [ IF EXISTS ] <replaceable class="parameter">name</replaceable> RESET
     <listitem>
      <para>
       These forms set or remove the default value for a column.
-      A default value associated with a view column is
-      inserted into <command>INSERT</> statements on the view before
-      the view's <literal>ON INSERT</literal> rule is applied, if
-      the <command>INSERT</> does not specify a value for the column.
+      A view column's default value is substituted into any
+      <command>INSERT</> or <command>UPDATE</> command whose target is the
+      view, before applying any rules or triggers for the view.  The view's
+      default will therefore take precedence over any default values from
+      underlying relations.
      </para>
     </listitem>
    </varlistentry>
index d4c3392129035a68fdd0918c417df4b3466f5156..381ea3ed6b4d259f7fedeeb7b77507193dd6cd6c 100644 (file)
@@ -45,10 +45,10 @@ CREATE [ OR REPLACE ] RULE <replaceable class="parameter">name</replaceable> AS
    additional commands to be executed when a given command on a given
    table is executed.  Alternatively, an <literal>INSTEAD</literal>
    rule can replace a given command by another, or cause a command
-   not to be executed at all.  Rules are used to implement table
+   not to be executed at all.  Rules are used to implement SQL
    views as well.  It is important to realize that a rule is really
    a command transformation mechanism, or command macro.  The
-   transformation happens before the execution of the commands starts.
+   transformation happens before the execution of the command starts.
    If you actually want an operation that fires independently for each
    physical row, you probably want to use a trigger, not a rule.
    More information about the rules system is in <xref linkend="rules">.
@@ -73,13 +73,11 @@ CREATE [ OR REPLACE ] RULE <replaceable class="parameter">name</replaceable> AS
    sufficient for your purposes) to replace update actions on the view
    with appropriate updates on other tables.  If you want to support
    <command>INSERT RETURNING</> and so on, then be sure to put a suitable
-   <literal>RETURNING</> clause into each of these rules.  Alternatively,
-   an updatable view can be implemented using <literal>INSTEAD OF</>
-   triggers (see <xref linkend="sql-createtrigger">).
+   <literal>RETURNING</> clause into each of these rules.
   </para>
 
   <para>
-   There is a catch if you try to use conditional rules for view
+   There is a catch if you try to use conditional rules for complex view
    updates: there <emphasis>must</> be an unconditional
    <literal>INSTEAD</literal> rule for each action you wish to allow
    on the view.  If the rule is conditional, or is not
@@ -95,6 +93,21 @@ CREATE [ OR REPLACE ] RULE <replaceable class="parameter">name</replaceable> AS
    <literal>INSTEAD NOTHING</literal> action.  (This method does not
    currently work to support <literal>RETURNING</> queries, however.)
   </para>
+
+  <note>
+   <para>
+    A view that is simple enough to be automatically updatable (see <xref
+    linkend="sql-createview">) does not require a user-created rule in
+    order to be updatable.  While you can create an explicit rule anyway,
+    the automatic update transformation will generally outperform an
+    explicit rule.
+   </para>
+
+   <para>
+    Another alternative worth considering is to use <literal>INSTEAD OF</>
+    triggers (see <xref linkend="sql-createtrigger">) in place of rules.
+   </para>
+  </note>
  </refsect1>
 
  <refsect1>
index 9e3bc2954f2deb472ed4f1298b29a031a3cca753..abbde94772c8dc733a3ec830eb2f2caea2ff2aec 100644 (file)
@@ -127,17 +127,6 @@ CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">n
  <refsect1>
   <title>Notes</title>
 
-   <para>
-    Currently, views are read only: the system will not allow an insert,
-    update, or delete on a view.  You can get the effect of an updatable
-    view by creating <literal>INSTEAD</> triggers on the view, which
-    must convert attempted inserts, etc. on the view into
-    appropriate actions on other tables.  For more information see
-    <xref linkend="sql-createtrigger">.  Another possibility is to create
-    rules (see <xref linkend="sql-createrule">), but in practice triggers
-    are easier to understand and use correctly.
-   </para>
-
    <para>
     Use the <xref linkend="sql-dropview">
     statement to drop views.
@@ -175,6 +164,105 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
     to replace it (this includes being a member of the owning role).
    </para>
 
+  <refsect2 id="SQL-CREATEVIEW-updatable-views">
+   <title id="SQL-CREATEVIEW-updatable-views-title">Updatable Views</title>
+
+   <indexterm zone="sql-createview-updatable-views">
+    <primary>updatable views</primary>
+   </indexterm>
+
+   <para>
+    Simple views are automatically updatable: the system will allow
+    <command>INSERT</>, <command>UPDATE</> and <command>DELETE</> statements
+    to be used on the view in the same way as on a regular table.  A view is
+    automatically updatable if it satisfies all of the following conditions:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The view must have exactly one entry in its <literal>FROM</> list,
+       which must be a table or another updatable view.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       The view definition must not contain <literal>WITH</>,
+       <literal>DISTINCT</>, <literal>GROUP BY</>, <literal>HAVING</>,
+       <literal>LIMIT</>, or <literal>OFFSET</> clauses at the top level.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       The view definition must not contain set operations (<literal>UNION</>,
+       <literal>INTERSECT</> or <literal>EXCEPT</>) at the top level.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       All columns in the view's select list must be simple references to
+       columns of the underlying relation.  They cannot be expressions,
+       literals or functions.  System columns cannot be referenced, either.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       No column of the underlying relation can appear more than once in
+       the view's select list.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       The view must not have the <literal>security_barrier</> property.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    If the view is automatically updatable the system will convert any
+    <command>INSERT</>, <command>UPDATE</> or <command>DELETE</> statement
+    on the view into the corresponding statement on the underlying base
+    relation.
+   </para>
+
+   <para>
+    If an automatically updatable view contains a <literal>WHERE</>
+    condition, the condition restricts which rows of the base relation are
+    available to be modified by <command>UPDATE</> and <command>DELETE</>
+    statements on the view.  However, an <command>UPDATE</> is allowed to
+    change a row so that it no longer satisfies the <literal>WHERE</>
+    condition, and thus is no longer visible through the view.  Similarly,
+    an <command>INSERT</> command can potentially insert base-relation rows
+    that do not satisfy the <literal>WHERE</> condition and thus are not
+    visible through the view.
+   </para>
+
+   <para>
+    A more complex view that does not satisfy all these conditions is
+    read-only by default: the system will not allow an insert, update, or
+    delete on the view.  You can get the effect of an updatable view by
+    creating <literal>INSTEAD OF</> triggers on the view, which must
+    convert attempted inserts, etc. on the view into appropriate actions
+    on other tables.  For more information see <xref
+    linkend="sql-createtrigger">.  Another possibility is to create rules
+    (see <xref linkend="sql-createrule">), but in practice triggers are
+    easier to understand and use correctly.
+   </para>
+
+   <para>
+    Note that the user performing the insert, update or delete on the view
+    must have the corresponding insert, update or delete privilege on the
+    view.  In addition the view's owner must have the relevant privileges on
+    the underlying base relations, but the user performing the update does
+    not need any permissions on the underlying base relations (see
+    <xref linkend="rules-privileges">).
+   </para>
+  </refsect2>
  </refsect1>
 
  <refsect1>
@@ -217,11 +305,15 @@ CREATE VIEW <replaceable class="parameter">name</replaceable> [ ( <replaceable c
       <term><literal>CHECK OPTION</literal></term>
       <listitem>
        <para>
-        This option has to do with updatable views.  All
-        <command>INSERT</> and <command>UPDATE</> commands on the view
-        will be checked to ensure data satisfy the view-defining
-        condition (that is, the new data would be visible through the
-        view). If they do not, the update will be rejected.
+        This option controls the behavior of automatically updatable views.
+        When given, <command>INSERT</> and <command>UPDATE</> commands on
+        the view will be checked to ensure new rows satisfy the
+        view-defining condition (that is, the new rows would be visible
+        through the view). If they do not, the update will be rejected.
+        Without <literal>CHECK OPTION</literal>, <command>INSERT</> and
+        <command>UPDATE</> commands on the view are allowed to create rows
+        that are not visible through the view.  (The latter behavior is the
+        only one currently provided by <productname>PostgreSQL</>.)
        </para>
       </listitem>
      </varlistentry>
@@ -252,6 +344,7 @@ CREATE VIEW <replaceable class="parameter">name</replaceable> [ ( <replaceable c
    <command>CREATE OR REPLACE VIEW</command> is a
    <productname>PostgreSQL</productname> language extension.
    So is the concept of a temporary view.
+   The <literal>WITH</> clause is an extension as well.
   </para>
  </refsect1>
 
index cc02ada7c715cc1f8e4b26f543302d0c81b49324..5811de7942f6a576c1df1e60f2d2ecb38ddddd82 100644 (file)
@@ -808,13 +808,28 @@ SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;
 <para>
     What happens if a view is named as the target relation for an
     <command>INSERT</command>, <command>UPDATE</command>, or
-    <command>DELETE</command>?  Simply doing the substitutions
+    <command>DELETE</command>?  Doing the substitutions
     described above would give a query tree in which the result
     relation points at a subquery range-table entry, which will not
-    work.  Instead, the rewriter assumes that the operation will be
-    handled by an <literal>INSTEAD OF</> trigger on the view.
-    (If there is no such trigger, the executor will throw an error
-    when execution starts.)  Rewriting works slightly differently
+    work.  There are several ways in which <productname>PostgreSQL</>
+    can support the appearance of updating a view, however.
+</para>
+
+<para>
+    If the subquery selects from a single base relation and is simple
+    enough, the rewriter can automatically replace the subquery with the
+    underlying base relation so that the <command>INSERT</command>,
+    <command>UPDATE</command>, or <command>DELETE</command> is applied to
+    the base relation in the appropriate way.  Views that are
+    <quote>simple enough</> for this are called <firstterm>automatically
+    updatable</>.  For detailed information on the kinds of view that can
+    be automatically updated, see <xref linkend="sql-createview">.
+</para>
+
+<para>
+    Alternatively, the operation may be handled by a user-provided
+    <literal>INSTEAD OF</> trigger on the view.
+    Rewriting works slightly differently
     in this case.  For <command>INSERT</command>, the rewriter does
     nothing at all with the view, leaving it as the result relation
     for the query.  For <command>UPDATE</command> and
@@ -842,10 +857,8 @@ SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;
 </para>
 
 <para>
-    If there are no <literal>INSTEAD OF</> triggers to update the view,
-    the executor will throw an error, because it cannot automatically
-    update a view by itself. To change this, we can define rules that
-    modify the behavior of <command>INSERT</command>,
+    Another possibility is for the user to define <literal>INSTEAD</>
+    rules that specify substitute actions for <command>INSERT</command>,
     <command>UPDATE</command>, and <command>DELETE</command> commands on
     a view. These rules will rewrite the command, typically into a command
     that updates one or more tables, rather than views. That is the topic
@@ -860,6 +873,22 @@ SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;
     evaluated first, and depending on the result, the triggers may not be
     used at all.
 </para>
+
+<para>
+    Automatic rewriting of an <command>INSERT</command>,
+    <command>UPDATE</command>, or <command>DELETE</command> query on a
+    simple view is always tried last. Therefore, if a view has rules or
+    triggers, they will override the default behavior of automatically
+    updatable views.
+</para>
+
+<para>
+    If there are no <literal>INSTEAD</> rules or <literal>INSTEAD OF</>
+    triggers for the view, and the rewriter cannot automatically rewrite
+    the query as an update on the underlying base relation, an error will
+    be thrown because the executor cannot update a view as such.
+</para>
+
 </sect2>
 
 </sect1>
index 4bd942fb6d5c597cc70cbaa5e9af72d3c916c7a3..fcac07ae485a9a88207cc618b6e49553ed22d23b 100644 (file)
@@ -730,10 +730,8 @@ CREATE VIEW columns AS
            CAST('NEVER' AS character_data) AS is_generated,
            CAST(null AS character_data) AS generation_expression,
 
-           CAST(CASE WHEN c.relkind = 'r'
-                          OR (c.relkind = 'v'
-                              AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '2' AND is_instead)
-                              AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '4' AND is_instead))
+           CAST(CASE WHEN c.relkind = 'r' OR
+                          (c.relkind = 'v' AND pg_view_is_updatable(c.oid))
                 THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_updatable
 
     FROM (pg_attribute a LEFT JOIN pg_attrdef ad ON attrelid = adrelid AND attnum = adnum)
@@ -1896,9 +1894,8 @@ CREATE VIEW tables AS
            CAST(nt.nspname AS sql_identifier) AS user_defined_type_schema,
            CAST(t.typname AS sql_identifier) AS user_defined_type_name,
 
-           CAST(CASE WHEN c.relkind = 'r'
-                          OR (c.relkind = 'v'
-                              AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '3' AND is_instead))
+           CAST(CASE WHEN c.relkind = 'r' OR
+                          (c.relkind = 'v' AND pg_view_is_insertable(c.oid))
                 THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_insertable_into,
 
            CAST(CASE WHEN t.typname IS NOT NULL THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_typed,
@@ -2497,14 +2494,11 @@ CREATE VIEW views AS
            CAST('NONE' AS character_data) AS check_option,
 
            CAST(
-             CASE WHEN EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '2' AND is_instead)
-                   AND EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '4' AND is_instead)
-                  THEN 'YES' ELSE 'NO' END
+             CASE WHEN pg_view_is_updatable(c.oid) THEN 'YES' ELSE 'NO' END
              AS yes_or_no) AS is_updatable,
 
            CAST(
-             CASE WHEN EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class = c.oid AND ev_type = '3' AND is_instead)
-                  THEN 'YES' ELSE 'NO' END
+             CASE WHEN pg_view_is_insertable(c.oid) THEN 'YES' ELSE 'NO' END
              AS yes_or_no) AS is_insertable_into,
 
            CAST(
index dbd3755b1b5e23db284a414b381349d889ae2a1b..0222d40b496d595a55064694dd5e9925633cfe8a 100644 (file)
@@ -923,9 +923,8 @@ InitPlan(QueryDesc *queryDesc, int eflags)
 /*
  * Check that a proposed result relation is a legal target for the operation
  *
- * In most cases parser and/or planner should have noticed this already, but
- * let's make sure.  In the view case we do need a test here, because if the
- * view wasn't rewritten by a rule, it had better have an INSTEAD trigger.
+ * Generally the parser and/or planner should have noticed any such mistake
+ * already, but let's make sure.
  *
  * Note: when changing this function, you probably also need to look at
  * CheckValidRowMarkRel.
@@ -953,6 +952,13 @@ CheckValidResultRel(Relation resultRel, CmdType operation)
                                                        RelationGetRelationName(resultRel))));
                        break;
                case RELKIND_VIEW:
+                       /*
+                        * Okay only if there's a suitable INSTEAD OF trigger.  Messages
+                        * here should match rewriteHandler.c's rewriteTargetView, except
+                        * that we omit errdetail because we haven't got the information
+                        * handy (and given that we really shouldn't get here anyway,
+                        * it's not worth great exertion to get).
+                        */
                        switch (operation)
                        {
                                case CMD_INSERT:
@@ -961,7 +967,7 @@ CheckValidResultRel(Relation resultRel, CmdType operation)
                                                  (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
                                                   errmsg("cannot insert into view \"%s\"",
                                                                  RelationGetRelationName(resultRel)),
-                                                  errhint("You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.")));
+                                                  errhint("To make the view insertable, provide an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.")));
                                        break;
                                case CMD_UPDATE:
                                        if (!trigDesc || !trigDesc->trig_update_instead_row)
@@ -969,7 +975,7 @@ CheckValidResultRel(Relation resultRel, CmdType operation)
                                                  (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
                                                   errmsg("cannot update view \"%s\"",
                                                                  RelationGetRelationName(resultRel)),
-                                                  errhint("You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.")));
+                                                  errhint("To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.")));
                                        break;
                                case CMD_DELETE:
                                        if (!trigDesc || !trigDesc->trig_delete_instead_row)
@@ -977,7 +983,7 @@ CheckValidResultRel(Relation resultRel, CmdType operation)
                                                  (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
                                                   errmsg("cannot delete from view \"%s\"",
                                                                  RelationGetRelationName(resultRel)),
-                                                  errhint("You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.")));
+                                                  errhint("To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.")));
                                        break;
                                default:
                                        elog(ERROR, "unrecognized CmdType: %d", (int) operation);
@@ -1028,7 +1034,7 @@ CheckValidRowMarkRel(Relation rel, RowMarkType markType)
                                                        RelationGetRelationName(rel))));
                        break;
                case RELKIND_VIEW:
-                       /* Should not get here */
+                       /* Should not get here; planner should have expanded the view */
                        ereport(ERROR,
                                        (errcode(ERRCODE_WRONG_OBJECT_TYPE),
                                         errmsg("cannot lock rows in view \"%s\"",
index b785c269a034c521b09d7d27c76537fe127d253c..990ca346816dcdcebc6c814588c8b59237d730a7 100644 (file)
@@ -1833,6 +1833,633 @@ fireRules(Query *parsetree,
 }
 
 
+/*
+ * get_view_query - get the Query from a view's _RETURN rule.
+ *
+ * Caller should have verified that the relation is a view, and therefore
+ * we should find an ON SELECT action.
+ */
+static Query *
+get_view_query(Relation view)
+{
+       int                     i;
+
+       Assert(view->rd_rel->relkind == RELKIND_VIEW);
+
+       for (i = 0; i < view->rd_rules->numLocks; i++)
+       {
+               RewriteRule *rule = view->rd_rules->rules[i];
+
+               if (rule->event == CMD_SELECT)
+               {
+                       /* A _RETURN rule should have only one action */
+                       if (list_length(rule->actions) != 1)
+                               elog(ERROR, "invalid _RETURN rule action specification");
+
+                       return (Query *) linitial(rule->actions);
+               }
+       }
+
+       elog(ERROR, "failed to find _RETURN rule for view");
+       return NULL;                            /* keep compiler quiet */
+}
+
+
+/*
+ * view_has_instead_trigger - does view have an INSTEAD OF trigger for event?
+ *
+ * If it does, we don't want to treat it as auto-updatable.  This test can't
+ * be folded into view_is_auto_updatable because it's not an error condition.
+ */
+static bool
+view_has_instead_trigger(Relation view, CmdType event)
+{
+       TriggerDesc *trigDesc = view->trigdesc;
+
+       switch (event)
+       {
+               case CMD_INSERT:
+                       if (trigDesc && trigDesc->trig_insert_instead_row)
+                               return true;
+                       break;
+               case CMD_UPDATE:
+                       if (trigDesc && trigDesc->trig_update_instead_row)
+                               return true;
+                       break;
+               case CMD_DELETE:
+                       if (trigDesc && trigDesc->trig_delete_instead_row)
+                               return true;
+                       break;
+               default:
+                       elog(ERROR, "unrecognized CmdType: %d", (int) event);
+                       break;
+       }
+       return false;
+}
+
+
+/*
+ * view_is_auto_updatable -
+ *       Test if the specified view can be automatically updated. This will
+ *       either return NULL (if the view can be updated) or a message string
+ *       giving the reason that it cannot be.
+ *
+ * Caller must have verified that relation is a view!
+ *
+ * Note that the checks performed here are local to this view. We do not
+ * check whether the view's underlying base relation is updatable; that
+ * will be dealt with in later, recursive processing.
+ *
+ * Also note that we don't check for INSTEAD triggers or rules here; those
+ * also prevent auto-update, but they must be checked for by the caller.
+ */
+static const char *
+view_is_auto_updatable(Relation view)
+{
+       Query      *viewquery = get_view_query(view);
+       RangeTblRef *rtr;
+       RangeTblEntry *base_rte;
+       Bitmapset  *bms;
+       ListCell   *cell;
+
+       /*----------
+        * Check if the view is simply updatable.  According to SQL-92 this means:
+        *      - No DISTINCT clause.
+        *      - Each TLE is a column reference, and each column appears at most once.
+        *      - FROM contains exactly one base relation.
+        *      - No GROUP BY or HAVING clauses.
+        *      - No set operations (UNION, INTERSECT or EXCEPT).
+        *      - No sub-queries in the WHERE clause that reference the target table.
+        *
+        * We ignore that last restriction since it would be complex to enforce
+        * and there isn't any actual benefit to disallowing sub-queries.  (The
+        * semantic issues that the standard is presumably concerned about don't
+        * arise in Postgres, since any such sub-query will not see any updates
+        * executed by the outer query anyway, thanks to MVCC snapshotting.)
+        *
+        * In addition we impose these constraints, involving features that are
+        * not part of SQL-92:
+        *      - No CTEs (WITH clauses).
+        *      - No OFFSET or LIMIT clauses (this matches a SQL:2008 restriction).
+        *      - No system columns (including whole-row references) in the tlist.
+        *
+        * Note that we do these checks without recursively expanding the view.
+        * If the base relation is a view, we'll recursively deal with it later.
+        *----------
+        */
+       if (viewquery->distinctClause != NIL)
+               return gettext_noop("Views containing DISTINCT are not automatically updatable.");
+
+       if (viewquery->groupClause != NIL)
+               return gettext_noop("Views containing GROUP BY are not automatically updatable.");
+
+       if (viewquery->havingQual != NULL)
+               return gettext_noop("Views containing HAVING are not automatically updatable.");
+
+       if (viewquery->setOperations != NULL)
+               return gettext_noop("Views containing UNION, INTERSECT or EXCEPT are not automatically updatable.");
+
+       if (viewquery->cteList != NIL)
+               return gettext_noop("Views containing WITH are not automatically updatable.");
+
+       if (viewquery->limitOffset != NULL || viewquery->limitCount != NULL)
+               return gettext_noop("Views containing LIMIT or OFFSET are not automatically updatable.");
+
+       /*
+        * For now, we also don't support security-barrier views, because of the
+        * difficulty of keeping upper-level qual expressions away from
+        * lower-level data.  This might get relaxed in future.
+        */
+       if (RelationIsSecurityView(view))
+               return gettext_noop("Security-barrier views are not automatically updatable.");
+
+       /*
+        * The view query should select from a single base relation, which must be
+        * a table or another view.
+        */
+       if (list_length(viewquery->jointree->fromlist) != 1)
+               return gettext_noop("Views that do not select from a single table or view are not automatically updatable.");
+
+       rtr = (RangeTblRef *) linitial(viewquery->jointree->fromlist);
+       if (!IsA(rtr, RangeTblRef))
+               return gettext_noop("Views that do not select from a single table or view are not automatically updatable.");
+
+       base_rte = rt_fetch(rtr->rtindex, viewquery->rtable);
+       if (base_rte->rtekind != RTE_RELATION ||
+               (base_rte->relkind != RELKIND_RELATION &&
+                base_rte->relkind != RELKIND_VIEW))
+               return gettext_noop("Views that do not select from a single table or view are not automatically updatable.");
+
+       /*
+        * The view's targetlist entries should all be Vars referring to user
+        * columns of the base relation, and no two should refer to the same
+        * column.
+        *
+        * Note however that we should ignore resjunk entries.  This proviso is
+        * relevant because ORDER BY is not disallowed, and we shouldn't reject a
+        * view defined like "SELECT * FROM t ORDER BY a+b".
+        */
+       bms = NULL;
+       foreach(cell, viewquery->targetList)
+       {
+               TargetEntry *tle = (TargetEntry *) lfirst(cell);
+               Var                *var = (Var *) tle->expr;
+
+               if (tle->resjunk)
+                       continue;
+
+               if (!IsA(var, Var) ||
+                       var->varno != rtr->rtindex ||
+                       var->varlevelsup != 0)
+                       return gettext_noop("Views that return columns that are not columns of their base relation are not automatically updatable.");
+
+               if (var->varattno < 0)
+                       return gettext_noop("Views that return system columns are not automatically updatable.");
+
+               if (var->varattno == 0)
+                       return gettext_noop("Views that return whole-row references are not automatically updatable.");
+
+               if (bms_is_member(var->varattno, bms))
+                       return gettext_noop("Views that return the same column more than once are not automatically updatable.");
+
+               bms = bms_add_member(bms, var->varattno);
+       }
+       bms_free(bms);                          /* just for cleanliness */
+
+       return NULL;                            /* the view is simply updatable */
+}
+
+
+/*
+ * relation_is_updatable - test if the specified relation is updatable.
+ *
+ * This is used for the information_schema views, which have separate concepts
+ * of "updatable" and "trigger updatable".     A relation is "updatable" if it
+ * can be updated without the need for triggers (either because it has a
+ * suitable RULE, or because it is simple enough to be automatically updated).
+ *
+ * A relation is "trigger updatable" if it has a suitable INSTEAD OF trigger.
+ * The SQL standard regards this as not necessarily updatable, presumably
+ * because there is no way of knowing what the trigger will actually do.
+ * That's currently handled directly in the information_schema views, so
+ * need not be considered here.
+ *
+ * In the case of an automatically updatable view, the base relation must
+ * also be updatable.
+ *
+ * reloid is the pg_class OID to examine.  req_events is a bitmask of
+ * rule event numbers; the relation is considered rule-updatable if it has
+ * all the specified rules.  (We do it this way so that we can test for
+ * UPDATE plus DELETE rules in a single call.)
+ */
+bool
+relation_is_updatable(Oid reloid, int req_events)
+{
+       Relation        rel;
+       RuleLock   *rulelocks;
+
+       rel = try_relation_open(reloid, AccessShareLock);
+
+       /*
+        * If the relation doesn't exist, say "false" rather than throwing an
+        * error.  This is helpful since scanning an information_schema view
+        * under MVCC rules can result in referencing rels that were just
+        * deleted according to a SnapshotNow probe.
+        */
+       if (rel == NULL)
+               return false;
+
+       /* Look for unconditional DO INSTEAD rules, and note supported events */
+       rulelocks = rel->rd_rules;
+       if (rulelocks != NULL)
+       {
+               int                     events = 0;
+               int                     i;
+
+               for (i = 0; i < rulelocks->numLocks; i++)
+               {
+                       if (rulelocks->rules[i]->isInstead &&
+                               rulelocks->rules[i]->qual == NULL)
+                       {
+                               events |= 1 << rulelocks->rules[i]->event;
+                       }
+               }
+
+               /* If we have all rules needed, say "yes" */
+               if ((events & req_events) == req_events)
+               {
+                       relation_close(rel, AccessShareLock);
+                       return true;
+               }
+       }
+
+       /* Check if this is an automatically updatable view */
+       if (rel->rd_rel->relkind == RELKIND_VIEW &&
+               view_is_auto_updatable(rel) == NULL)
+       {
+               Query      *viewquery;
+               RangeTblRef *rtr;
+               RangeTblEntry *base_rte;
+               Oid                     baseoid;
+
+               /* The base relation must also be updatable */
+               viewquery = get_view_query(rel);
+               rtr = (RangeTblRef *) linitial(viewquery->jointree->fromlist);
+               base_rte = rt_fetch(rtr->rtindex, viewquery->rtable);
+
+               if (base_rte->relkind == RELKIND_RELATION)
+               {
+                       /* Tables are always updatable */
+                       relation_close(rel, AccessShareLock);
+                       return true;
+               }
+               else
+               {
+                       /* Do a recursive check for any other kind of base relation */
+                       baseoid = base_rte->relid;
+                       relation_close(rel, AccessShareLock);
+                       return relation_is_updatable(baseoid, req_events);
+               }
+       }
+
+       /* If we reach here, the relation is not updatable */
+       relation_close(rel, AccessShareLock);
+       return false;
+}
+
+
+/*
+ * adjust_view_column_set - map a set of column numbers according to targetlist
+ *
+ * This is used with simply-updatable views to map column-permissions sets for
+ * the view columns onto the matching columns in the underlying base relation.
+ * The targetlist is expected to be a list of plain Vars of the underlying
+ * relation (as per the checks above in view_is_auto_updatable).
+ */
+static Bitmapset *
+adjust_view_column_set(Bitmapset *cols, List *targetlist)
+{
+       Bitmapset  *result = NULL;
+       Bitmapset  *tmpcols;
+       AttrNumber      col;
+
+       tmpcols = bms_copy(cols);
+       while ((col = bms_first_member(tmpcols)) >= 0)
+       {
+               /* bit numbers are offset by FirstLowInvalidHeapAttributeNumber */
+               AttrNumber      attno = col + FirstLowInvalidHeapAttributeNumber;
+
+               if (attno == InvalidAttrNumber)
+               {
+                       /*
+                        * There's a whole-row reference to the view.  For permissions
+                        * purposes, treat it as a reference to each column available from
+                        * the view.  (We should *not* convert this to a whole-row
+                        * reference to the base relation, since the view may not touch
+                        * all columns of the base relation.)
+                        */
+                       ListCell   *lc;
+
+                       foreach(lc, targetlist)
+                       {
+                               TargetEntry *tle = (TargetEntry *) lfirst(lc);
+                               Var                *var;
+
+                               if (tle->resjunk)
+                                       continue;
+                               var = (Var *) tle->expr;
+                               Assert(IsA(var, Var));
+                               result = bms_add_member(result,
+                                                var->varattno - FirstLowInvalidHeapAttributeNumber);
+                       }
+               }
+               else
+               {
+                       /*
+                        * Views do not have system columns, so we do not expect to see
+                        * any other system attnos here.  If we do find one, the error
+                        * case will apply.
+                        */
+                       TargetEntry *tle = get_tle_by_resno(targetlist, attno);
+
+                       if (tle != NULL && !tle->resjunk && IsA(tle->expr, Var))
+                       {
+                               Var                *var = (Var *) tle->expr;
+
+                               result = bms_add_member(result,
+                                                var->varattno - FirstLowInvalidHeapAttributeNumber);
+                       }
+                       else
+                               elog(ERROR, "attribute number %d not found in view targetlist",
+                                        attno);
+               }
+       }
+       bms_free(tmpcols);
+
+       return result;
+}
+
+
+/*
+ * rewriteTargetView -
+ *       Attempt to rewrite a query where the target relation is a view, so that
+ *       the view's base relation becomes the target relation.
+ *
+ * Note that the base relation here may itself be a view, which may or may not
+ * have INSTEAD OF triggers or rules to handle the update.     That is handled by
+ * the recursion in RewriteQuery.
+ */
+static Query *
+rewriteTargetView(Query *parsetree, Relation view)
+{
+       const char *auto_update_detail;
+       Query      *viewquery;
+       RangeTblRef *rtr;
+       int                     base_rt_index;
+       int                     new_rt_index;
+       RangeTblEntry *base_rte;
+       RangeTblEntry *view_rte;
+       RangeTblEntry *new_rte;
+       Relation        base_rel;
+       List       *view_targetlist;
+       ListCell   *lc;
+
+       /* The view must be simply updatable, else fail */
+       auto_update_detail = view_is_auto_updatable(view);
+       if (auto_update_detail)
+       {
+               /* messages here should match execMain.c's CheckValidResultRel */
+               switch (parsetree->commandType)
+               {
+                       case CMD_INSERT:
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                                                errmsg("cannot insert into view \"%s\"",
+                                                               RelationGetRelationName(view)),
+                                                errdetail_internal("%s", _(auto_update_detail)),
+                                                errhint("To make the view insertable, provide an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.")));
+                               break;
+                       case CMD_UPDATE:
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                                                errmsg("cannot update view \"%s\"",
+                                                               RelationGetRelationName(view)),
+                                                errdetail_internal("%s", _(auto_update_detail)),
+                                                errhint("To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.")));
+                               break;
+                       case CMD_DELETE:
+                               ereport(ERROR,
+                                               (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                                                errmsg("cannot delete from view \"%s\"",
+                                                               RelationGetRelationName(view)),
+                                                errdetail_internal("%s", _(auto_update_detail)),
+                                                errhint("To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.")));
+                               break;
+                       default:
+                               elog(ERROR, "unrecognized CmdType: %d",
+                                        (int) parsetree->commandType);
+                               break;
+               }
+       }
+
+       /* Locate RTE describing the view in the outer query */
+       view_rte = rt_fetch(parsetree->resultRelation, parsetree->rtable);
+
+       /*
+        * If we get here, view_is_auto_updatable() has verified that the view
+        * contains a single base relation.
+        */
+       viewquery = get_view_query(view);
+
+       Assert(list_length(viewquery->jointree->fromlist) == 1);
+       rtr = (RangeTblRef *) linitial(viewquery->jointree->fromlist);
+       Assert(IsA(rtr, RangeTblRef));
+
+       base_rt_index = rtr->rtindex;
+       base_rte = rt_fetch(base_rt_index, viewquery->rtable);
+       Assert(base_rte->rtekind == RTE_RELATION);
+
+       /*
+        * Up to now, the base relation hasn't been touched at all in our query.
+        * We need to acquire lock on it before we try to do anything with it.
+        * (The subsequent recursive call of RewriteQuery will suppose that we
+        * already have the right lock!)  Since it will become the query target
+        * relation, RowExclusiveLock is always the right thing.
+        */
+       base_rel = heap_open(base_rte->relid, RowExclusiveLock);
+
+       /*
+        * While we have the relation open, update the RTE's relkind, just in case
+        * it changed since this view was made (cf. AcquireRewriteLocks).
+        */
+       base_rte->relkind = base_rel->rd_rel->relkind;
+
+       heap_close(base_rel, NoLock);
+
+       /*
+        * Create a new target RTE describing the base relation, and add it to the
+        * outer query's rangetable.  (What's happening in the next few steps is
+        * very much like what the planner would do to "pull up" the view into the
+        * outer query.  Perhaps someday we should refactor things enough so that
+        * we can share code with the planner.)
+        */
+       new_rte = (RangeTblEntry *) copyObject(base_rte);
+       parsetree->rtable = lappend(parsetree->rtable, new_rte);
+       new_rt_index = list_length(parsetree->rtable);
+
+       /*
+        * Make a copy of the view's targetlist, adjusting its Vars to reference
+        * the new target RTE, ie make their varnos be new_rt_index instead of
+        * base_rt_index.  There can be no Vars for other rels in the tlist, so
+        * this is sufficient to pull up the tlist expressions for use in the
+        * outer query.  The tlist will provide the replacement expressions used
+        * by ReplaceVarsFromTargetList below.
+        */
+       view_targetlist = copyObject(viewquery->targetList);
+
+       ChangeVarNodes((Node *) view_targetlist,
+                                  base_rt_index,
+                                  new_rt_index,
+                                  0);
+
+       /*
+        * Mark the new target RTE for the permissions checks that we want to
+        * enforce against the view owner, as distinct from the query caller.  At
+        * the relation level, require the same INSERT/UPDATE/DELETE permissions
+        * that the query caller needs against the view.  We drop the ACL_SELECT
+        * bit that is presumably in new_rte->requiredPerms initially.
+        *
+        * Note: the original view RTE remains in the query's rangetable list.
+        * Although it will be unused in the query plan, we need it there so that
+        * the executor still performs appropriate permissions checks for the
+        * query caller's use of the view.
+        */
+       new_rte->checkAsUser = view->rd_rel->relowner;
+       new_rte->requiredPerms = view_rte->requiredPerms;
+
+       /*
+        * Now for the per-column permissions bits.
+        *
+        * Initially, new_rte contains selectedCols permission check bits for all
+        * base-rel columns referenced by the view, but since the view is a SELECT
+        * query its modifiedCols is empty.  We set modifiedCols to include all
+        * the columns the outer query is trying to modify, adjusting the column
+        * numbers as needed.  But we leave selectedCols as-is, so the view owner
+        * must have read permission for all columns used in the view definition,
+        * even if some of them are not read by the outer query.  We could try to
+        * limit selectedCols to only columns used in the transformed query, but
+        * that does not correspond to what happens in ordinary SELECT usage of a
+        * view: all referenced columns must have read permission, even if
+        * optimization finds that some of them can be discarded during query
+        * transformation.  The flattening we're doing here is an optional
+        * optimization, too.  (If you are unpersuaded and want to change this,
+        * note that applying adjust_view_column_set to view_rte->selectedCols is
+        * clearly *not* the right answer, since that neglects base-rel columns
+        * used in the view's WHERE quals.)
+        *
+        * This step needs the modified view targetlist, so we have to do things
+        * in this order.
+        */
+       Assert(bms_is_empty(new_rte->modifiedCols));
+       new_rte->modifiedCols = adjust_view_column_set(view_rte->modifiedCols,
+                                                                                                  view_targetlist);
+
+       /*
+        * For UPDATE/DELETE, rewriteTargetListUD will have added a wholerow junk
+        * TLE for the view to the end of the targetlist, which we no longer need.
+        * Remove it to avoid unnecessary work when we process the targetlist.
+        * Note that when we recurse through rewriteQuery a new junk TLE will be
+        * added to allow the executor to find the proper row in the new target
+        * relation.  (So, if we failed to do this, we might have multiple junk
+        * TLEs with the same name, which would be disastrous.)
+        */
+       if (parsetree->commandType != CMD_INSERT)
+       {
+               TargetEntry *tle = (TargetEntry *) llast(parsetree->targetList);
+
+               Assert(tle->resjunk);
+               Assert(IsA(tle->expr, Var) &&
+                          ((Var *) tle->expr)->varno == parsetree->resultRelation &&
+                          ((Var *) tle->expr)->varattno == 0);
+               parsetree->targetList = list_delete_ptr(parsetree->targetList, tle);
+       }
+
+       /*
+        * Now update all Vars in the outer query that reference the view to
+        * reference the appropriate column of the base relation instead.
+        */
+       parsetree = (Query *)
+               ReplaceVarsFromTargetList((Node *) parsetree,
+                                                                 parsetree->resultRelation,
+                                                                 0,
+                                                                 view_rte,
+                                                                 view_targetlist,
+                                                                 REPLACEVARS_REPORT_ERROR,
+                                                                 0,
+                                                                 &parsetree->hasSubLinks);
+
+       /*
+        * Update all other RTI references in the query that point to the view
+        * (for example, parsetree->resultRelation itself) to point to the new
+        * base relation instead.  Vars will not be affected since none of them
+        * reference parsetree->resultRelation any longer.
+        */
+       ChangeVarNodes((Node *) parsetree,
+                                  parsetree->resultRelation,
+                                  new_rt_index,
+                                  0);
+       Assert(parsetree->resultRelation == new_rt_index);
+
+       /*
+        * For INSERT/UPDATE we must also update resnos in the targetlist to refer
+        * to columns of the base relation, since those indicate the target
+        * columns to be affected.
+        *
+        * Note that this destroys the resno ordering of the targetlist, but that
+        * will be fixed when we recurse through rewriteQuery, which will invoke
+        * rewriteTargetListIU again on the updated targetlist.
+        */
+       if (parsetree->commandType != CMD_DELETE)
+       {
+               foreach(lc, parsetree->targetList)
+               {
+                       TargetEntry *tle = (TargetEntry *) lfirst(lc);
+                       TargetEntry *view_tle;
+
+                       if (tle->resjunk)
+                               continue;
+
+                       view_tle = get_tle_by_resno(view_targetlist, tle->resno);
+                       if (view_tle != NULL && !view_tle->resjunk && IsA(view_tle->expr, Var))
+                               tle->resno = ((Var *) view_tle->expr)->varattno;
+                       else
+                               elog(ERROR, "attribute number %d not found in view targetlist",
+                                        tle->resno);
+               }
+       }
+
+       /*
+        * For UPDATE/DELETE, pull up any WHERE quals from the view.  We know that
+        * any Vars in the quals must reference the one base relation, so we need
+        * only adjust their varnos to reference the new target (just the same as
+        * we did with the view targetlist).
+        *
+        * For INSERT, the view's quals can be ignored for now.  When we implement
+        * WITH CHECK OPTION, this might be a good place to collect them.
+        */
+       if (parsetree->commandType != CMD_INSERT &&
+               viewquery->jointree->quals != NULL)
+       {
+               Node       *viewqual = (Node *) copyObject(viewquery->jointree->quals);
+
+               ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
+               AddQual(parsetree, (Node *) viewqual);
+       }
+
+       return parsetree;
+}
+
+
 /*
  * RewriteQuery -
  *       rewrites the query and apply the rules again on the queries rewritten
@@ -1927,6 +2554,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
                RangeTblEntry *rt_entry;
                Relation        rt_entry_relation;
                List       *locks;
+               List       *product_queries;
 
                result_relation = parsetree->resultRelation;
                Assert(result_relation != 0);
@@ -1997,17 +2625,54 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
                locks = matchLocks(event, rt_entry_relation->rd_rules,
                                                   result_relation, parsetree);
 
-               if (locks != NIL)
+               product_queries = fireRules(parsetree,
+                                                                       result_relation,
+                                                                       event,
+                                                                       locks,
+                                                                       &instead,
+                                                                       &returning,
+                                                                       &qual_product);
+
+               /*
+                * If there were no INSTEAD rules, and the target relation is a view
+                * without any INSTEAD OF triggers, see if the view can be
+                * automatically updated.  If so, we perform the necessary query
+                * transformation here and add the resulting query to the
+                * product_queries list, so that it gets recursively rewritten if
+                * necessary.
+                */
+               if (!instead && qual_product == NULL &&
+                       rt_entry_relation->rd_rel->relkind == RELKIND_VIEW &&
+                       !view_has_instead_trigger(rt_entry_relation, event))
                {
-                       List       *product_queries;
+                       /*
+                        * This throws an error if the view can't be automatically
+                        * updated, but that's OK since the query would fail at runtime
+                        * anyway.
+                        */
+                       parsetree = rewriteTargetView(parsetree, rt_entry_relation);
 
-                       product_queries = fireRules(parsetree,
-                                                                               result_relation,
-                                                                               event,
-                                                                               locks,
-                                                                               &instead,
-                                                                               &returning,
-                                                                               &qual_product);
+                       /*
+                        * At this point product_queries contains any DO ALSO rule actions.
+                        * Add the rewritten query before or after those.  This must match
+                        * the handling the original query would have gotten below, if
+                        * we allowed it to be included again.
+                        */
+                       if (parsetree->commandType == CMD_INSERT)
+                               product_queries = lcons(parsetree, product_queries);
+                       else
+                               product_queries = lappend(product_queries, parsetree);
+
+                       /*
+                        * Set the "instead" flag, as if there had been an unqualified
+                        * INSTEAD, to prevent the original query from being included a
+                        * second time below.  The transformation will have rewritten any
+                        * RETURNING list, so we can also set "returning" to forestall
+                        * throwing an error below.
+                        */
+                       instead = true;
+                       returning = true;
+               }
 
                        /*
                         * If we got any product queries, recursively rewrite them --- but
@@ -2045,7 +2710,6 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
 
                                rewrite_events = list_delete_first(rewrite_events);
                        }
-               }
 
                /*
                 * If there is an INSTEAD, and the original query has a RETURNING, we
index cd20b838416834461a46ca5829a949c7bba50d6b..407946715e25576d639a28559c2050d8cf81e3e4 100644 (file)
@@ -28,6 +28,7 @@
 #include "miscadmin.h"
 #include "parser/keywords.h"
 #include "postmaster/syslogger.h"
+#include "rewrite/rewriteHandler.h"
 #include "storage/fd.h"
 #include "storage/pmsignal.h"
 #include "storage/proc.h"
@@ -523,3 +524,33 @@ pg_collation_for(PG_FUNCTION_ARGS)
                PG_RETURN_NULL();
        PG_RETURN_TEXT_P(cstring_to_text(generate_collation_name(collid)));
 }
+
+
+/*
+ * information_schema support functions
+ *
+ * Test whether a view (identified by pg_class OID) is insertable-into or
+ * updatable.  The latter requires delete capability too.  This is an
+ * artifact of the way the SQL standard defines the information_schema views:
+ * if we defined separate functions for update and delete, we'd double the
+ * work required to compute the view columns.
+ *
+ * These rely on relation_is_updatable(), which is in rewriteHandler.c.
+ */
+Datum
+pg_view_is_insertable(PG_FUNCTION_ARGS)
+{
+       Oid                     viewoid = PG_GETARG_OID(0);
+       int                     req_events = (1 << CMD_INSERT);
+
+       PG_RETURN_BOOL(relation_is_updatable(viewoid, req_events));
+}
+
+Datum
+pg_view_is_updatable(PG_FUNCTION_ARGS)
+{
+       Oid                     viewoid = PG_GETARG_OID(0);
+       int                     req_events = (1 << CMD_UPDATE) | (1 << CMD_DELETE);
+
+       PG_RETURN_BOOL(relation_is_updatable(viewoid, req_events));
+}
index 9622356a6376e85fc2ccbf16afc0d1e9e68ab28f..e98a225fbca9a4c2bd58fa497d884ad402666a9e 100644 (file)
@@ -53,6 +53,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     201211281
+#define CATALOG_VERSION_NO     201212081
 
 #endif
index f935eb1df825e70b1e6f02b660604a076fdaaf2b..d1b22d172daa40b2dda864394936e8676ab90522 100644 (file)
@@ -1976,6 +1976,11 @@ DESCR("type of the argument");
 DATA(insert OID = 3162 (  pg_collation_for             PGNSP PGUID 12 1 0 0 0 f f f f f f s 1 0   25 "2276" _null_ _null_ _null_ _null_  pg_collation_for _null_ _null_ _null_ ));
 DESCR("collation of the argument; implementation of the COLLATION FOR expression");
 
+DATA(insert OID = 3842 (  pg_view_is_insertable PGNSP PGUID 12 10 0 0 0 f f f f t f s 1 0 16 "26" _null_ _null_ _null_ _null_ pg_view_is_insertable _null_ _null_ _null_ ));
+DESCR("is a view insertable-into");
+DATA(insert OID = 3843 (  pg_view_is_updatable PGNSP PGUID 12 10 0 0 0 f f f f t f s 1 0 16 "26" _null_ _null_ _null_ _null_ pg_view_is_updatable _null_ _null_ _null_ ));
+DESCR("is a view updatable");
+
 /* Deferrable unique constraint trigger */
 DATA(insert OID = 1250 (  unique_key_recheck   PGNSP PGUID 12 1 0 0 0 f f f f t f v 0 0 2279 "" _null_ _null_ _null_ _null_ unique_key_recheck _null_ _null_ _null_ ));
 DESCR("deferred UNIQUE constraint check");
index 50625d4c371960937a30916f931151b4f83297d1..3540e1b0348880430a5815b794f28ec29fa3d95b 100644 (file)
@@ -19,6 +19,8 @@
 
 extern List *QueryRewrite(Query *parsetree);
 extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
+
 extern Node *build_column_default(Relation rel, int attrno);
+extern bool relation_is_updatable(Oid reloid, int req_events);
 
 #endif   /* REWRITEHANDLER_H */
index 5bc3a75856d8efe53ed50064ae71d15711b2beaf..ad82dcc209397e2af16c2ba6a355b5fdef3124a1 100644 (file)
@@ -482,6 +482,8 @@ extern Datum pg_sleep(PG_FUNCTION_ARGS);
 extern Datum pg_get_keywords(PG_FUNCTION_ARGS);
 extern Datum pg_typeof(PG_FUNCTION_ARGS);
 extern Datum pg_collation_for(PG_FUNCTION_ARGS);
+extern Datum pg_view_is_insertable(PG_FUNCTION_ARGS);
+extern Datum pg_view_is_updatable(PG_FUNCTION_ARGS);
 
 /* oid.c */
 extern Datum oidin(PG_FUNCTION_ARGS);
index 94ea61f80c74ac2c915e9d83140d13582fc1b35e..5140575f2a101d7cf9b75a34932818c08767baa0 100644 (file)
@@ -820,20 +820,6 @@ DROP TABLE min_updates_test_oids;
 -- Test triggers on views
 --
 CREATE VIEW main_view AS SELECT a, b FROM main_table;
--- Updates should fail without rules or triggers
-INSERT INTO main_view VALUES (1,2);
-ERROR:  cannot insert into view "main_view"
-HINT:  You need an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
-UPDATE main_view SET b = 20 WHERE a = 50;
-ERROR:  cannot update view "main_view"
-HINT:  You need an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
-DELETE FROM main_view WHERE a = 50;
-ERROR:  cannot delete from view "main_view"
-HINT:  You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
--- Should fail even when there are no matching rows
-DELETE FROM main_view WHERE a = 51;
-ERROR:  cannot delete from view "main_view"
-HINT:  You need an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
 -- VIEW trigger function
 CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
 LANGUAGE plpgsql AS $$
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644 (file)
index 0000000..ead08d6
--- /dev/null
@@ -0,0 +1,1069 @@
+--
+-- UPDATABLE VIEWS
+--
+-- check that non-updatable views are rejected with useful error messages
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+CREATE VIEW ro_view1 AS SELECT DISTINCT a, b FROM base_tbl; -- DISTINCT not supported
+CREATE VIEW ro_view2 AS SELECT a, b FROM base_tbl GROUP BY a, b; -- GROUP BY not supported
+CREATE VIEW ro_view3 AS SELECT 1 FROM base_tbl HAVING max(a) > 0; -- HAVING not supported
+CREATE VIEW ro_view4 AS SELECT count(*) FROM base_tbl; -- Aggregate functions not supported
+CREATE VIEW ro_view5 AS SELECT a, rank() OVER() FROM base_tbl; -- Window functions not supported
+CREATE VIEW ro_view6 AS SELECT a, b FROM base_tbl UNION SELECT -a, b FROM base_tbl; -- Set ops not supported
+CREATE VIEW ro_view7 AS WITH t AS (SELECT a, b FROM base_tbl) SELECT * FROM t; -- WITH not supported
+CREATE VIEW ro_view8 AS SELECT a, b FROM base_tbl ORDER BY a OFFSET 1; -- OFFSET not supported
+CREATE VIEW ro_view9 AS SELECT a, b FROM base_tbl ORDER BY a LIMIT 1; -- LIMIT not supported
+CREATE VIEW ro_view10 AS SELECT 1 AS a; -- No base relations
+CREATE VIEW ro_view11 AS SELECT b1.a, b2.b FROM base_tbl b1, base_tbl b2; -- Multiple base relations
+CREATE VIEW ro_view12 AS SELECT * FROM generate_series(1, 10) AS g(a); -- SRF in rangetable
+CREATE VIEW ro_view13 AS SELECT a, b FROM (SELECT * FROM base_tbl) AS t; -- Subselect in rangetable
+CREATE VIEW ro_view14 AS SELECT ctid FROM base_tbl; -- System columns not supported
+CREATE VIEW ro_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function in targetlist
+CREATE VIEW ro_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column
+CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
+CREATE VIEW ro_view18 WITH (security_barrier = true)
+  AS SELECT * FROM base_tbl; -- Security barrier views not updatable
+CREATE VIEW ro_view19 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
+CREATE SEQUENCE seq;
+CREATE VIEW ro_view20 AS SELECT * FROM seq; -- View based on a sequence
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'ro_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ ro_view1   | NO
+ ro_view10  | NO
+ ro_view11  | NO
+ ro_view12  | NO
+ ro_view13  | NO
+ ro_view14  | NO
+ ro_view15  | NO
+ ro_view16  | NO
+ ro_view17  | NO
+ ro_view18  | NO
+ ro_view19  | NO
+ ro_view2   | NO
+ ro_view20  | NO
+ ro_view3   | NO
+ ro_view4   | NO
+ ro_view5   | NO
+ ro_view6   | NO
+ ro_view7   | NO
+ ro_view8   | NO
+ ro_view9   | NO
+(20 rows)
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'ro_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into 
+------------+--------------+--------------------
+ ro_view1   | NO           | NO
+ ro_view10  | NO           | NO
+ ro_view11  | NO           | NO
+ ro_view12  | NO           | NO
+ ro_view13  | NO           | NO
+ ro_view14  | NO           | NO
+ ro_view15  | NO           | NO
+ ro_view16  | NO           | NO
+ ro_view17  | NO           | NO
+ ro_view18  | NO           | NO
+ ro_view19  | NO           | NO
+ ro_view2   | NO           | NO
+ ro_view20  | NO           | NO
+ ro_view3   | NO           | NO
+ ro_view4   | NO           | NO
+ ro_view5   | NO           | NO
+ ro_view6   | NO           | NO
+ ro_view7   | NO           | NO
+ ro_view8   | NO           | NO
+ ro_view9   | NO           | NO
+(20 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'ro_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name |  column_name  | is_updatable 
+------------+---------------+--------------
+ ro_view1   | a             | NO
+ ro_view1   | b             | NO
+ ro_view10  | a             | NO
+ ro_view11  | a             | NO
+ ro_view11  | b             | NO
+ ro_view12  | a             | NO
+ ro_view13  | a             | NO
+ ro_view13  | b             | NO
+ ro_view14  | ctid          | NO
+ ro_view15  | a             | NO
+ ro_view15  | upper         | NO
+ ro_view16  | a             | NO
+ ro_view16  | b             | NO
+ ro_view16  | aa            | NO
+ ro_view17  | a             | NO
+ ro_view17  | b             | NO
+ ro_view18  | a             | NO
+ ro_view18  | b             | NO
+ ro_view19  | a             | NO
+ ro_view2   | a             | NO
+ ro_view2   | b             | NO
+ ro_view20  | sequence_name | NO
+ ro_view20  | last_value    | NO
+ ro_view20  | start_value   | NO
+ ro_view20  | increment_by  | NO
+ ro_view20  | max_value     | NO
+ ro_view20  | min_value     | NO
+ ro_view20  | cache_value   | NO
+ ro_view20  | log_cnt       | NO
+ ro_view20  | is_cycled     | NO
+ ro_view20  | is_called     | NO
+ ro_view3   | ?column?      | NO
+ ro_view4   | count         | NO
+ ro_view5   | a             | NO
+ ro_view5   | rank          | NO
+ ro_view6   | a             | NO
+ ro_view6   | b             | NO
+ ro_view7   | a             | NO
+ ro_view7   | b             | NO
+ ro_view8   | a             | NO
+ ro_view8   | b             | NO
+ ro_view9   | a             | NO
+ ro_view9   | b             | NO
+(43 rows)
+
+DELETE FROM ro_view1;
+ERROR:  cannot delete from view "ro_view1"
+DETAIL:  Views containing DISTINCT are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+DELETE FROM ro_view2;
+ERROR:  cannot delete from view "ro_view2"
+DETAIL:  Views containing GROUP BY are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+DELETE FROM ro_view3;
+ERROR:  cannot delete from view "ro_view3"
+DETAIL:  Views containing HAVING are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+DELETE FROM ro_view4;
+ERROR:  cannot delete from view "ro_view4"
+DETAIL:  Views that return columns that are not columns of their base relation are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+DELETE FROM ro_view5;
+ERROR:  cannot delete from view "ro_view5"
+DETAIL:  Views that return columns that are not columns of their base relation are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+DELETE FROM ro_view6;
+ERROR:  cannot delete from view "ro_view6"
+DETAIL:  Views containing UNION, INTERSECT or EXCEPT are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+UPDATE ro_view7 SET a=a+1;
+ERROR:  cannot update view "ro_view7"
+DETAIL:  Views containing WITH are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+UPDATE ro_view8 SET a=a+1;
+ERROR:  cannot update view "ro_view8"
+DETAIL:  Views containing LIMIT or OFFSET are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+UPDATE ro_view9 SET a=a+1;
+ERROR:  cannot update view "ro_view9"
+DETAIL:  Views containing LIMIT or OFFSET are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+UPDATE ro_view10 SET a=a+1;
+ERROR:  cannot update view "ro_view10"
+DETAIL:  Views that do not select from a single table or view are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+UPDATE ro_view11 SET a=a+1;
+ERROR:  cannot update view "ro_view11"
+DETAIL:  Views that do not select from a single table or view are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+UPDATE ro_view12 SET a=a+1;
+ERROR:  cannot update view "ro_view12"
+DETAIL:  Views that do not select from a single table or view are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+INSERT INTO ro_view13 VALUES (3, 'Row 3');
+ERROR:  cannot insert into view "ro_view13"
+DETAIL:  Views that do not select from a single table or view are not automatically updatable.
+HINT:  To make the view insertable, provide an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+INSERT INTO ro_view14 VALUES (null);
+ERROR:  cannot insert into view "ro_view14"
+DETAIL:  Views that return system columns are not automatically updatable.
+HINT:  To make the view insertable, provide an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+INSERT INTO ro_view15 VALUES (3, 'ROW 3');
+ERROR:  cannot insert into view "ro_view15"
+DETAIL:  Views that return columns that are not columns of their base relation are not automatically updatable.
+HINT:  To make the view insertable, provide an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+INSERT INTO ro_view16 VALUES (3, 'Row 3', 3);
+ERROR:  cannot insert into view "ro_view16"
+DETAIL:  Views that return the same column more than once are not automatically updatable.
+HINT:  To make the view insertable, provide an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+INSERT INTO ro_view17 VALUES (3, 'ROW 3');
+ERROR:  cannot insert into view "ro_view1"
+DETAIL:  Views containing DISTINCT are not automatically updatable.
+HINT:  To make the view insertable, provide an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+INSERT INTO ro_view18 VALUES (3, 'ROW 3');
+ERROR:  cannot insert into view "ro_view18"
+DETAIL:  Security-barrier views are not automatically updatable.
+HINT:  To make the view insertable, provide an unconditional ON INSERT DO INSTEAD rule or an INSTEAD OF INSERT trigger.
+DELETE FROM ro_view19;
+ERROR:  cannot delete from view "ro_view19"
+DETAIL:  Views that do not select from a single table or view are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON DELETE DO INSTEAD rule or an INSTEAD OF DELETE trigger.
+UPDATE ro_view20 SET max_value=1000;
+ERROR:  cannot update view "ro_view20"
+DETAIL:  Views that do not select from a single table or view are not automatically updatable.
+HINT:  To make the view updatable, provide an unconditional ON UPDATE DO INSTEAD rule or an INSTEAD OF UPDATE trigger.
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to 16 other objects
+DETAIL:  drop cascades to view ro_view1
+drop cascades to view ro_view17
+drop cascades to view ro_view2
+drop cascades to view ro_view3
+drop cascades to view ro_view5
+drop cascades to view ro_view6
+drop cascades to view ro_view7
+drop cascades to view ro_view8
+drop cascades to view ro_view9
+drop cascades to view ro_view11
+drop cascades to view ro_view13
+drop cascades to view ro_view15
+drop cascades to view ro_view16
+drop cascades to view ro_view18
+drop cascades to view ro_view4
+drop cascades to view ro_view14
+DROP VIEW ro_view10, ro_view12, ro_view19;
+DROP SEQUENCE seq CASCADE;
+NOTICE:  drop cascades to view ro_view20
+-- simple updatable view
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0;
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name = 'rw_view1';
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | YES
+(1 row)
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name = 'rw_view1';
+ table_name | is_updatable | is_insertable_into 
+------------+--------------+--------------------
+ rw_view1   | YES          | YES
+(1 row)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name = 'rw_view1'
+ ORDER BY ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | YES
+ rw_view1   | b           | YES
+(2 rows)
+
+INSERT INTO rw_view1 VALUES (3, 'Row 3');
+INSERT INTO rw_view1 (a) VALUES (4);
+UPDATE rw_view1 SET a=5 WHERE a=4;
+DELETE FROM rw_view1 WHERE b='Row 2';
+SELECT * FROM base_tbl;
+ a  |      b      
+----+-------------
+ -2 | Row -2
+ -1 | Row -1
+  0 | Row 0
+  1 | Row 1
+  3 | Row 3
+  5 | Unspecified
+(6 rows)
+
+EXPLAIN (costs off) UPDATE rw_view1 SET a=6 WHERE a=5;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Update on base_tbl
+   ->  Index Scan using base_tbl_pkey on base_tbl
+         Index Cond: ((a > 0) AND (a = 5))
+(3 rows)
+
+EXPLAIN (costs off) DELETE FROM rw_view1 WHERE a=5;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Delete on base_tbl
+   ->  Index Scan using base_tbl_pkey on base_tbl
+         Index Cond: ((a > 0) AND (a = 5))
+(3 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to view rw_view1
+-- view on top of view
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+CREATE VIEW rw_view1 AS SELECT b AS bb, a AS aa FROM base_tbl WHERE a>0;
+CREATE VIEW rw_view2 AS SELECT aa AS aaa, bb AS bbb FROM rw_view1 WHERE aa<10;
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name = 'rw_view2';
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view2   | YES
+(1 row)
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name = 'rw_view2';
+ table_name | is_updatable | is_insertable_into 
+------------+--------------+--------------------
+ rw_view2   | YES          | YES
+(1 row)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name = 'rw_view2'
+ ORDER BY ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view2   | aaa         | YES
+ rw_view2   | bbb         | YES
+(2 rows)
+
+INSERT INTO rw_view2 VALUES (3, 'Row 3');
+INSERT INTO rw_view2 (aaa) VALUES (4);
+SELECT * FROM rw_view2;
+ aaa |     bbb     
+-----+-------------
+   1 | Row 1
+   2 | Row 2
+   3 | Row 3
+   4 | Unspecified
+(4 rows)
+
+UPDATE rw_view2 SET bbb='Row 4' WHERE aaa=4;
+DELETE FROM rw_view2 WHERE aaa=2;
+SELECT * FROM rw_view2;
+ aaa |  bbb  
+-----+-------
+   1 | Row 1
+   3 | Row 3
+   4 | Row 4
+(3 rows)
+
+EXPLAIN (costs off) UPDATE rw_view2 SET aaa=5 WHERE aaa=4;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Update on base_tbl
+   ->  Index Scan using base_tbl_pkey on base_tbl
+         Index Cond: ((a < 10) AND (a > 0) AND (a = 4))
+(3 rows)
+
+EXPLAIN (costs off) DELETE FROM rw_view2 WHERE aaa=4;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Delete on base_tbl
+   ->  Index Scan using base_tbl_pkey on base_tbl
+         Index Cond: ((a < 10) AND (a > 0) AND (a = 4))
+(3 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to view rw_view1
+drop cascades to view rw_view2
+-- view on top of view with rules
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0 OFFSET 0; -- not updatable without rules/triggers
+CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a<10;
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | NO
+ rw_view2   | NO
+(2 rows)
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into 
+------------+--------------+--------------------
+ rw_view1   | NO           | NO
+ rw_view2   | NO           | NO
+(2 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | NO
+ rw_view1   | b           | NO
+ rw_view2   | a           | NO
+ rw_view2   | b           | NO
+(4 rows)
+
+CREATE RULE rw_view1_ins_rule AS ON INSERT TO rw_view1
+  DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a, NEW.b) RETURNING *;
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | YES
+ rw_view2   | YES
+(2 rows)
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into 
+------------+--------------+--------------------
+ rw_view1   | NO           | YES
+ rw_view2   | NO           | YES
+(2 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | NO
+ rw_view1   | b           | NO
+ rw_view2   | a           | NO
+ rw_view2   | b           | NO
+(4 rows)
+
+CREATE RULE rw_view1_upd_rule AS ON UPDATE TO rw_view1
+  DO INSTEAD UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a RETURNING NEW.*;
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | YES
+ rw_view2   | YES
+(2 rows)
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into 
+------------+--------------+--------------------
+ rw_view1   | NO           | YES
+ rw_view2   | NO           | YES
+(2 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | NO
+ rw_view1   | b           | NO
+ rw_view2   | a           | NO
+ rw_view2   | b           | NO
+(4 rows)
+
+CREATE RULE rw_view1_del_rule AS ON DELETE TO rw_view1
+  DO INSTEAD DELETE FROM base_tbl WHERE a=OLD.a RETURNING OLD.*;
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | YES
+ rw_view2   | YES
+(2 rows)
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into 
+------------+--------------+--------------------
+ rw_view1   | YES          | YES
+ rw_view2   | YES          | YES
+(2 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | YES
+ rw_view1   | b           | YES
+ rw_view2   | a           | YES
+ rw_view2   | b           | YES
+(4 rows)
+
+INSERT INTO rw_view2 VALUES (3, 'Row 3') RETURNING *;
+ a |   b   
+---+-------
+ 3 | Row 3
+(1 row)
+
+UPDATE rw_view2 SET b='Row three' WHERE a=3 RETURNING *;
+ a |     b     
+---+-----------
+ 3 | Row three
+(1 row)
+
+SELECT * FROM rw_view2;
+ a |     b     
+---+-----------
+ 1 | Row 1
+ 2 | Row 2
+ 3 | Row three
+(3 rows)
+
+DELETE FROM rw_view2 WHERE a=3 RETURNING *;
+ a |     b     
+---+-----------
+ 3 | Row three
+(1 row)
+
+SELECT * FROM rw_view2;
+ a |   b   
+---+-------
+ 1 | Row 1
+ 2 | Row 2
+(2 rows)
+
+EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Update on base_tbl
+   ->  Nested Loop
+         ->  Index Scan using base_tbl_pkey on base_tbl
+               Index Cond: (a = 2)
+         ->  Subquery Scan on rw_view1
+               Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2))
+               ->  Limit
+                     ->  Bitmap Heap Scan on base_tbl base_tbl_1
+                           Recheck Cond: (a > 0)
+                           ->  Bitmap Index Scan on base_tbl_pkey
+                                 Index Cond: (a > 0)
+(11 rows)
+
+EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Delete on base_tbl
+   ->  Nested Loop
+         ->  Index Scan using base_tbl_pkey on base_tbl
+               Index Cond: (a = 2)
+         ->  Subquery Scan on rw_view1
+               Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2))
+               ->  Limit
+                     ->  Bitmap Heap Scan on base_tbl base_tbl_1
+                           Recheck Cond: (a > 0)
+                           ->  Bitmap Index Scan on base_tbl_pkey
+                                 Index Cond: (a > 0)
+(11 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to view rw_view1
+drop cascades to view rw_view2
+-- view on top of view with triggers
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0 OFFSET 0; -- not updatable without rules/triggers
+CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a<10;
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | NO
+ rw_view2   | NO
+(2 rows)
+
+SELECT table_name, is_updatable, is_insertable_into,
+       is_trigger_updatable, is_trigger_deletable,
+       is_trigger_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into 
+------------+--------------+--------------------+----------------------+----------------------+----------------------------
+ rw_view1   | NO           | NO                 | NO                   | NO                   | NO
+ rw_view2   | NO           | NO                 | NO                   | NO                   | NO
+(2 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | NO
+ rw_view1   | b           | NO
+ rw_view2   | a           | NO
+ rw_view2   | b           | NO
+(4 rows)
+
+CREATE FUNCTION rw_view1_trig_fn()
+RETURNS trigger AS
+$$
+BEGIN
+  IF TG_OP = 'INSERT' THEN
+    INSERT INTO base_tbl VALUES (NEW.a, NEW.b);
+    RETURN NEW;
+  ELSIF TG_OP = 'UPDATE' THEN
+    UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a;
+    RETURN NEW;
+  ELSIF TG_OP = 'DELETE' THEN
+    DELETE FROM base_tbl WHERE a=OLD.a;
+    RETURN OLD;
+  END IF;
+END;
+$$
+LANGUAGE plpgsql;
+CREATE TRIGGER rw_view1_ins_trig INSTEAD OF INSERT ON rw_view1
+  FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn();
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | NO
+ rw_view2   | NO
+(2 rows)
+
+SELECT table_name, is_updatable, is_insertable_into,
+       is_trigger_updatable, is_trigger_deletable,
+       is_trigger_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into 
+------------+--------------+--------------------+----------------------+----------------------+----------------------------
+ rw_view1   | NO           | NO                 | NO                   | NO                   | YES
+ rw_view2   | NO           | NO                 | NO                   | NO                   | NO
+(2 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | NO
+ rw_view1   | b           | NO
+ rw_view2   | a           | NO
+ rw_view2   | b           | NO
+(4 rows)
+
+CREATE TRIGGER rw_view1_upd_trig INSTEAD OF UPDATE ON rw_view1
+  FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn();
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | NO
+ rw_view2   | NO
+(2 rows)
+
+SELECT table_name, is_updatable, is_insertable_into,
+       is_trigger_updatable, is_trigger_deletable,
+       is_trigger_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into 
+------------+--------------+--------------------+----------------------+----------------------+----------------------------
+ rw_view1   | NO           | NO                 | YES                  | NO                   | YES
+ rw_view2   | NO           | NO                 | NO                   | NO                   | NO
+(2 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | NO
+ rw_view1   | b           | NO
+ rw_view2   | a           | NO
+ rw_view2   | b           | NO
+(4 rows)
+
+CREATE TRIGGER rw_view1_del_trig INSTEAD OF DELETE ON rw_view1
+  FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn();
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_insertable_into 
+------------+--------------------
+ rw_view1   | NO
+ rw_view2   | NO
+(2 rows)
+
+SELECT table_name, is_updatable, is_insertable_into,
+       is_trigger_updatable, is_trigger_deletable,
+       is_trigger_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+ table_name | is_updatable | is_insertable_into | is_trigger_updatable | is_trigger_deletable | is_trigger_insertable_into 
+------------+--------------+--------------------+----------------------+----------------------+----------------------------
+ rw_view1   | NO           | NO                 | YES                  | YES                  | YES
+ rw_view2   | NO           | NO                 | NO                   | NO                   | NO
+(2 rows)
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+ table_name | column_name | is_updatable 
+------------+-------------+--------------
+ rw_view1   | a           | NO
+ rw_view1   | b           | NO
+ rw_view2   | a           | NO
+ rw_view2   | b           | NO
+(4 rows)
+
+INSERT INTO rw_view2 VALUES (3, 'Row 3') RETURNING *;
+ a |   b   
+---+-------
+ 3 | Row 3
+(1 row)
+
+UPDATE rw_view2 SET b='Row three' WHERE a=3 RETURNING *;
+ a |     b     
+---+-----------
+ 3 | Row three
+(1 row)
+
+SELECT * FROM rw_view2;
+ a |     b     
+---+-----------
+ 1 | Row 1
+ 2 | Row 2
+ 3 | Row three
+(3 rows)
+
+DELETE FROM rw_view2 WHERE a=3 RETURNING *;
+ a |     b     
+---+-----------
+ 3 | Row three
+(1 row)
+
+SELECT * FROM rw_view2;
+ a |   b   
+---+-------
+ 1 | Row 1
+ 2 | Row 2
+(2 rows)
+
+EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Update on rw_view1 rw_view1_1
+   ->  Subquery Scan on rw_view1
+         Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2))
+         ->  Limit
+               ->  Bitmap Heap Scan on base_tbl
+                     Recheck Cond: (a > 0)
+                     ->  Bitmap Index Scan on base_tbl_pkey
+                           Index Cond: (a > 0)
+(8 rows)
+
+EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Delete on rw_view1 rw_view1_1
+   ->  Subquery Scan on rw_view1
+         Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2))
+         ->  Limit
+               ->  Bitmap Heap Scan on base_tbl
+                     Recheck Cond: (a > 0)
+                     ->  Bitmap Index Scan on base_tbl_pkey
+                           Index Cond: (a > 0)
+(8 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to view rw_view1
+drop cascades to view rw_view2
+DROP FUNCTION rw_view1_trig_fn();
+-- update using whole row from view
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+CREATE VIEW rw_view1 AS SELECT b AS bb, a AS aa FROM base_tbl;
+CREATE FUNCTION rw_view1_aa(x rw_view1)
+  RETURNS int AS $$ SELECT x.aa $$ LANGUAGE sql;
+UPDATE rw_view1 v SET bb='Updated row 2' WHERE rw_view1_aa(v)=2
+  RETURNING rw_view1_aa(v), v.bb;
+ rw_view1_aa |      bb       
+-------------+---------------
+           2 | Updated row 2
+(1 row)
+
+SELECT * FROM base_tbl;
+ a  |       b       
+----+---------------
+ -2 | Row -2
+ -1 | Row -1
+  0 | Row 0
+  1 | Row 1
+  2 | Updated row 2
+(5 rows)
+
+EXPLAIN (costs off)
+UPDATE rw_view1 v SET bb='Updated row 2' WHERE rw_view1_aa(v)=2
+  RETURNING rw_view1_aa(v), v.bb;
+                    QUERY PLAN                    
+--------------------------------------------------
+ Update on base_tbl
+   ->  Index Scan using base_tbl_pkey on base_tbl
+         Index Cond: (a = 2)
+(3 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to view rw_view1
+drop cascades to function rw_view1_aa(rw_view1)
+-- permissions checks
+CREATE USER view_user1;
+CREATE USER view_user2;
+SET SESSION AUTHORIZATION view_user1;
+CREATE TABLE base_tbl(a int, b text, c float);
+INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0);
+CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl;
+INSERT INTO rw_view1 VALUES ('Row 2', 2.0, 2);
+GRANT SELECT ON base_tbl TO view_user2;
+GRANT SELECT ON rw_view1 TO view_user2;
+GRANT UPDATE (a,c) ON base_tbl TO view_user2;
+GRANT UPDATE (bb,cc) ON rw_view1 TO view_user2;
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION view_user2;
+CREATE VIEW rw_view2 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl;
+SELECT * FROM base_tbl; -- ok
+ a |   b   | c 
+---+-------+---
+ 1 | Row 1 | 1
+ 2 | Row 2 | 2
+(2 rows)
+
+SELECT * FROM rw_view1; -- ok
+  bb   | cc | aa 
+-------+----+----
+ Row 1 |  1 |  1
+ Row 2 |  2 |  2
+(2 rows)
+
+SELECT * FROM rw_view2; -- ok
+  bb   | cc | aa 
+-------+----+----
+ Row 1 |  1 |  1
+ Row 2 |  2 |  2
+(2 rows)
+
+INSERT INTO base_tbl VALUES (3, 'Row 3', 3.0); -- not allowed
+ERROR:  permission denied for relation base_tbl
+INSERT INTO rw_view1 VALUES ('Row 3', 3.0, 3); -- not allowed
+ERROR:  permission denied for relation rw_view1
+INSERT INTO rw_view2 VALUES ('Row 3', 3.0, 3); -- not allowed
+ERROR:  permission denied for relation base_tbl
+UPDATE base_tbl SET a=a, c=c; -- ok
+UPDATE base_tbl SET b=b; -- not allowed
+ERROR:  permission denied for relation base_tbl
+UPDATE rw_view1 SET bb=bb, cc=cc; -- ok
+UPDATE rw_view1 SET aa=aa; -- not allowed
+ERROR:  permission denied for relation rw_view1
+UPDATE rw_view2 SET aa=aa, cc=cc; -- ok
+UPDATE rw_view2 SET bb=bb; -- not allowed
+ERROR:  permission denied for relation base_tbl
+DELETE FROM base_tbl; -- not allowed
+ERROR:  permission denied for relation base_tbl
+DELETE FROM rw_view1; -- not allowed
+ERROR:  permission denied for relation rw_view1
+DELETE FROM rw_view2; -- not allowed
+ERROR:  permission denied for relation base_tbl
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION view_user1;
+GRANT INSERT, DELETE ON base_tbl TO view_user2;
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION view_user2;
+INSERT INTO base_tbl VALUES (3, 'Row 3', 3.0); -- ok
+INSERT INTO rw_view1 VALUES ('Row 4', 4.0, 4); -- not allowed
+ERROR:  permission denied for relation rw_view1
+INSERT INTO rw_view2 VALUES ('Row 4', 4.0, 4); -- ok
+DELETE FROM base_tbl WHERE a=1; -- ok
+DELETE FROM rw_view1 WHERE aa=2; -- not allowed
+ERROR:  permission denied for relation rw_view1
+DELETE FROM rw_view2 WHERE aa=2; -- ok
+SELECT * FROM base_tbl;
+ a |   b   | c 
+---+-------+---
+ 3 | Row 3 | 3
+ 4 | Row 4 | 4
+(2 rows)
+
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION view_user1;
+REVOKE INSERT, DELETE ON base_tbl FROM view_user2;
+GRANT INSERT, DELETE ON rw_view1 TO view_user2;
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION view_user2;
+INSERT INTO base_tbl VALUES (5, 'Row 5', 5.0); -- not allowed
+ERROR:  permission denied for relation base_tbl
+INSERT INTO rw_view1 VALUES ('Row 5', 5.0, 5); -- ok
+INSERT INTO rw_view2 VALUES ('Row 6', 6.0, 6); -- not allowed
+ERROR:  permission denied for relation base_tbl
+DELETE FROM base_tbl WHERE a=3; -- not allowed
+ERROR:  permission denied for relation base_tbl
+DELETE FROM rw_view1 WHERE aa=3; -- ok
+DELETE FROM rw_view2 WHERE aa=4; -- not allowed
+ERROR:  permission denied for relation base_tbl
+SELECT * FROM base_tbl;
+ a |   b   | c 
+---+-------+---
+ 4 | Row 4 | 4
+ 5 | Row 5 | 5
+(2 rows)
+
+RESET SESSION AUTHORIZATION;
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to view rw_view1
+drop cascades to view rw_view2
+DROP USER view_user1;
+DROP USER view_user2;
+-- column defaults
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified', c serial);
+INSERT INTO base_tbl VALUES (1, 'Row 1');
+INSERT INTO base_tbl VALUES (2, 'Row 2');
+INSERT INTO base_tbl VALUES (3);
+CREATE VIEW rw_view1 AS SELECT a AS aa, b AS bb FROM base_tbl;
+ALTER VIEW rw_view1 ALTER COLUMN bb SET DEFAULT 'View default';
+INSERT INTO rw_view1 VALUES (4, 'Row 4');
+INSERT INTO rw_view1 (aa) VALUES (5);
+SELECT * FROM base_tbl;
+ a |      b       | c 
+---+--------------+---
+ 1 | Row 1        | 1
+ 2 | Row 2        | 2
+ 3 | Unspecified  | 3
+ 4 | Row 4        | 4
+ 5 | View default | 5
+(5 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to view rw_view1
+-- Table having triggers
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl VALUES (1, 'Row 1');
+INSERT INTO base_tbl VALUES (2, 'Row 2');
+CREATE FUNCTION rw_view1_trig_fn()
+RETURNS trigger AS
+$$
+BEGIN
+  IF TG_OP = 'INSERT' THEN
+    UPDATE base_tbl SET b=NEW.b WHERE a=1;
+    RETURN NULL;
+  END IF;
+  RETURN NULL;
+END;
+$$
+LANGUAGE plpgsql;
+CREATE TRIGGER rw_view1_ins_trig AFTER INSERT ON base_tbl
+  FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn();
+CREATE VIEW rw_view1 AS SELECT a AS aa, b AS bb FROM base_tbl;
+INSERT INTO rw_view1 VALUES (3, 'Row 3');
+select * from base_tbl;
+ a |   b   
+---+-------
+ 2 | Row 2
+ 3 | Row 3
+ 1 | Row 3
+(3 rows)
+
+DROP VIEW rw_view1;
+DROP TRIGGER rw_view1_ins_trig on base_tbl;
+DROP FUNCTION rw_view1_trig_fn();
+DROP TABLE base_tbl;
+-- view with ORDER BY
+CREATE TABLE base_tbl (a int, b int);
+INSERT INTO base_tbl VALUES (1,2), (4,5), (3,-3);
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl ORDER BY a+b;
+SELECT * FROM rw_view1;
+ a | b  
+---+----
+ 3 | -3
+ 1 |  2
+ 4 |  5
+(3 rows)
+
+INSERT INTO rw_view1 VALUES (7,-8);
+SELECT * FROM rw_view1;
+ a | b  
+---+----
+ 7 | -8
+ 3 | -3
+ 1 |  2
+ 4 |  5
+(4 rows)
+
+EXPLAIN (verbose, costs off) UPDATE rw_view1 SET b = b + 1 RETURNING *;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Update on public.base_tbl
+   Output: base_tbl.a, base_tbl.b
+   ->  Seq Scan on public.base_tbl
+         Output: base_tbl.a, (base_tbl.b + 1), base_tbl.ctid
+(4 rows)
+
+UPDATE rw_view1 SET b = b + 1 RETURNING *;
+ a | b  
+---+----
+ 1 |  3
+ 4 |  6
+ 3 | -2
+ 7 | -7
+(4 rows)
+
+SELECT * FROM rw_view1;
+ a | b  
+---+----
+ 7 | -7
+ 3 | -2
+ 1 |  3
+ 4 |  6
+(4 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to view rw_view1
+-- multiple array-column updates
+CREATE TABLE base_tbl (a int, arr int[]);
+INSERT INTO base_tbl VALUES (1,ARRAY[2]), (3,ARRAY[4]);
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl;
+UPDATE rw_view1 SET arr[1] = 42, arr[2] = 77 WHERE a = 3;
+SELECT * FROM rw_view1;
+ a |   arr   
+---+---------
+ 1 | {2}
+ 3 | {42,77}
+(2 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to view rw_view1
index 663bf8ac56bd157a6f1e8fd4996ae74e5f5b4cfd..bdcf3a6a559192564ae9bd27ea4753dfae5a93fa 100644 (file)
@@ -59,7 +59,7 @@ test: create_index create_view
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists
+test: create_aggregate create_function_3 create_cast constraints triggers inherit create_table_like typed_table vacuum drop_if_exists updatable_views
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
index be789e3f442529f9d6aed282f24c4cbeabf83b8f..c7c2ed0f6a0c472db981e8b115adffb4be40bc4d 100644 (file)
@@ -67,6 +67,7 @@ test: create_table_like
 test: typed_table
 test: vacuum
 test: drop_if_exists
+test: updatable_views
 test: sanity_check
 test: errors
 test: select
index 78c5407560a0628158c0b0c209dccab797407c5d..0ea2c314dee49735cc0fce4b35a8d5e057626532 100644 (file)
@@ -611,13 +611,6 @@ DROP TABLE min_updates_test_oids;
 
 CREATE VIEW main_view AS SELECT a, b FROM main_table;
 
--- Updates should fail without rules or triggers
-INSERT INTO main_view VALUES (1,2);
-UPDATE main_view SET b = 20 WHERE a = 50;
-DELETE FROM main_view WHERE a = 50;
--- Should fail even when there are no matching rows
-DELETE FROM main_view WHERE a = 51;
-
 -- VIEW trigger function
 CREATE OR REPLACE FUNCTION view_trigger() RETURNS trigger
 LANGUAGE plpgsql AS $$
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644 (file)
index 0000000..49dfedd
--- /dev/null
@@ -0,0 +1,511 @@
+--
+-- UPDATABLE VIEWS
+--
+
+-- check that non-updatable views are rejected with useful error messages
+
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+
+CREATE VIEW ro_view1 AS SELECT DISTINCT a, b FROM base_tbl; -- DISTINCT not supported
+CREATE VIEW ro_view2 AS SELECT a, b FROM base_tbl GROUP BY a, b; -- GROUP BY not supported
+CREATE VIEW ro_view3 AS SELECT 1 FROM base_tbl HAVING max(a) > 0; -- HAVING not supported
+CREATE VIEW ro_view4 AS SELECT count(*) FROM base_tbl; -- Aggregate functions not supported
+CREATE VIEW ro_view5 AS SELECT a, rank() OVER() FROM base_tbl; -- Window functions not supported
+CREATE VIEW ro_view6 AS SELECT a, b FROM base_tbl UNION SELECT -a, b FROM base_tbl; -- Set ops not supported
+CREATE VIEW ro_view7 AS WITH t AS (SELECT a, b FROM base_tbl) SELECT * FROM t; -- WITH not supported
+CREATE VIEW ro_view8 AS SELECT a, b FROM base_tbl ORDER BY a OFFSET 1; -- OFFSET not supported
+CREATE VIEW ro_view9 AS SELECT a, b FROM base_tbl ORDER BY a LIMIT 1; -- LIMIT not supported
+CREATE VIEW ro_view10 AS SELECT 1 AS a; -- No base relations
+CREATE VIEW ro_view11 AS SELECT b1.a, b2.b FROM base_tbl b1, base_tbl b2; -- Multiple base relations
+CREATE VIEW ro_view12 AS SELECT * FROM generate_series(1, 10) AS g(a); -- SRF in rangetable
+CREATE VIEW ro_view13 AS SELECT a, b FROM (SELECT * FROM base_tbl) AS t; -- Subselect in rangetable
+CREATE VIEW ro_view14 AS SELECT ctid FROM base_tbl; -- System columns not supported
+CREATE VIEW ro_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function in targetlist
+CREATE VIEW ro_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column
+CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
+CREATE VIEW ro_view18 WITH (security_barrier = true)
+  AS SELECT * FROM base_tbl; -- Security barrier views not updatable
+CREATE VIEW ro_view19 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
+CREATE SEQUENCE seq;
+CREATE VIEW ro_view20 AS SELECT * FROM seq; -- View based on a sequence
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'ro_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'ro_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'ro_view%'
+ ORDER BY table_name, ordinal_position;
+
+DELETE FROM ro_view1;
+DELETE FROM ro_view2;
+DELETE FROM ro_view3;
+DELETE FROM ro_view4;
+DELETE FROM ro_view5;
+DELETE FROM ro_view6;
+UPDATE ro_view7 SET a=a+1;
+UPDATE ro_view8 SET a=a+1;
+UPDATE ro_view9 SET a=a+1;
+UPDATE ro_view10 SET a=a+1;
+UPDATE ro_view11 SET a=a+1;
+UPDATE ro_view12 SET a=a+1;
+INSERT INTO ro_view13 VALUES (3, 'Row 3');
+INSERT INTO ro_view14 VALUES (null);
+INSERT INTO ro_view15 VALUES (3, 'ROW 3');
+INSERT INTO ro_view16 VALUES (3, 'Row 3', 3);
+INSERT INTO ro_view17 VALUES (3, 'ROW 3');
+INSERT INTO ro_view18 VALUES (3, 'ROW 3');
+DELETE FROM ro_view19;
+UPDATE ro_view20 SET max_value=1000;
+
+DROP TABLE base_tbl CASCADE;
+DROP VIEW ro_view10, ro_view12, ro_view19;
+DROP SEQUENCE seq CASCADE;
+
+-- simple updatable view
+
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0;
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name = 'rw_view1';
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name = 'rw_view1';
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name = 'rw_view1'
+ ORDER BY ordinal_position;
+
+INSERT INTO rw_view1 VALUES (3, 'Row 3');
+INSERT INTO rw_view1 (a) VALUES (4);
+UPDATE rw_view1 SET a=5 WHERE a=4;
+DELETE FROM rw_view1 WHERE b='Row 2';
+SELECT * FROM base_tbl;
+
+EXPLAIN (costs off) UPDATE rw_view1 SET a=6 WHERE a=5;
+EXPLAIN (costs off) DELETE FROM rw_view1 WHERE a=5;
+
+DROP TABLE base_tbl CASCADE;
+
+-- view on top of view
+
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+
+CREATE VIEW rw_view1 AS SELECT b AS bb, a AS aa FROM base_tbl WHERE a>0;
+CREATE VIEW rw_view2 AS SELECT aa AS aaa, bb AS bbb FROM rw_view1 WHERE aa<10;
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name = 'rw_view2';
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name = 'rw_view2';
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name = 'rw_view2'
+ ORDER BY ordinal_position;
+
+INSERT INTO rw_view2 VALUES (3, 'Row 3');
+INSERT INTO rw_view2 (aaa) VALUES (4);
+SELECT * FROM rw_view2;
+UPDATE rw_view2 SET bbb='Row 4' WHERE aaa=4;
+DELETE FROM rw_view2 WHERE aaa=2;
+SELECT * FROM rw_view2;
+
+EXPLAIN (costs off) UPDATE rw_view2 SET aaa=5 WHERE aaa=4;
+EXPLAIN (costs off) DELETE FROM rw_view2 WHERE aaa=4;
+
+DROP TABLE base_tbl CASCADE;
+
+-- view on top of view with rules
+
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0 OFFSET 0; -- not updatable without rules/triggers
+CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a<10;
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+
+CREATE RULE rw_view1_ins_rule AS ON INSERT TO rw_view1
+  DO INSTEAD INSERT INTO base_tbl VALUES (NEW.a, NEW.b) RETURNING *;
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+
+CREATE RULE rw_view1_upd_rule AS ON UPDATE TO rw_view1
+  DO INSTEAD UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a RETURNING NEW.*;
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+
+CREATE RULE rw_view1_del_rule AS ON DELETE TO rw_view1
+  DO INSTEAD DELETE FROM base_tbl WHERE a=OLD.a RETURNING OLD.*;
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+
+INSERT INTO rw_view2 VALUES (3, 'Row 3') RETURNING *;
+UPDATE rw_view2 SET b='Row three' WHERE a=3 RETURNING *;
+SELECT * FROM rw_view2;
+DELETE FROM rw_view2 WHERE a=3 RETURNING *;
+SELECT * FROM rw_view2;
+
+EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
+EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2;
+
+DROP TABLE base_tbl CASCADE;
+
+-- view on top of view with triggers
+
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a>0 OFFSET 0; -- not updatable without rules/triggers
+CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a<10;
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into,
+       is_trigger_updatable, is_trigger_deletable,
+       is_trigger_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+
+CREATE FUNCTION rw_view1_trig_fn()
+RETURNS trigger AS
+$$
+BEGIN
+  IF TG_OP = 'INSERT' THEN
+    INSERT INTO base_tbl VALUES (NEW.a, NEW.b);
+    RETURN NEW;
+  ELSIF TG_OP = 'UPDATE' THEN
+    UPDATE base_tbl SET b=NEW.b WHERE a=OLD.a;
+    RETURN NEW;
+  ELSIF TG_OP = 'DELETE' THEN
+    DELETE FROM base_tbl WHERE a=OLD.a;
+    RETURN OLD;
+  END IF;
+END;
+$$
+LANGUAGE plpgsql;
+
+CREATE TRIGGER rw_view1_ins_trig INSTEAD OF INSERT ON rw_view1
+  FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn();
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into,
+       is_trigger_updatable, is_trigger_deletable,
+       is_trigger_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+
+CREATE TRIGGER rw_view1_upd_trig INSTEAD OF UPDATE ON rw_view1
+  FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn();
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into,
+       is_trigger_updatable, is_trigger_deletable,
+       is_trigger_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+
+CREATE TRIGGER rw_view1_del_trig INSTEAD OF DELETE ON rw_view1
+  FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn();
+
+SELECT table_name, is_insertable_into
+  FROM information_schema.tables
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, is_updatable, is_insertable_into,
+       is_trigger_updatable, is_trigger_deletable,
+       is_trigger_insertable_into
+  FROM information_schema.views
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name;
+
+SELECT table_name, column_name, is_updatable
+  FROM information_schema.columns
+ WHERE table_name LIKE 'rw_view%'
+ ORDER BY table_name, ordinal_position;
+
+INSERT INTO rw_view2 VALUES (3, 'Row 3') RETURNING *;
+UPDATE rw_view2 SET b='Row three' WHERE a=3 RETURNING *;
+SELECT * FROM rw_view2;
+DELETE FROM rw_view2 WHERE a=3 RETURNING *;
+SELECT * FROM rw_view2;
+
+EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
+EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2;
+
+DROP TABLE base_tbl CASCADE;
+DROP FUNCTION rw_view1_trig_fn();
+
+-- update using whole row from view
+
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl SELECT i, 'Row ' || i FROM generate_series(-2, 2) g(i);
+
+CREATE VIEW rw_view1 AS SELECT b AS bb, a AS aa FROM base_tbl;
+
+CREATE FUNCTION rw_view1_aa(x rw_view1)
+  RETURNS int AS $$ SELECT x.aa $$ LANGUAGE sql;
+
+UPDATE rw_view1 v SET bb='Updated row 2' WHERE rw_view1_aa(v)=2
+  RETURNING rw_view1_aa(v), v.bb;
+SELECT * FROM base_tbl;
+
+EXPLAIN (costs off)
+UPDATE rw_view1 v SET bb='Updated row 2' WHERE rw_view1_aa(v)=2
+  RETURNING rw_view1_aa(v), v.bb;
+
+DROP TABLE base_tbl CASCADE;
+
+-- permissions checks
+
+CREATE USER view_user1;
+CREATE USER view_user2;
+
+SET SESSION AUTHORIZATION view_user1;
+CREATE TABLE base_tbl(a int, b text, c float);
+INSERT INTO base_tbl VALUES (1, 'Row 1', 1.0);
+CREATE VIEW rw_view1 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl;
+INSERT INTO rw_view1 VALUES ('Row 2', 2.0, 2);
+
+GRANT SELECT ON base_tbl TO view_user2;
+GRANT SELECT ON rw_view1 TO view_user2;
+GRANT UPDATE (a,c) ON base_tbl TO view_user2;
+GRANT UPDATE (bb,cc) ON rw_view1 TO view_user2;
+RESET SESSION AUTHORIZATION;
+
+SET SESSION AUTHORIZATION view_user2;
+CREATE VIEW rw_view2 AS SELECT b AS bb, c AS cc, a AS aa FROM base_tbl;
+SELECT * FROM base_tbl; -- ok
+SELECT * FROM rw_view1; -- ok
+SELECT * FROM rw_view2; -- ok
+
+INSERT INTO base_tbl VALUES (3, 'Row 3', 3.0); -- not allowed
+INSERT INTO rw_view1 VALUES ('Row 3', 3.0, 3); -- not allowed
+INSERT INTO rw_view2 VALUES ('Row 3', 3.0, 3); -- not allowed
+
+UPDATE base_tbl SET a=a, c=c; -- ok
+UPDATE base_tbl SET b=b; -- not allowed
+UPDATE rw_view1 SET bb=bb, cc=cc; -- ok
+UPDATE rw_view1 SET aa=aa; -- not allowed
+UPDATE rw_view2 SET aa=aa, cc=cc; -- ok
+UPDATE rw_view2 SET bb=bb; -- not allowed
+
+DELETE FROM base_tbl; -- not allowed
+DELETE FROM rw_view1; -- not allowed
+DELETE FROM rw_view2; -- not allowed
+RESET SESSION AUTHORIZATION;
+
+SET SESSION AUTHORIZATION view_user1;
+GRANT INSERT, DELETE ON base_tbl TO view_user2;
+RESET SESSION AUTHORIZATION;
+
+SET SESSION AUTHORIZATION view_user2;
+INSERT INTO base_tbl VALUES (3, 'Row 3', 3.0); -- ok
+INSERT INTO rw_view1 VALUES ('Row 4', 4.0, 4); -- not allowed
+INSERT INTO rw_view2 VALUES ('Row 4', 4.0, 4); -- ok
+DELETE FROM base_tbl WHERE a=1; -- ok
+DELETE FROM rw_view1 WHERE aa=2; -- not allowed
+DELETE FROM rw_view2 WHERE aa=2; -- ok
+SELECT * FROM base_tbl;
+RESET SESSION AUTHORIZATION;
+
+SET SESSION AUTHORIZATION view_user1;
+REVOKE INSERT, DELETE ON base_tbl FROM view_user2;
+GRANT INSERT, DELETE ON rw_view1 TO view_user2;
+RESET SESSION AUTHORIZATION;
+
+SET SESSION AUTHORIZATION view_user2;
+INSERT INTO base_tbl VALUES (5, 'Row 5', 5.0); -- not allowed
+INSERT INTO rw_view1 VALUES ('Row 5', 5.0, 5); -- ok
+INSERT INTO rw_view2 VALUES ('Row 6', 6.0, 6); -- not allowed
+DELETE FROM base_tbl WHERE a=3; -- not allowed
+DELETE FROM rw_view1 WHERE aa=3; -- ok
+DELETE FROM rw_view2 WHERE aa=4; -- not allowed
+SELECT * FROM base_tbl;
+RESET SESSION AUTHORIZATION;
+
+DROP TABLE base_tbl CASCADE;
+
+DROP USER view_user1;
+DROP USER view_user2;
+
+-- column defaults
+
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified', c serial);
+INSERT INTO base_tbl VALUES (1, 'Row 1');
+INSERT INTO base_tbl VALUES (2, 'Row 2');
+INSERT INTO base_tbl VALUES (3);
+
+CREATE VIEW rw_view1 AS SELECT a AS aa, b AS bb FROM base_tbl;
+ALTER VIEW rw_view1 ALTER COLUMN bb SET DEFAULT 'View default';
+
+INSERT INTO rw_view1 VALUES (4, 'Row 4');
+INSERT INTO rw_view1 (aa) VALUES (5);
+
+SELECT * FROM base_tbl;
+
+DROP TABLE base_tbl CASCADE;
+
+-- Table having triggers
+
+CREATE TABLE base_tbl (a int PRIMARY KEY, b text DEFAULT 'Unspecified');
+INSERT INTO base_tbl VALUES (1, 'Row 1');
+INSERT INTO base_tbl VALUES (2, 'Row 2');
+
+CREATE FUNCTION rw_view1_trig_fn()
+RETURNS trigger AS
+$$
+BEGIN
+  IF TG_OP = 'INSERT' THEN
+    UPDATE base_tbl SET b=NEW.b WHERE a=1;
+    RETURN NULL;
+  END IF;
+  RETURN NULL;
+END;
+$$
+LANGUAGE plpgsql;
+
+CREATE TRIGGER rw_view1_ins_trig AFTER INSERT ON base_tbl
+  FOR EACH ROW EXECUTE PROCEDURE rw_view1_trig_fn();
+
+CREATE VIEW rw_view1 AS SELECT a AS aa, b AS bb FROM base_tbl;
+
+INSERT INTO rw_view1 VALUES (3, 'Row 3');
+select * from base_tbl;
+
+DROP VIEW rw_view1;
+DROP TRIGGER rw_view1_ins_trig on base_tbl;
+DROP FUNCTION rw_view1_trig_fn();
+DROP TABLE base_tbl;
+
+-- view with ORDER BY
+
+CREATE TABLE base_tbl (a int, b int);
+INSERT INTO base_tbl VALUES (1,2), (4,5), (3,-3);
+
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl ORDER BY a+b;
+
+SELECT * FROM rw_view1;
+
+INSERT INTO rw_view1 VALUES (7,-8);
+SELECT * FROM rw_view1;
+
+EXPLAIN (verbose, costs off) UPDATE rw_view1 SET b = b + 1 RETURNING *;
+UPDATE rw_view1 SET b = b + 1 RETURNING *;
+SELECT * FROM rw_view1;
+
+DROP TABLE base_tbl CASCADE;
+
+-- multiple array-column updates
+
+CREATE TABLE base_tbl (a int, arr int[]);
+INSERT INTO base_tbl VALUES (1,ARRAY[2]), (3,ARRAY[4]);
+
+CREATE VIEW rw_view1 AS SELECT * FROM base_tbl;
+
+UPDATE rw_view1 SET arr[1] = 42, arr[2] = 77 WHERE a = 3;
+
+SELECT * FROM rw_view1;
+
+DROP TABLE base_tbl CASCADE;