WIP patch for updatable security barrier views

Started by Craig Ringerabout 12 years ago71 messages
#1Craig Ringer
craig@2ndquadrant.com
1 attachment(s)

Hi all

I have updatable security barrier views working for INSERT and DELETE,
so this might be a good chance to see whether the described approach is
acceptable in reality, not just in theory.

I've been surprised by how well it worked out. I actually landed up
removing a lot of the existing updatable views code; once update knows
how to operate on a subquery it becomes unnecessary to duplicate the
optimiser's knowledge of how to expand and flatten a view in the rewriter.

INSERT and DELETE work. I haven't tested INSERT with defaults on the
base rel yet but suspect it'll need the same support as for update.

UPDATE isn't yet supported because of the need to inject references to
cols in the base rel that aren't selected in the view.
expand_targetlist(...) in prep/preptlist.c already does most of this
work so I hope to be able to use or adapt that.

This patch isn't subject to the replanning and invalidation issues
discussed for RLS because updatable s.b. views don't depend on the
current user or some special "bypass RLS" right like RLS would.

The regression tests die because they try to update an updatable view.

This isn't proposed for inclusion as it stands, it's a chance to comment
and say "why the heck would you do that".

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

0001-First-pass-updatable-s.b.-views-support-only-handles.patchtext/x-patch; name=0001-First-pass-updatable-s.b.-views-support-only-handles.patchDownload
>From 93d6bb42040bc9a062d5aa345362b1f0be81670d Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Thu, 21 Nov 2013 21:05:39 +0800
Subject: [PATCH] First pass updatable s.b. views support - only handles DELETE
 from a view

---
 src/backend/commands/tablecmds.c       |   6 +-
 src/backend/commands/view.c            |  11 +-
 src/backend/optimizer/plan/planmain.c  |  15 +++
 src/backend/optimizer/prep/preptlist.c |   2 +
 src/backend/rewrite/rewriteHandler.c   | 239 ++++++---------------------------
 src/include/rewrite/rewriteHandler.h   |   1 -
 6 files changed, 65 insertions(+), 209 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0b31f55..128af98 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8798,7 +8798,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 		List	   *view_options = untransformRelOptions(newOptions);
 		ListCell   *cell;
 		bool		check_option = false;
-		bool		security_barrier = false;
 
 		foreach(cell, view_options)
 		{
@@ -8806,8 +8805,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 
 			if (pg_strcasecmp(defel->defname, "check_option") == 0)
 				check_option = true;
-			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
-				security_barrier = defGetBoolean(defel);
 		}
 
 		/*
@@ -8817,8 +8814,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 		if (check_option)
 		{
 			const char *view_updatable_error =
-				view_query_is_auto_updatable(view_query,
-											 security_barrier, true);
+				view_query_is_auto_updatable(view_query, true);
 
 			if (view_updatable_error)
 				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index aca40e7..5b1457a 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -313,8 +313,11 @@ DefineViewRules(Oid viewOid, Query *viewParse, bool replace)
 					   list_make1(viewParse));
 
 	/*
-	 * Someday: automatic ON INSERT, etc
+	 * No automatic ON INSERT, etc is needed, since DML can operate
+	 * directly on the subquery expansion of the SELECT view with
+	 * a little massaging. See the rewriter.
 	 */
+
 }
 
 /*---------------------------------------------------------------
@@ -395,7 +398,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	RangeVar   *view;
 	ListCell   *cell;
 	bool		check_option;
-	bool		security_barrier;
 
 	/*
 	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
@@ -450,7 +452,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	 * specified.
 	 */
 	check_option = false;
-	security_barrier = false;
 
 	foreach(cell, stmt->options)
 	{
@@ -458,8 +459,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 
 		if (pg_strcasecmp(defel->defname, "check_option") == 0)
 			check_option = true;
-		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
-			security_barrier = defGetBoolean(defel);
 	}
 
 	/*
@@ -469,7 +468,7 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	if (check_option)
 	{
 		const char *view_updatable_error =
-			view_query_is_auto_updatable(viewParse, security_barrier, true);
+			view_query_is_auto_updatable(viewParse, true);
 
 		if (view_updatable_error)
 			ereport(ERROR,
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 3a4e836..d3eae04 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -126,6 +126,21 @@ query_planner(PlannerInfo *root, List *tlist,
 	add_base_rels_to_query(root, (Node *) parse->jointree);
 
 	/*
+	 * The top query's resultRelation RTI may not be referenced in the query
+	 * tree its self, so we need to ensure it gets cached too. Curently arises
+	 * when we're doing DML on a view, where the injected top-level RTE for the
+	 * view's base rel isn't referenced in the query tree.
+	 *
+	 * It isn't a RELOPT_BASEREL because it doesn't appear in the query
+	 * join tree, but RELOPT_DEADREL may not be quite right either. Do we
+	 * need a RELOPT_RESULTREL? FIXME?
+	 */
+	if (root->parse->resultRelation && root->simple_rel_array[root->parse->resultRelation] == NULL)
+		(void) build_simple_rel(root, root->parse->resultRelation, RELOPT_DEADREL);
+
+	Assert(root->parse->resultRelation == 0 || root->simple_rel_array[root->parse->resultRelation] != NULL);
+
+	/*
 	 * Examine the targetlist and join tree, adding entries to baserel
 	 * targetlists for all referenced Vars, and generating PlaceHolderInfo
 	 * entries for all referenced PlaceHolderVars.	Restrict and join clauses
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index fb67f9e..81200d1 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -58,6 +58,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 	/*
 	 * Sanity check: if there is a result relation, it'd better be a real
 	 * relation not a subquery.  Else parser or rewriter messed up.
+	 * This is true even if we're updating a view/subquery; the result_relation
+	 * must still be the underlying relation RTE.
 	 */
 	if (result_relation)
 	{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index c52a374..0d9fd2d 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -28,6 +28,7 @@
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "nodes/print.h" /* Temporary for elog_print_display only */
 
 
 /* We use a list of these to detect recursion in RewriteQuery */
@@ -1973,7 +1974,7 @@ view_col_is_auto_updatable(RangeTblRef *rtr, TargetEntry *tle)
  * updatable.
  */
 const char *
-view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
+view_query_is_auto_updatable(Query *viewquery,
 							 bool check_cols)
 {
 	RangeTblRef *rtr;
@@ -2048,14 +2049,6 @@ view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
 		return gettext_noop("Views that return set-returning functions 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 the future.
-	 */
-	if (security_barrier)
-		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.
 	 */
@@ -2304,7 +2297,6 @@ relation_is_updatable(Oid reloid,
 		Query	   *viewquery = get_view_query(rel);
 
 		if (view_query_is_auto_updatable(viewquery,
-										 RelationIsSecurityView(rel),
 										 false) == NULL)
 		{
 			Bitmapset  *updatable_cols;
@@ -2452,7 +2444,6 @@ rewriteTargetView(Query *parsetree, Relation view)
 	RangeTblEntry *view_rte;
 	RangeTblEntry *new_rte;
 	Relation	base_rel;
-	List	   *view_targetlist;
 	ListCell   *lc;
 
 	/* The view must be updatable, else fail */
@@ -2460,7 +2451,6 @@ rewriteTargetView(Query *parsetree, Relation view)
 
 	auto_update_detail =
 		view_query_is_auto_updatable(viewquery,
-									 RelationIsSecurityView(view),
 									 parsetree->commandType != CMD_DELETE);
 
 	if (auto_update_detail)
@@ -2590,14 +2580,41 @@ rewriteTargetView(Query *parsetree, Relation view)
 
 	/*
 	 * 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.)
+	 * outer query's rangetable, then set it as the targetRelation for the query.
+	 * 
+	 * We don't do any pull-up here; the optimizer will do it for non-security-barrier
+	 * views later if it feels like it. The purpose of this RTE is to be the targetRelation,
+	 * and to be the vehicle for write-permission checks against this view.
 	 */
 	new_rte = (RangeTblEntry *) copyObject(base_rte);
 	parsetree->rtable = lappend(parsetree->rtable, new_rte);
 	new_rt_index = list_length(parsetree->rtable);
+	parsetree->resultRelation = new_rt_index;
+
+	/*
+	 * For UPDATE, add references to all columns of the base relation to the
+	 * expanded subquery if they weren't already present. We need all cols of
+	 * the base relation to generate the new tuple, even though they aren't
+	 * visible directly to the view user. These must not be revealed in
+	 * a RETURNING clause, and do *not* need the select permission bit on them
+	 * since the user won't ever actually see them.
+	 *
+	 * There's no need to sort these into the proper attribute ordering;
+	 * that'll be done for us later. TODO where?
+	 */
+	if (parsetree->commandType == CMD_UPDATE)
+	{
+		elog(ERROR, "UPDATE not supported yet, needs injection of ctid and all cols into view subquery");
+	}
+
+	/*
+ 	 * We need to know the ctid of the base rel for all command types, so
+	 * we need to add it to the view's select-list as a resjunk column if
+	 * it isn't already present.
+	 *
+	 * The base rel may its self be a view that doesn't have a ctid column,
+	 * though. TODO.
+ 	 */
 
 	/*
 	 * INSERTs never inherit.  For UPDATE/DELETE, we use the view query's
@@ -2607,21 +2624,6 @@ rewriteTargetView(Query *parsetree, Relation view)
 		new_rte->inh = false;
 
 	/*
-	 * 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
@@ -2650,179 +2652,22 @@ rewriteTargetView(Query *parsetree, Relation view)
 	 * 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.)
+	 * transformation.
+	 *
+	 * (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;
+												   viewquery->targetList);
 
-			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 in the main query.
-	 */
-	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);
-	}
-
-	/*
-	 * For INSERT/UPDATE, if the view has the WITH CHECK OPTION, or any parent
-	 * view specified WITH CASCADED CHECK OPTION, add the quals from the view
-	 * to the query's withCheckOptions list.
-	 */
-	if (parsetree->commandType != CMD_DELETE)
-	{
-		bool		has_wco = RelationHasCheckOption(view);
-		bool		cascaded = RelationHasCascadedCheckOption(view);
-
-		/*
-		 * If the parent view has a cascaded check option, treat this view as
-		 * if it also had a cascaded check option.
-		 *
-		 * New WithCheckOptions are added to the start of the list, so if there
-		 * is a cascaded check option, it will be the first item in the list.
-		 */
-		if (parsetree->withCheckOptions != NIL)
-		{
-			WithCheckOption *parent_wco =
-				(WithCheckOption *) linitial(parsetree->withCheckOptions);
-
-			if (parent_wco->cascaded)
-			{
-				has_wco = true;
-				cascaded = true;
-			}
-		}
-
-		/*
-		 * Add the new WithCheckOption to the start of the list, so that
-		 * checks on inner views are run before checks on outer views, as
-		 * required by the SQL standard.
-		 *
-		 * If the new check is CASCADED, we need to add it even if this view
-		 * has no quals, since there may be quals on child views.  A LOCAL
-		 * check can be omitted if this view has no quals.
-		 */
-		if (has_wco && (cascaded || viewquery->jointree->quals != NULL))
-		{
-			WithCheckOption *wco;
-
-			wco = makeNode(WithCheckOption);
-			wco->viewname = pstrdup(RelationGetRelationName(view));
-			wco->qual = NULL;
-			wco->cascaded = cascaded;
-
-			parsetree->withCheckOptions = lcons(wco,
-												parsetree->withCheckOptions);
-
-			if (viewquery->jointree->quals != NULL)
-			{
-				wco->qual = (Node *) copyObject(viewquery->jointree->quals);
-				ChangeVarNodes(wco->qual, base_rt_index, new_rt_index, 0);
-
-				/*
-				 * Make sure that the query is marked correctly if the added
-				 * qual has sublinks.  We can skip this check if the query is
-				 * already marked, or if the command is an UPDATE, in which
-				 * case the same qual will have already been added to the
-				 * query's WHERE clause, and AddQual will have already done
-				 * this check.
-				 */
-				if (!parsetree->hasSubLinks &&
-					parsetree->commandType != CMD_UPDATE)
-					parsetree->hasSubLinks = checkExprHasSubLink(wco->qual);
-			}
-		}
-	}
+    elog_node_display(LOG, "parse_tree after upd view rewrite", parsetree,
+                      true);
 
 	return parsetree;
 }
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index c959590..ce56d7b 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -23,7 +23,6 @@ extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
 extern Node *build_column_default(Relation rel, int attrno);
 extern Query *get_view_query(Relation view);
 extern const char *view_query_is_auto_updatable(Query *viewquery,
-										 bool security_barrier,
 										 bool check_cols);
 extern int	relation_is_updatable(Oid reloid,
 						  bool include_triggers,
-- 
1.8.3.1

#2Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#1)
Re: WIP patch for updatable security barrier views

On 21 November 2013 13:15, Craig Ringer <craig@2ndquadrant.com> wrote:

Hi all

I have updatable security barrier views working for INSERT and DELETE,
so this might be a good chance to see whether the described approach is
acceptable in reality, not just in theory.

I've been surprised by how well it worked out. I actually landed up
removing a lot of the existing updatable views code;

I fear you have removed a little too much though.

For example, something somewhere has to deal with re-mapping of
attributes. That's pretty fundamental, otherwise it can't hope to work
properly.

CREATE TABLE t1(a int, b text);
CREATE VIEW v1 AS SELECT b, a FROM t1;
INSERT INTO v1(a, b) VALUES(1, 'B');

ERROR: table row type and query-specified row type do not match
DETAIL: Table has type integer at ordinal position 1, but query expects text.

Also, you have deleted the code that supports WITH CHECK OPTION.

once update knows

how to operate on a subquery it becomes unnecessary to duplicate the
optimiser's knowledge of how to expand and flatten a view in the rewriter.

INSERT and DELETE work. I haven't tested INSERT with defaults on the
base rel yet but suspect it'll need the same support as for update.

UPDATE isn't yet supported because of the need to inject references to
cols in the base rel that aren't selected in the view.
expand_targetlist(...) in prep/preptlist.c already does most of this
work so I hope to be able to use or adapt that.

This patch isn't subject to the replanning and invalidation issues
discussed for RLS because updatable s.b. views don't depend on the
current user or some special "bypass RLS" right like RLS would.

The regression tests die because they try to update an updatable view.

This isn't proposed for inclusion as it stands, it's a chance to comment
and say "why the heck would you do that".

It sounds like it could be an interesting approach in principle, but
you haven't yet shown how you intend to replace some of the rewriter
code that you've removed. It feels to me that some of those things
pretty much have to happen in the rewriter, because the planner just
doesn't have the information it needs to be able to do it.

Regards,
Dean

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

#3Craig Ringer
craig@2ndquadrant.com
In reply to: Dean Rasheed (#2)
Re: WIP patch for updatable security barrier views

On 11/21/2013 10:55 PM, Dean Rasheed wrote:

On 21 November 2013 13:15, Craig Ringer <craig@2ndquadrant.com> wrote:

Hi all

I have updatable security barrier views working for INSERT and DELETE,
so this might be a good chance to see whether the described approach is
acceptable in reality, not just in theory.

I've been surprised by how well it worked out. I actually landed up
removing a lot of the existing updatable views code;

I fear you have removed a little too much though.

Absolutely. This is really a proof of concept to show that we can do DML
directly on a subquery by adding a new RTE for the resultRelation to the
top level of the query.

WITH CHECK OPTION was a casualty of cutting to prove the concept; I know
it needs to be fitted into the subquery based approach. I really haven't
thought about how WITH CHECK OPTION will fit into this, which may be a
mistake - I'm hoping to deal with it after I have the basics working.

There's lots of work to do, some of which will be adapting the code in
your updatable views code to work with this approach, moving them around
where appropriate.

There's also the need to inject columns for UPDATE so the whole tuple is
produced, and deal with DEFAULTs for INSERT.

For example, something somewhere has to deal with re-mapping of
attributes. That's pretty fundamental, otherwise it can't hope to work
properly.

CREATE TABLE t1(a int, b text);
CREATE VIEW v1 AS SELECT b, a FROM t1;
INSERT INTO v1(a, b) VALUES(1, 'B');

ERROR: table row type and query-specified row type do not match
DETAIL: Table has type integer at ordinal position 1, but query expects text.

Thanks. At this point I only expect it to work solidly for DELETE, and
was frankly surprised that INSERT worked at all.

The example of the problem is clear and useful, so thanks. I'm still
learning about the handling of attributes and target lists - that's the
next step in work on this.

It sounds like it could be an interesting approach in principle, but
you haven't yet shown how you intend to replace some of the rewriter
code that you've removed. It feels to me that some of those things
pretty much have to happen in the rewriter, because the planner just
doesn't have the information it needs to be able to do it.

I'm still working a lot of that out. At this point I just wanted to
demonstrate that we can in fact do DML directly on a subquery without
view qual pull-up and view subquery flattening.

One of my main worries is that adding and re-ordering columns needs to
happen from the bottom up - for nested views, it needs to start at the
real base rel and then proceed back up the chain of nested subqueries.
View expansion happens recursively in the rewriter, so this isn't too
easy. I'm thinking of doing it when we hit a real baserel during view
expansion, walking back up the query tree doing fixups then.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#4Craig Ringer
craig@2ndquadrant.com
In reply to: Craig Ringer (#3)
1 attachment(s)
Re: WIP patch (v2) for updatable security barrier views

Hi all

Here's an updated version of the updatable security barrier views patch
that's proposed as the next stage of progressing row-security toward
useful and maintainable inclusion in core.

If updatable security barrier views are available then the row-security
code won't have to play around with remapping vars and attrs during
query planning when it injects its subquery late. We'll simply stop the
planner flattening an updatable security barrier view, and for
row-security we'll dynamically inject one during rewriting when we see a
relation that has a row-security attribute.

This patch just implements updatable security barrier views. It is a
work in progress with at least two known crash bugs and limited testing.
I'd greatly appreciate comments (and advice) from those who are
interested in the problem domain as we proceed further into work on 9.4.

The patch is attached; it's on top of
46328916eefc5f9eaf249518e96f68afcd35923b, current head. It doens't yet
touch the documentation, but the only change needed should be to remove
the restriction on security_barrier views from the definition of what a
"simply updatable" view is.

There are couple of known issues: a crash with INSERT ... RETURNING;
DEFAULT handling through views isn't implemented yet; and a crash I just
found on UPDATE of a view that re-orders the original table columns. As
a result it doesn't survive "make check" yet.

I'm still working on fixing these issues and on finding more.
Suggestions/comments would be appreciated. I'll post specifics of the
INSERT ... RETURNING one soon, as I'm increasingly stuck on it.

Simple demo:

-- The 'id' is 'integer' not 'serial' because of the limitation with
-- DEFAULT mentioned below.

CREATE TABLE t (id integer primary key, secret text);

INSERT INTO t(id, secret)
SELECT x, 'secret'||x FROM generate_series(1,100) x;

CREATE VIEW t_even AS
SELECT id, secret FROM t WHERE id % 2 = 0;

CREATE VIEW t_even_sb WITH (security_barrier) AS
SELECT id, secret FROM t WHERE id % 2 = 0;

CREATE VIEW t_even_check WITH (check_option = 'cascaded') AS
SELECT id, secret FROM t WHERE id % 2 = 0;

CREATE VIEW t_even_check_sb WITH (check_option = 'cascaded',
security_barrier) AS
SELECT id, secret FROM t WHERE id % 2 = 0;

You'll find that the same f_leak tests used in the original read
security barrier views work here, too.

-- Subsets of cols work:

CREATE VIEW just_id AS SELECT id FROM t;
INSERT INTO just_id(id) VALUES (101);

CREATE VIEW just_id_sb WITH (security_barrier) AS
SELECT id FROM t;
INSERT INTO just_id_sb(id) VALUES (101);

-- Reversed column-lists work:

CREATE VIEW reversed AS SELECT secret, id FROM t;
INSERT INTO reversed(id, secret) VALUES (102, 'fred');

CREATE VIEW reversed_sb WITH (security_barrier) AS
SELECT secret, id FROM t;
INSERT INTO reversed_sb(id, secret) VALUES (102, 'fred');

-- WITH CHECK OPTION is working

postgres=# INSERT INTO t_even_check(id, secret) values (296, 'blah');
INSERT 0 1
postgres=# INSERT INTO t_even_check(id, secret) values (297, 'blah');
ERROR: new row violates WITH CHECK OPTION for view "t_even_check"
DETAIL: Failing row contains (297, blah).

postgres=# INSERT INTO t_even_check_sb(id, secret) values (298, 'blah');
INSERT 0 1
postgres=# INSERT INTO t_even_check_sb(id, secret) values (299, 'blah');
ERROR: new row violates WITH CHECK OPTION for view "t_even_check_sb"
DETAIL: Failing row contains (299, blah).

-- 3-col views are OK with various permutations

CREATE TABLE t3 ( id integer primary key, aa text, bb text );

CREATE VIEW t3_bai AS SELECT bb, aa, id FROM t3;
INSERT INTO t3_bai VALUES ('bb','aa',3);
UPDATE t3_bai SET bb = 'whatever' WHERE id = 3 RETURNING *;
DELETE FROM t3_bai RETURNING *;

CREATE VIEW t3_bai_sb WITH (security_barrier)
AS SELECT bb, aa, id FROM t3;
INSERT INTO t3_bai_sb VALUES ('bb','aa',3);

-- This crashes, with or without RETURNING. Bug in re-ord
-- UPDATE t3_bai_sb SET bb = 'whatever' WHERE id = 3 RETURNING *;

-- This is OK:
DELETE FROM t3_bai_sb RETURNING *;

--

Known issues:

DEFAULT injection doesn't occur correctly through the view. Needs some
changes in the rewriter where expand_target_list is called. Haven't
investigated in detail yet. Causes inserts through views to fail if
there's a default not null constraint, among other issues.

Any INSERT with a RETURNING clause through a view causes a crash (simple
VALUES clauses) or fails with "no relation entry for relid 1" (INSERT
... SELECT). UPDATE and DELETE is fine. Seems to be doing subquery
pull-up, producing a simple result sub-plan that incorrectly has a Var
reference but doesn't perform any scan. Issue traced to plan_subquery,
but no deeper yet.

Privilege enforcement has not yet been through a thorough test matrix.

UPDATE of a subset of columns fails. E.g.:

CREATE VIEW just_secret AS SELECT secret FROM t;
UPDATE just_secret SET secret = 'fred';

Known failing queries. Note that it doesn't matter what you choose from
RETURNING, any reference to a result relation will fail; constant
expressions succeed.

INSERT INTO t_even(id, secret) VALUES (218, 'fred')
RETURNING *;
-- **crash**

INSERT INTO t_even_sb(id, secret) VALUES (218, 'fred')
RETURNING *;
-- **crash**

INSERT INTO t_even_sb
SELECT x, 'secret'||x from generate_series(220,225) x
RETURNING *;
-- ERROR: no relation entry for relid 1

INSERT INTO t_even
SELECT x, 'secret'||x from generate_series(226,230) x
RETURNING *;
-- ERROR: no relation entry for relid 1

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

0001-Updatable-views-WIP-2.patchtext/x-patch; name=0001-Updatable-views-WIP-2.patchDownload
>From 8bd08e9bb9c984d017512536a17b4578f4c1c157 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Fri, 13 Dec 2013 21:47:22 +0800
Subject: [PATCH] Updatable views WIP 2

---
 src/backend/commands/tablecmds.c       |   6 +-
 src/backend/commands/view.c            |  11 +-
 src/backend/optimizer/plan/planmain.c  |  16 ++
 src/backend/optimizer/plan/setrefs.c   |  12 +-
 src/backend/optimizer/prep/preptlist.c |   9 +-
 src/backend/rewrite/rewriteHandler.c   | 333 +++++++++++++--------------------
 src/include/nodes/relation.h           |  14 +-
 src/include/optimizer/prep.h           |   3 +
 src/include/rewrite/rewriteHandler.h   |   1 -
 9 files changed, 185 insertions(+), 220 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b9cd88d..a984c03 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8806,7 +8806,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 		List	   *view_options = untransformRelOptions(newOptions);
 		ListCell   *cell;
 		bool		check_option = false;
-		bool		security_barrier = false;
 
 		foreach(cell, view_options)
 		{
@@ -8814,8 +8813,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 
 			if (pg_strcasecmp(defel->defname, "check_option") == 0)
 				check_option = true;
-			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
-				security_barrier = defGetBoolean(defel);
 		}
 
 		/*
@@ -8825,8 +8822,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 		if (check_option)
 		{
 			const char *view_updatable_error =
-				view_query_is_auto_updatable(view_query,
-											 security_barrier, true);
+				view_query_is_auto_updatable(view_query, true);
 
 			if (view_updatable_error)
 				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 0703c05..ff84cfb 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -314,8 +314,11 @@ DefineViewRules(Oid viewOid, Query *viewParse, bool replace)
 					   list_make1(viewParse));
 
 	/*
-	 * Someday: automatic ON INSERT, etc
+	 * No automatic ON INSERT, etc is needed, since DML can operate
+	 * directly on the subquery expansion of the SELECT view with
+	 * a little massaging. See the rewriter.
 	 */
+
 }
 
 /*---------------------------------------------------------------
@@ -396,7 +399,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	RangeVar   *view;
 	ListCell   *cell;
 	bool		check_option;
-	bool		security_barrier;
 
 	/*
 	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
@@ -451,7 +453,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	 * specified.
 	 */
 	check_option = false;
-	security_barrier = false;
 
 	foreach(cell, stmt->options)
 	{
@@ -459,8 +460,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 
 		if (pg_strcasecmp(defel->defname, "check_option") == 0)
 			check_option = true;
-		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
-			security_barrier = defGetBoolean(defel);
 	}
 
 	/*
@@ -470,7 +469,7 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	if (check_option)
 	{
 		const char *view_updatable_error =
-			view_query_is_auto_updatable(viewParse, security_barrier, true);
+			view_query_is_auto_updatable(viewParse, true);
 
 		if (view_updatable_error)
 			ereport(ERROR,
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index 3a4e836..5bd7e31 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -126,6 +126,22 @@ query_planner(PlannerInfo *root, List *tlist,
 	add_base_rels_to_query(root, (Node *) parse->jointree);
 
 	/*
+	 * The top query's resultRelation RTI may not be referenced in the query
+	 * tree its self, so we need to ensure it gets cached too. Curently arises
+	 * when we're doing DML on a view, where the injected top-level RTE for the
+	 * view's base rel isn't referenced in the query tree.
+	 *
+	 * It is marked RELOPT_RESULTREL because it isn't quite "dead" - it
+	 * must appear in the top level flattened range table of the plan tree
+	 * - but neither does it appear in the join tree and shouldn't be
+	 *   considered in path generation.
+	 */
+	if (root->parse->resultRelation && root->simple_rel_array[root->parse->resultRelation] == NULL)
+		(void) build_simple_rel(root, root->parse->resultRelation, RELOPT_RESULTREL);
+
+	Assert(root->parse->resultRelation == 0 || root->simple_rel_array[root->parse->resultRelation] != NULL);
+
+	/*
 	 * Examine the targetlist and join tree, adding entries to baserel
 	 * targetlists for all referenced Vars, and generating PlaceHolderInfo
 	 * entries for all referenced PlaceHolderVars.	Restrict and join clauses
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 5c9f3d6..f503b83 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -268,6 +268,9 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 	 * Note: this pass over the rangetable can't be combined with the previous
 	 * one, because that would mess up the numbering of the live RTEs in the
 	 * flattened rangetable.
+	 *
+	 * This *only* scans the range-table; it won't see dead subqueries in
+	 * WHERE clauses, etc.
 	 */
 	rti = 1;
 	foreach(lc, root->parse->rtable)
@@ -279,8 +282,15 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing)
 		 * pulled up into our rangetable already.  Also ignore any subquery
 		 * RTEs without matching RelOptInfos, as they likewise have been
 		 * pulled up.
+		 *
+		 * Testing simple_rel_array_size here ensures that we don't try to access
+		 * root->simple_rel_array when it isn't defined in the simple-query fast-path
+		 * where the join tree is empty - see query_planner(...) in planmain.c.
+		 * This code only ever worked without this test because we don't add dead
+		 * subqueries in WHERE/HAVING/etc to the range-table so if the join tree
+		 * was empty this condition could never be true.
 		 */
-		if (rte->rtekind == RTE_SUBQUERY && !rte->inh)
+		if (rte->rtekind == RTE_SUBQUERY && !rte->inh && root->simple_rel_array_size != 0)
 		{
 			RelOptInfo *rel = root->simple_rel_array[rti];
 
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index fb67f9e..80db564 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -35,11 +35,6 @@
 #include "parser/parse_coerce.h"
 #include "utils/rel.h"
 
-
-static List *expand_targetlist(List *tlist, int command_type,
-				  Index result_relation, List *range_table);
-
-
 /*
  * preprocess_targetlist
  *	  Driver for preprocessing the parse tree targetlist.
@@ -58,6 +53,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 	/*
 	 * Sanity check: if there is a result relation, it'd better be a real
 	 * relation not a subquery.  Else parser or rewriter messed up.
+	 * This is true even if we're updating a view/subquery; the result_relation
+	 * must still be the underlying relation RTE.
 	 */
 	if (result_relation)
 	{
@@ -193,7 +190,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
  *	  add targetlist entries for any missing attributes, and ensure the
  *	  non-junk attributes appear in proper field order.
  */
-static List *
+List *
 expand_targetlist(List *tlist, int command_type,
 				  Index result_relation, List *range_table)
 {
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 50cb753..bb32e45 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -28,6 +28,7 @@
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "nodes/print.h" /* Temporary for elog_print_display only */
 
 
 /* We use a list of these to detect recursion in RewriteQuery */
@@ -1973,7 +1974,7 @@ view_col_is_auto_updatable(RangeTblRef *rtr, TargetEntry *tle)
  * updatable.
  */
 const char *
-view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
+view_query_is_auto_updatable(Query *viewquery,
 							 bool check_cols)
 {
 	RangeTblRef *rtr;
@@ -2048,14 +2049,6 @@ view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
 		return gettext_noop("Views that return set-returning functions 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 the future.
-	 */
-	if (security_barrier)
-		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.
 	 */
@@ -2304,7 +2297,6 @@ relation_is_updatable(Oid reloid,
 		Query	   *viewquery = get_view_query(rel);
 
 		if (view_query_is_auto_updatable(viewquery,
-										 RelationIsSecurityView(rel),
 										 false) == NULL)
 		{
 			Bitmapset  *updatable_cols;
@@ -2452,15 +2444,14 @@ rewriteTargetView(Query *parsetree, Relation view)
 	RangeTblEntry *view_rte;
 	RangeTblEntry *new_rte;
 	Relation	base_rel;
-	List	   *view_targetlist;
 	ListCell   *lc;
+	const int command_type = parsetree->commandType;
 
 	/* The view must be updatable, else fail */
 	viewquery = get_view_query(view);
 
 	auto_update_detail =
 		view_query_is_auto_updatable(viewquery,
-									 RelationIsSecurityView(view),
 									 parsetree->commandType != CMD_DELETE);
 
 	if (auto_update_detail)
@@ -2589,15 +2580,32 @@ rewriteTargetView(Query *parsetree, Relation view)
 	heap_close(base_rel, NoLock);
 
 	/*
+	 * For UPDATE, add references to all columns of the base relation to the
+	 * expanded subquery if they weren't already present. We need all cols of
+	 * the base relation to generate the new tuple, even though they aren't
+	 * visible directly to the view user. These must not be revealed in
+	 * a RETURNING clause, and do *not* need the select permission bit on them
+	 * since the user won't ever actually see them.
+	 *
+	 * There's no need to sort these into the proper attribute ordering;
+	 * that'll be done for us in the next rewrite pass.
+	 */
+	if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
+		parsetree->targetList = expand_targetlist(parsetree->targetList, command_type, 
+						  						  parsetree->resultRelation, parsetree->rtable);
+
+	/*
 	 * 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.)
+	 * outer query's rangetable, then set it as the targetRelation for the query.
+	 * 
+	 * We don't do any pull-up here; the optimizer will do it for non-security-barrier
+	 * views later if it feels like it. The purpose of this RTE is to be the targetRelation,
+	 * and to be the vehicle for write-permission checks against this view.
 	 */
 	new_rte = (RangeTblEntry *) copyObject(base_rte);
 	parsetree->rtable = lappend(parsetree->rtable, new_rte);
 	new_rt_index = list_length(parsetree->rtable);
+	parsetree->resultRelation = new_rt_index;
 
 	/*
 	 * INSERTs never inherit.  For UPDATE/DELETE, we use the view query's
@@ -2607,21 +2615,6 @@ rewriteTargetView(Query *parsetree, Relation view)
 		new_rte->inh = false;
 
 	/*
-	 * 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
@@ -2650,179 +2643,123 @@ rewriteTargetView(Query *parsetree, Relation view)
 	 * 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.)
+	 * transformation.
+	 *
+	 * (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 in the main query.
-	 */
-	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);
-	}
-
-	/*
-	 * For INSERT/UPDATE, if the view has the WITH CHECK OPTION, or any parent
-	 * view specified WITH CASCADED CHECK OPTION, add the quals from the view
-	 * to the query's withCheckOptions list.
-	 */
-	if (parsetree->commandType != CMD_DELETE)
-	{
-		bool		has_wco = RelationHasCheckOption(view);
-		bool		cascaded = RelationHasCascadedCheckOption(view);
-
-		/*
-		 * If the parent view has a cascaded check option, treat this view as
-		 * if it also had a cascaded check option.
-		 *
-		 * New WithCheckOptions are added to the start of the list, so if there
-		 * is a cascaded check option, it will be the first item in the list.
-		 */
-		if (parsetree->withCheckOptions != NIL)
-		{
-			WithCheckOption *parent_wco =
-				(WithCheckOption *) linitial(parsetree->withCheckOptions);
-
-			if (parent_wco->cascaded)
-			{
-				has_wco = true;
-				cascaded = true;
-			}
-		}
-
-		/*
-		 * Add the new WithCheckOption to the start of the list, so that
-		 * checks on inner views are run before checks on outer views, as
-		 * required by the SQL standard.
-		 *
-		 * If the new check is CASCADED, we need to add it even if this view
-		 * has no quals, since there may be quals on child views.  A LOCAL
-		 * check can be omitted if this view has no quals.
-		 */
-		if (has_wco && (cascaded || viewquery->jointree->quals != NULL))
-		{
-			WithCheckOption *wco;
-
-			wco = makeNode(WithCheckOption);
-			wco->viewname = pstrdup(RelationGetRelationName(view));
-			wco->qual = NULL;
-			wco->cascaded = cascaded;
-
-			parsetree->withCheckOptions = lcons(wco,
-												parsetree->withCheckOptions);
-
-			if (viewquery->jointree->quals != NULL)
-			{
-				wco->qual = (Node *) copyObject(viewquery->jointree->quals);
-				ChangeVarNodes(wco->qual, base_rt_index, new_rt_index, 0);
-
-				/*
-				 * Make sure that the query is marked correctly if the added
-				 * qual has sublinks.  We can skip this check if the query is
-				 * already marked, or if the command is an UPDATE, in which
-				 * case the same qual will have already been added to the
-				 * query's WHERE clause, and AddQual will have already done
-				 * this check.
-				 */
-				if (!parsetree->hasSubLinks &&
-					parsetree->commandType != CMD_UPDATE)
-					parsetree->hasSubLinks = checkExprHasSubLink(wco->qual);
-			}
-		}
-	}
+												   viewquery->targetList);
+
+ 	/*
+ 	 * 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.
+ 	 *
+ 	 * Failure to perform this step would cause UPDATEs or INSERTs on 
+ 	 * a view with a different column ordering to the base relation to fail.
+ 	 */
+ 	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(viewquery->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 INSERT/UPDATE, if the view has the WITH CHECK OPTION, or any parent
+ 	 * view specified WITH CASCADED CHECK OPTION, add the quals from the view
+ 	 * to the query's withCheckOptions list.
+ 	 */
+ 	if (parsetree->commandType != CMD_DELETE)
+ 	{
+ 		bool		has_wco = RelationHasCheckOption(view);
+ 		bool		cascaded = RelationHasCascadedCheckOption(view);
+ 
+ 		/*
+ 		 * If the parent view has a cascaded check option, treat this view as
+ 		 * if it also had a cascaded check option.
+ 		 *
+ 		 * New WithCheckOptions are added to the start of the list, so if there
+ 		 * is a cascaded check option, it will be the first item in the list.
+ 		 */
+ 		if (parsetree->withCheckOptions != NIL)
+ 		{
+ 			WithCheckOption *parent_wco =
+ 				(WithCheckOption *) linitial(parsetree->withCheckOptions);
+ 
+ 			if (parent_wco->cascaded)
+ 			{
+ 				has_wco = true;
+ 				cascaded = true;
+ 			}
+ 		}
+ 
+ 		/*
+ 		 * Add the new WithCheckOption to the start of the list, so that
+ 		 * checks on inner views are run before checks on outer views, as
+ 		 * required by the SQL standard.
+ 		 *
+ 		 * If the new check is CASCADED, we need to add it even if this view
+ 		 * has no quals, since there may be quals on child views.  A LOCAL
+ 		 * check can be omitted if this view has no quals.
+ 		 */
+ 		if (has_wco && (cascaded || viewquery->jointree->quals != NULL))
+ 		{
+ 			WithCheckOption *wco;
+ 
+ 			wco = makeNode(WithCheckOption);
+ 			wco->viewname = pstrdup(RelationGetRelationName(view));
+ 			wco->qual = NULL;
+ 			wco->cascaded = cascaded;
+ 
+ 			parsetree->withCheckOptions = lcons(wco,
+ 												parsetree->withCheckOptions);
+ 
+ 			if (viewquery->jointree->quals != NULL)
+ 			{
+ 				wco->qual = (Node *) copyObject(viewquery->jointree->quals);
+ 				ChangeVarNodes(wco->qual, base_rt_index, new_rt_index, 0);
+ 
+ 				/*
+ 				 * Make sure that the query is marked correctly if the added
+ 				 * qual has sublinks.  We can skip this check if the query is
+ 				 * already marked, or if the command is an UPDATE, in which
+ 				 * case the same qual will have already been added to the
+ 				 * query's WHERE clause, and AddQual will have already done
+ 				 * this check.
+ 				 */
+ 				if (!parsetree->hasSubLinks &&
+ 					parsetree->commandType != CMD_UPDATE)
+ 					parsetree->hasSubLinks = checkExprHasSubLink(wco->qual);
+ 			}
+ 		}
+ 	}
+
+    elog_node_display(LOG, "parse_tree after upd view rewrite", parsetree,
+                      true);
 
 	return parsetree;
 }
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 6d7b594..70c2972 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -275,7 +275,7 @@ typedef struct PlannerInfo
  * is the joining of two or more base rels.  A joinrel is identified by
  * the set of RT indexes for its component baserels.  We create RelOptInfo
  * nodes for each baserel and joinrel, and store them in the PlannerInfo's
- * simple_rel_array and join_rel_list respectively.
+ * simple_rel_array and join_rel_list respectively. 
  *
  * Note that there is only one joinrel for any given set of component
  * baserels, no matter what order we assemble them in; so an unordered
@@ -283,10 +283,17 @@ typedef struct PlannerInfo
  *
  * We also have "other rels", which are like base rels in that they refer to
  * single RT indexes; but they are not part of the join tree, and are given
- * a different RelOptKind to identify them.  Lastly, there is a RelOptKind
+ * a different RelOptKind to identify them.  There is also a RelOptKind
  * for "dead" relations, which are base rels that we have proven we don't
  * need to join after all.
  *
+ * A "resultrel" (RELOPT_RESULTREL) is a relation that doesn't appear in the join
+ * tree, and is used only as the target for the ModifyTable node in an
+ * INSERT/UPDATE/DELETE. This arises when we're updating a view or subquery, as
+ * the RTE for the target is buried in a subquery and the ModifyTable node
+ * needs one at the top level. The injected RTE is marked RELOPT_RESULTREL
+ * so it isn't considered in join tree sanity checking, etc.
+ *
  * Currently the only kind of otherrels are those made for member relations
  * of an "append relation", that is an inheritance set or UNION ALL subquery.
  * An append relation has a parent RTE that is a base rel, which represents
@@ -404,7 +411,8 @@ typedef enum RelOptKind
 	RELOPT_BASEREL,
 	RELOPT_JOINREL,
 	RELOPT_OTHER_MEMBER_REL,
-	RELOPT_DEADREL
+	RELOPT_DEADREL,
+	RELOPT_RESULTREL
 } RelOptKind;
 
 typedef struct RelOptInfo
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
index 0934e63..e37b8a5 100644
--- a/src/include/optimizer/prep.h
+++ b/src/include/optimizer/prep.h
@@ -40,6 +40,9 @@ extern Expr *canonicalize_qual(Expr *qual);
  */
 extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
 
+extern List *expand_targetlist(List *tlist, int command_type,
+							   Index result_relation, List *range_table);
+
 extern PlanRowMark *get_plan_rowmark(List *rowmarks, Index rtindex);
 
 /*
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index c959590..ce56d7b 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -23,7 +23,6 @@ extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
 extern Node *build_column_default(Relation rel, int attrno);
 extern Query *get_view_query(Relation view);
 extern const char *view_query_is_auto_updatable(Query *viewquery,
-										 bool security_barrier,
 										 bool check_cols);
 extern int	relation_is_updatable(Oid reloid,
 						  bool include_triggers,
-- 
1.8.3.1

#5Craig Ringer
craig@2ndquadrant.com
In reply to: Craig Ringer (#4)
Re: varattno remapping

Hi all

I'm finding it necessary to remap the varno and varattno of Var elements
in RETURNING lists as part of updatable s.b. view processing. A
reference to a view col in RETURNING must be rewritten to point to the
new resultRelation at the top level, so that the effects of BEFORE
triggers are visible in the returned result.

Usually the view will have a different structure to the base table, and
in this case it's necessary to change the varattno of the Var to point
to the corresponding col on the base rel.

So the short version: Given the RTE for a simple view with only one base
rel and an attribute number for a col in that view, and assuming that
the view col is a simple reference to a col in the underlying base rel,
is there any sane way to get the attribute number for the corresponding
col on the base rel?

For example, given:

CREATE TABLE t (a integer, b text, c numeric);
CREATE VIEW v1 AS SELECT c, a FROM t;
UPDATE v1 SET c = NUMERIC '42' RETURNING a, c;

... the Vars for a and c in the RETURNING clause need to be remapped so
their varno is the rti for t once the RTE has been injected, and the
varattnos need changing to the corresponding attribute numbers on the
base table. So a Var for v1(c), say (1,1), must be remapped to (2,3)
i.e. varno 2 (t), varattno 3 (c).

I'm aware that this is somewhat like the var/attr twiddling being
complained about in the RLS patch. I don't see a way around it, though.
I can't push the RETURNING tlist entries down into the expanded view's
subquery and add an outer Var reference - they must point directly to
the resultRelation so that we see the effects of any BEFORE triggers.

I'm looking for advice on how to determine, given an RTI and an
attribute number for a simple view, what the attribute number of the col
in the view's base relation is. It can be assumed for this purpose that
the view attribute is a simple Var (otherwise we'll ERROR out, since we
can't update an expression).

I'm a bit stumped at this point.

I could adapt the ChangeVarNodes walker to handle the actual recursive
rewriting and Var changing. It assumes that the varattno is the same on
both the old and new varno, as it's intended for when you have an RT
index change, but could be taught to optionally remap varattnos. To do
that, though, I need a way to actually do that varattno remapping cleanly.

Anyone got any ideas?
--
Craig Ringer

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

#6Craig Ringer
craig@2ndquadrant.com
In reply to: Craig Ringer (#5)
Re: varattno remapping

On 12/24/2013 02:35 PM, Craig Ringer wrote:

So the short version: Given the RTE for a simple view with only one base
rel and an attribute number for a col in that view, and assuming that
the view col is a simple reference to a col in the underlying base rel,
is there any sane way to get the attribute number for the corresponding
col on the base rel?

So, of course, I find it as soon as I post.

map_variable_attnos(...), also in src/backend/rewrite/rewriteManip.c .

Just generate the mapping table and go.

Sorry for the noise folks.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#7Abbas Butt
abbas.butt@enterprisedb.com
In reply to: Craig Ringer (#6)
Re: varattno remapping

On Tue, Dec 24, 2013 at 11:47 AM, Craig Ringer <craig@2ndquadrant.com>wrote:

On 12/24/2013 02:35 PM, Craig Ringer wrote:

So the short version: Given the RTE for a simple view with only one base
rel and an attribute number for a col in that view, and assuming that
the view col is a simple reference to a col in the underlying base rel,
is there any sane way to get the attribute number for the corresponding
col on the base rel?

So, of course, I find it as soon as I post.

map_variable_attnos(...), also in src/backend/rewrite/rewriteManip.c .

Just generate the mapping table and go.

Could you please explain a little bit more how would you solve the posed
problem using map_variable_attnos?

I was recently working on a similar problem and used the following algo to
solve it.

I had to find to which column of the base table does a column in the select
statement of the view query belong.
To relate a target list entry in the select query of the view to an actual
column in base table here is what I did

First determine whether the var's varno refer to an RTE which is a view?
If yes then get the view query (RangeTblEntry::subquery) and see which
element in the view query's target list does this var's varattno point to.
Get the varno of that target list entry. Look for that RTE in the view's
query and see if that RTE is a real table then use that var making sure its
varno now points to the actual table.

Thanks in advance.

Sorry for the noise folks.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

--
--
*Abbas*
Architect

Ph: 92.334.5100153
Skype ID: gabbasb
www.enterprisedb.co
<http://www.enterprisedb.com/&gt;m&lt;http://www.enterprisedb.com/&gt;

*Follow us on Twitter*
@EnterpriseDB

Visit EnterpriseDB for tutorials, webinars,
whitepapers<http://www.enterprisedb.com/resources-community&gt;and
more<http://www.enterprisedb.com/resources-community&gt;

#8Craig Ringer
craig@2ndquadrant.com
In reply to: Abbas Butt (#7)
Re: varattno remapping

On 12/24/2013 03:21 PM, Abbas Butt wrote:

Could you please explain a little bit more how would you solve the posed
problem using map_variable_attnos?

I was recently working on a similar problem and used the following algo
to solve it.

I had to find to which column of the base table does a column in
the select statement of the view query belong.
To relate a target list entry in the select query of the view to
an actual column in base table.

Sounds similar. My problem is simplified by the constraint that the view
must be a simple view (only one base relation) and must only contain
simple column-references to single the base relation. No expressions,
no joins. (These are the rules for simply updatable views).

I'm new to the planner and rewriter, so don't take what I do as any kind
of example beyond "this seems to work".

I generate a varattno mapping as follows:

/*
* Scan the passed view target list, whose members must consist solely
* of Var nodes with a varno equal to the passed targetvarno.
*
* A mapping is built from the resno (i.e. tlist index) of the view
* tlist to the corresponding attribute number of the base relation
* the varattno points to.
*
* Must not be called with a targetlist containing non-Var entries.
*/
static void
gen_view_base_attr_map(List *viewtlist, AttrNumber * attnomap, int
targetvarno)
{
ListCell *lc;
TargetEntry *te;
Var *tev;
int l_viewtlist = list_length(viewtlist);

foreach(lc, viewtlist)
{
te = (TargetEntry*) lfirst(lc);
/* Could relax this in future and map only the var entries,
* ignoring everything else, but currently pointless since we
* are only interested in simple views. */
Assert(IsA(te->expr, Var));
tev = (Var*) te->expr;
Assert(tev->varno == targetvarno);
Assert(te->resno - 1 < l_viewtlist);
attnomap[te->resno - 1] = tev->varattno;
}
}

producing a forward mapping of view attno to base relation attno.

I then apply the varattno remapping, in this case to the returning list
of a DML query acting on a view, with something like:

varattno_map = palloc( list_length(viewquery->targetList) *
sizeof(AttrNumber) );

gen_view_base_attr_map(viewquery->targetList,
varattno_map, rtr->rtindex);

parsetree->returningList = map_variable_attnos(
(Node*) parsetree->returningList,
old_result_rt_index, 0,
varattno_map, list_length(viewquery->targetList),
&found_whole_row_var
);

if (found_whole_row_var)
{
/* TODO: Extend map_variable_attnos API to
pass a mutator to handle whole-row vars. */
elog(ERROR, "RETURNING list contains a whole-row variable, "
"which is not currently supported for updatable "
"views");
}

ChangeVarNodes((Node*) parsetree->returningList,
old_result_rt_index,
new_result_rt_index,
0);

I'd prefer to be doing the map_variable_attnos and ChangeVarNodes work
in a single pass, but it looks like a clumsy and verbose process to
write a new walker. So I'm going to leave it as an "opportunity for
future optimisation" for now ;-)

(As it happens that "found_whole_row_var" is a real pain; I'm going to
have to deal with a TODO item in map_variable_attnos to provide a
callback that replaces a whole-row Var with an expansion of it into a
row-expression).

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#9Craig Ringer
craig@2ndquadrant.com
In reply to: Abbas Butt (#7)
Re: varattno remapping

On 12/24/2013 03:21 PM, Abbas Butt wrote:

Could you please explain a little bit more how would you solve the posed
problem using map_variable_attnos?

It actually turns out to be even simpler, and easy to do in one pass,
when using ReplaceVarsFromTargetList .

You just generate a temporary new list of TargetEntry, with resnos
matching the attribute numbers of the view. Each contained Var points to
the remapped varno and varattno.

Then you let ReplaceVarsFromTargetList substitute Vars recursively
through the expression tree with ones in the replacement tlist you supply.

The more I've thought about it, the shorter this code has got. Currently:

/*
* Scan the passed view target list, whose members must consist solely
* of Var nodes with a varno equal to the passed targetvarno, and
* produce a targetlist of Var nodes with the corresponding varno and
* varattno of the base relation 'targetvarno'.
*
* This tlist is used when remapping Vars that point to a view so they
* point to the base relation of the view instead. It is entirely
* newly allocated. The result tlist is not in resno order.
*
* Must not be called with a targetlist containing non-Var entries.
*/
static List *
gen_view_base_attr_map(List *viewtlist, int targetvarno, int newvarno)
{
ListCell *lc;
TargetEntry *te, *newte;
Var *tev, *newvar;
int l_viewtlist = list_length(viewtlist);
List *newtlist = NIL;

foreach(lc, viewtlist)
{
te = (TargetEntry*) lfirst(lc);
/* Could relax this in future and map only the var entries,
* ignoring everything else, but currently pointless since we
* are only interested in simple views. */
Assert(IsA(te->expr, Var));
tev = (Var*) te->expr;
Assert(tev->varno == targetvarno);
Assert(te->resno - 1 < l_viewtlist);

/* Construct the new Var with the remapped attno and varno */
newvar = (Var*) palloc(sizeof(Var));
*newvar = *tev;
newvar->varno = newvarno;
newvar->varattno = tev->varattno;

/* and wrap it in a new tle to cons to the list */
newte = flatCopyTargetEntry(te);
newte->expr = (Expr*) newvar;
newtlist = lcons(newte, newtlist);
}

return newtlist;
}

and invocation:

/*
* We need to adjust any RETURNING clause entries to point to the new
* target RTE instead of the old one so that we see the effects of
* BEFORE triggers. Varattnos must be remapped so that the new Var
* points to the correct col of the base rel, since the view will
* usually have a different set of columns / column order.
*
* As part of this pass any whole-row references to the view are
* expanded into ROW(...) expressions to ensure we don't expose
* columns that are not visible through the view, and to make sure
* the client gets the result type it expected.
*/

List * remap_tlist = gen_view_base_attr_map(
viewquery->targetList, rtr->rtindex, new_result_rt_index);

parsetree->returningList = ReplaceVarsFromTargetList(
(Node*) parsetree->returningList,
old_result_rt_index, 0 /*sublevels_up*/,
view_rte,
remap_tlist,
REPLACEVARS_REPORT_ERROR, 0 /*nomatch_varno, unused */,
NULL /* outer_hasSubLinks, unused */
);

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#10Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#9)
Re: varattno remapping

On 24 December 2013 12:12, Craig Ringer <craig@2ndquadrant.com> wrote:

On 12/24/2013 03:21 PM, Abbas Butt wrote:

Could you please explain a little bit more how would you solve the posed
problem using map_variable_attnos?

It actually turns out to be even simpler, and easy to do in one pass,
when using ReplaceVarsFromTargetList .

You just generate a temporary new list of TargetEntry, with resnos
matching the attribute numbers of the view. Each contained Var points to
the remapped varno and varattno.

Then you let ReplaceVarsFromTargetList substitute Vars recursively
through the expression tree with ones in the replacement tlist you supply.

The more I've thought about it, the shorter this code has got. Currently:

/*
* Scan the passed view target list, whose members must consist solely
* of Var nodes with a varno equal to the passed targetvarno, and
* produce a targetlist of Var nodes with the corresponding varno and
* varattno of the base relation 'targetvarno'.
*
* This tlist is used when remapping Vars that point to a view so they
* point to the base relation of the view instead. It is entirely
* newly allocated. The result tlist is not in resno order.
*
* Must not be called with a targetlist containing non-Var entries.
*/
static List *
gen_view_base_attr_map(List *viewtlist, int targetvarno, int newvarno)
{
ListCell *lc;
TargetEntry *te, *newte;
Var *tev, *newvar;
int l_viewtlist = list_length(viewtlist);
List *newtlist = NIL;

foreach(lc, viewtlist)
{
te = (TargetEntry*) lfirst(lc);
/* Could relax this in future and map only the var entries,
* ignoring everything else, but currently pointless since we
* are only interested in simple views. */
Assert(IsA(te->expr, Var));
tev = (Var*) te->expr;
Assert(tev->varno == targetvarno);
Assert(te->resno - 1 < l_viewtlist);

/* Construct the new Var with the remapped attno and varno */
newvar = (Var*) palloc(sizeof(Var));
*newvar = *tev;
newvar->varno = newvarno;
newvar->varattno = tev->varattno;

/* and wrap it in a new tle to cons to the list */
newte = flatCopyTargetEntry(te);
newte->expr = (Expr*) newvar;
newtlist = lcons(newte, newtlist);
}

return newtlist;
}

I don't think this bit is quite right.

It's not correct to assume that all the view columns are simple
references to columns of the base relation --- auto-updatable views
may now contain a mix of updatable and non-updatable columns, so some
of the view columns may be arbitrary expressions.

There is already code in rewriteTargetView() that does something very
similar (to the whole parsetree, rather than just the returning list)
with 2 function calls:

/*
* 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);

and then later:

/*
* 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);

Regards,
Dean

and invocation:

/*
* We need to adjust any RETURNING clause entries to point to the new
* target RTE instead of the old one so that we see the effects of
* BEFORE triggers. Varattnos must be remapped so that the new Var
* points to the correct col of the base rel, since the view will
* usually have a different set of columns / column order.
*
* As part of this pass any whole-row references to the view are
* expanded into ROW(...) expressions to ensure we don't expose
* columns that are not visible through the view, and to make sure
* the client gets the result type it expected.
*/

List * remap_tlist = gen_view_base_attr_map(
viewquery->targetList, rtr->rtindex, new_result_rt_index);

parsetree->returningList = ReplaceVarsFromTargetList(
(Node*) parsetree->returningList,
old_result_rt_index, 0 /*sublevels_up*/,
view_rte,
remap_tlist,
REPLACEVARS_REPORT_ERROR, 0 /*nomatch_varno, unused */,
NULL /* outer_hasSubLinks, unused */
);

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

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

#11Craig Ringer
craig@2ndquadrant.com
In reply to: Dean Rasheed (#10)
Re: varattno remapping

On 12/24/2013 11:17 PM, Dean Rasheed wrote:

I don't think this bit is quite right.

It's not correct to assume that all the view columns are simple
references to columns of the base relation --- auto-updatable views
may now contain a mix of updatable and non-updatable columns, so some
of the view columns may be arbitrary expressions.

Ah - it looks like I'd checked against 9.3 and missed the relaxation of
those requirements.

There is already code in rewriteTargetView() that does something very
similar (to the whole parsetree, rather than just the returning list)
with 2 function calls:

Copying the view tlist and then adjusting it is a much smarter way to do
it. I should've seen that the pull-up code could be adapted to deal with
the RETURNING list, so thankyou.

It's a cleaner way to do it.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#12Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#4)
1 attachment(s)
Re: WIP patch (v2) for updatable security barrier views

On 13 December 2013 13:52, Craig Ringer <craig@2ndquadrant.com> wrote:

Hi all

Here's an updated version of the updatable security barrier views patch
that's proposed as the next stage of progressing row-security toward
useful and maintainable inclusion in core.

If updatable security barrier views are available then the row-security
code won't have to play around with remapping vars and attrs during
query planning when it injects its subquery late. We'll simply stop the
planner flattening an updatable security barrier view, and for
row-security we'll dynamically inject one during rewriting when we see a
relation that has a row-security attribute.

Hi,

I've been thinking about this some more and I don't think you can
avoid the need to remap vars and attrs.

AIUI, your modified version of rewriteTargetView() will turn an UPDATE
query with a rangetable of the form

rtable:
1: relation RTE (view) <- resultRelation
fromList: [1]

into one of the form

rtable:
1: relation RTE (view)
2: relation RTE (base table) <- resultRelation
fromList: [1]

which will then get transformed later in the rewriter by
fireRIRrules() into

rtable:
1: subquery RTE (expanded view)
2: relation RTE (base table) <- resultRelation
fromList: [1]

The problem is that the subquery RTE will only expose the columns of
the view, which doesn't necessarily include all the columns from the
base table. These are required for UPDATE, for example:

create table t(a int, b int);
insert into t values(1, 2);
create view v with (security_barrier=true) as select a from t;
update v set a=a+1;
ERROR: invalid attribute number 2

The columns actually output from the subquery are:

explain (verbose, costs off) update v set a=a+1;
QUERY PLAN
--------------------------------------------------
Update on public.t
-> Subquery Scan on v
Output: (v.a + 1), t.b, v.*, t.ctid, v.*
-> Seq Scan on public.t t_1
Output: t_1.a

which is clearly broken.

So more code will be needed to add the right set of columns to that
subquery RTE, and that's where it will need to mess with the mapping
between columns of the view and columns of the underlying base
relation.

[snip] Needs some
changes in the rewriter where expand_target_list is called.

It doesn't look right to be calling expand_target_list() from the
rewriter. It looks like you are calling it before the rangetable
mangling, so all it will do is add targetlist entries for columns of
the view, not for columns of the base relation as the preceding
comment suggests. I think that explains the EXPLAIN output above.

A related problem is that the base table may be the parent of an
inheritance set, in which case attempting to add all the required
columns here in the rewriter is never going to work, because the
inheritance set hasn't been expanded yet, and so the columns of child
tables will be missed.

Normally expand_target_list() is only called from the planner, after
expand_inherited_tables(), at which point it's working with a subplan
with the appropriate parent/child relation, and so it sees the correct
set of columns.

The more I think about this, the more I think that the approach of my
original patch was neater. The idea was to have 2 new pieces of code:

1). In rewriteTargetView() decorate the target RTE with any security
barrier quals (in the new rte->securityQuals field), instead of
merging them with the main query's quals. So the output of this
stage of rewriting would be something like

rtable:
1: relation RTE (base table) <- resultRelation
- securityQuals = [view quals]
fromList: [1]

2). After all rewriting is complete, scan the query and turn all
relation RTEs with non-empty securityQuals into subquery RTEs
(making a copy of the original RTE in the case of the result
relation).

A future RLS patch would then be able to make use of this simply by
adding its own securityQuals to the relevant RTEs and letting (2)
expand them.

The problem with my earlier patch was that it was calling the subquery
expansion code (2) in the final stage of the rewriter. In light of the
above, that really needs to happen after expand_inherited_tables() so
that it can see the correct parent/child base relation. Another ugly
feature of my earlier patch was the changes it made to
expand_target_list() and the need to track the query's sourceRelation.
Both of those things can be avoided simply by moving the subquery
expansion code (2) to after expand_target_list(), and hence also after
expand_inherited_tables().

Attached is an updated version of my earlier patch with the subquery
expansion code (2) moved into the planner, in a new file
optimizer/prep/prepsecurity.c (it didn't seem to obviously belong in
any of the existing files) and invoked after calling
preprocess_targetlist(). It turns out that this also allows that new
code to be tidied up a bit, and it is easy for it to work out which
attributes are actually required and only include the minimum
necessary set of attributes in the subquery.

Also, since this is now all happening after inheritance expansion, the
subplan's subquery is built with just the relevant parent/child
relation, rather than the complete hierarchy. For example:

create table parent_tbl(a int);
insert into parent_tbl select * from generate_series(1,1000);
create table child_tbl(b int) inherits (parent_tbl);
insert into child_tbl select i,i*10 from generate_series(1001,2000) g(i);
create index child_tbl_idx on child_tbl(a);

create view v with (security_barrier=true)
as select * from parent_tbl where a > 0;

explain (verbose, costs off) update v set a=a+1 where a=1500;
QUERY PLAN
------------------------------------------------------------------------------
Update on public.parent_tbl parent_tbl_2
-> Subquery Scan on parent_tbl
Output: (parent_tbl.a + 1), parent_tbl.ctid
-> Seq Scan on public.parent_tbl parent_tbl_3
Output: parent_tbl_3.a, parent_tbl_3.ctid
Filter: ((parent_tbl_3.a > 0) AND (parent_tbl_3.a = 1500))
-> Subquery Scan on parent_tbl_1
Output: (parent_tbl_1.a + 1), parent_tbl_1.b, parent_tbl_1.ctid
-> Bitmap Heap Scan on public.child_tbl
Output: child_tbl.a, child_tbl.ctid, child_tbl.b
Recheck Cond: ((child_tbl.a > 0) AND (child_tbl.a = 1500))
-> Bitmap Index Scan on child_tbl_idx
Index Cond: ((child_tbl.a > 0) AND (child_tbl.a = 1500))

where the second subquery is for updating child_tbl, and has a
different set of columns (and a different access path in this case).
OTOH, your patch generates the following plan for this query:

Update on public.parent_tbl
-> Subquery Scan on v
Output: (v.a + 1), v.*, parent_tbl.ctid, v.*
-> Append
-> Seq Scan on public.parent_tbl parent_tbl_1
Output: parent_tbl_1.a
Filter: ((parent_tbl_1.a > 0) AND (parent_tbl_1.a = 1500))
-> Bitmap Heap Scan on public.child_tbl
Output: child_tbl.a
Recheck Cond: ((child_tbl.a > 0) AND (child_tbl.a = 1500))
-> Bitmap Index Scan on child_tbl_idx
Index Cond: ((child_tbl.a > 0) AND
(child_tbl.a = 1500))
-> Subquery Scan on v_1
Output: (v_1.a + 1), b, v_1.*, ctid, v_1.*
-> Append
-> Seq Scan on public.parent_tbl parent_tbl_2
Output: parent_tbl_2.a
Filter: ((parent_tbl_2.a > 0) AND (parent_tbl_2.a = 1500))
-> Bitmap Heap Scan on public.child_tbl child_tbl_1
Output: child_tbl_1.a
Recheck Cond: ((child_tbl_1.a > 0) AND
(child_tbl_1.a = 1500))
-> Bitmap Index Scan on child_tbl_idx
Index Cond: ((child_tbl_1.a > 0) AND
(child_tbl_1.a = 1500))

which is the wrong set of rows (and columns) in each subquery and it
crashes when it is run.

There is still a lot more testing to be done with my patch, so there
may well be unforeseen problems, but it feels like a cleaner, more
straightforward approach.

Thoughts?

Regards,
Dean

Attachments:

updatable-sb-views.patchtext/x-diff; charset=US-ASCII; name=updatable-sb-views.patchDownload
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
new file mode 100644
index e0fbe1e..888410f
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
*************** CREATE VIEW vista AS SELECT text 'Hello
*** 323,334 ****
         or set-returning functions.
        </para>
       </listitem>
- 
-      <listitem>
-       <para>
-        The view must not have the <literal>security_barrier</> property.
-       </para>
-      </listitem>
      </itemizedlist>
     </para>
  
--- 323,328 ----
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index b9cd88d..a984c03
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** ATExecSetRelOptions(Relation rel, List *
*** 8806,8812 ****
  		List	   *view_options = untransformRelOptions(newOptions);
  		ListCell   *cell;
  		bool		check_option = false;
- 		bool		security_barrier = false;
  
  		foreach(cell, view_options)
  		{
--- 8806,8811 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8814,8821 ****
  
  			if (pg_strcasecmp(defel->defname, "check_option") == 0)
  				check_option = true;
- 			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 				security_barrier = defGetBoolean(defel);
  		}
  
  		/*
--- 8813,8818 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8825,8832 ****
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query,
! 											 security_barrier, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
--- 8822,8828 ----
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
new file mode 100644
index 0703c05..8a43aa4
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
*************** DefineView(ViewStmt *stmt, const char *q
*** 396,402 ****
  	RangeVar   *view;
  	ListCell   *cell;
  	bool		check_option;
- 	bool		security_barrier;
  
  	/*
  	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
--- 396,401 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 451,457 ****
  	 * specified.
  	 */
  	check_option = false;
- 	security_barrier = false;
  
  	foreach(cell, stmt->options)
  	{
--- 450,455 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 459,466 ****
  
  		if (pg_strcasecmp(defel->defname, "check_option") == 0)
  			check_option = true;
- 		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 			security_barrier = defGetBoolean(defel);
  	}
  
  	/*
--- 457,462 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 470,476 ****
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, security_barrier, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
--- 466,472 ----
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index e4184c5..255ade9
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 1998,2003 ****
--- 1998,2004 ----
  	COPY_SCALAR_FIELD(checkAsUser);
  	COPY_BITMAPSET_FIELD(selectedCols);
  	COPY_BITMAPSET_FIELD(modifiedCols);
+ 	COPY_NODE_FIELD(securityQuals);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 0cdb947..cab71ac
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2278,2283 ****
--- 2278,2284 ----
  	COMPARE_SCALAR_FIELD(checkAsUser);
  	COMPARE_BITMAPSET_FIELD(selectedCols);
  	COMPARE_BITMAPSET_FIELD(modifiedCols);
+ 	COMPARE_NODE_FIELD(securityQuals);
  
  	return true;
  }
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 17626f9..9bb62ba
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** range_table_walker(List *rtable,
*** 2020,2025 ****
--- 2020,2028 ----
  					return true;
  				break;
  		}
+ 
+ 		if (walker(rte->securityQuals, context))
+ 			return true;
  	}
  	return false;
  }
*************** range_table_mutator(List *rtable,
*** 2755,2760 ****
--- 2758,2764 ----
  				MUTATE(newrte->values_lists, rte->values_lists, List *);
  				break;
  		}
+ 		MUTATE(newrte->securityQuals, rte->securityQuals, List *);
  		newrt = lappend(newrt, newrte);
  	}
  	return newrt;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 4f63906..43502b0
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outRangeTblEntry(StringInfo str, const
*** 2409,2414 ****
--- 2409,2415 ----
  	WRITE_OID_FIELD(checkAsUser);
  	WRITE_BITMAPSET_FIELD(selectedCols);
  	WRITE_BITMAPSET_FIELD(modifiedCols);
+ 	WRITE_NODE_FIELD(securityQuals);
  }
  
  static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index aba6d4e..941c038
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readRangeTblEntry(void)
*** 1250,1255 ****
--- 1250,1256 ----
  	READ_OID_FIELD(checkAsUser);
  	READ_BITMAPSET_FIELD(selectedCols);
  	READ_BITMAPSET_FIELD(modifiedCols);
+ 	READ_NODE_FIELD(securityQuals);
  
  	READ_DONE();
  }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 1da4b2f..93097b0
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 916,921 ****
--- 916,927 ----
  		subplan = grouping_planner(&subroot, 0.0 /* retrieve all tuples */ );
  
  		/*
+ 		 * Planning may have modified the query result relation (if there
+ 		 * were security barrier quals on the result RTE).
+ 		 */
+ 		appinfo->child_relid = subroot.parse->resultRelation;
+ 
+ 		/*
  		 * If this child rel was excluded by constraint exclusion, exclude it
  		 * from the result plan.
  		 */
*************** inheritance_planner(PlannerInfo *root)
*** 932,940 ****
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 			final_rtable = list_concat(final_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
--- 938,969 ----
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 		{
! 			List	   *tmp_rtable = NIL;
! 			ListCell   *cell1, *cell2;
! 
! 			/*
! 			 * Planning this new child may have turned some of the original
! 			 * RTEs into subqueries (if they had security barrier quals). If
! 			 * so, we want to use these in the final rtable.
! 			 */
! 			forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
! 			{
! 				RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
! 				RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
! 
! 				if (rte1->rtekind == RTE_RELATION &&
! 					rte1->securityQuals != NIL &&
! 					rte2->rtekind == RTE_SUBQUERY)
! 					tmp_rtable = lappend(tmp_rtable, rte2);
! 				else
! 					tmp_rtable = lappend(tmp_rtable, rte1);
! 			}
! 
! 			final_rtable = list_concat(tmp_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
+ 		}
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
*************** grouping_planner(PlannerInfo *root, doub
*** 1163,1168 ****
--- 1192,1203 ----
  		tlist = preprocess_targetlist(root, tlist);
  
  		/*
+ 		 * Expand any rangetable entries that have security barrier quals.
+ 		 * This may add new security barrier subquery RTEs to the rangetable.
+ 		 */
+ 		expand_security_quals(root, tlist);
+ 
+ 		/*
  		 * Locate any window functions in the tlist.  (We don't need to look
  		 * anywhere else, since expressions used in ORDER BY will be in there
  		 * too.)  Note that they could all have been eliminated by constant
diff --git a/src/backend/optimizer/prep/Makefile b/src/backend/optimizer/prep/Makefile
new file mode 100644
index 86301bf..5195d9b
*** a/src/backend/optimizer/prep/Makefile
--- b/src/backend/optimizer/prep/Makefile
*************** subdir = src/backend/optimizer/prep
*** 12,17 ****
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
--- 12,17 ----
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o prepsecurity.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
new file mode 100644
index ...e7a3f56
*** a/src/backend/optimizer/prep/prepsecurity.c
--- b/src/backend/optimizer/prep/prepsecurity.c
***************
*** 0 ****
--- 1,423 ----
+ /*-------------------------------------------------------------------------
+  *
+  * prepsecurity.c
+  *	  Routines for preprocessing security barrier quals.
+  *
+  * Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
+  * Portions Copyright (c) 1994, Regents of the University of California
+  *
+  *
+  * IDENTIFICATION
+  *	  src/backend/optimizer/prep/prepsecurity.c
+  *
+  *-------------------------------------------------------------------------
+  */
+ #include "postgres.h"
+ 
+ #include "access/heapam.h"
+ #include "access/sysattr.h"
+ #include "catalog/heap.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/prep.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteManip.h"
+ #include "utils/rel.h"
+ 
+ 
+ typedef struct
+ {
+ 	int			rt_index;		/* Index of security barrier RTE */
+ 	int			sublevels_up;	/* Current nesting depth */
+ 	Relation	rel;			/* RTE relation at rt_index */
+ 	List	   *targetlist;		/* Targetlist for new subquery RTE */
+ 	List	   *colnames;		/* Column names in subquery RTE */
+ 	List	   *vars_processed;	/* List of Vars already processed */
+ } security_barrier_replace_vars_context;
+ 
+ static void expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual);
+ 
+ static void security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context);
+ 
+ static bool security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context);
+ 
+ 
+ /*
+  * expand_security_quals -
+  *	  expands any security barrier quals on RTEs in the query rtable, turning
+  *	  them into security barrier subqueries.
+  *
+  * Any given RTE may have multiple security barrier quals in a list, from which
+  * we create a set of nested subqueries to isolate each security barrier from
+  * the others, providing protection against malicious user-defined security
+  * barriers.  The first security barrier qual in the list will be used in the
+  * innermost subquery.
+  */
+ void
+ expand_security_quals(PlannerInfo *root, List *tlist)
+ {
+ 	Query	   *parse = root->parse;
+ 	int			rt_index;
+ 
+ 	/*
+ 	 * Process each RTE in the rtable list.
+ 	 *
+ 	 * Note that this is deliberately not a foreach loop, since the rtable may
+ 	 * be modified each time through the loop.
+ 	 */
+ 	rt_index = 0;
+ 	while (rt_index < list_length(parse->rtable))
+ 	{
+ 		RangeTblEntry *rte;
+ 
+ 		rt_index++;
+ 		rte = rt_fetch(rt_index, parse->rtable);
+ 
+ 		if (rte->securityQuals == NIL)
+ 			continue;
+ 
+ 		/*
+ 		 * Ignore any RTEs that aren't used in the query (such RTEs may be
+ 		 * present for permissions checks).
+ 		 */
+ 		if (rt_index != parse->resultRelation &&
+ 			!rangeTableEntry_used((Node *) parse, rt_index, 0))
+ 			continue;
+ 
+ 		/*
+ 		 * If this RTE is the target then we need to make a copy of it before
+ 		 * expanding it.  The unexpanded copy will become the new target, and
+ 		 * the original RTE will be expanded to become the source of rows to
+ 		 * update/delete.
+ 		 */
+ 		if (rt_index == parse->resultRelation)
+ 		{
+ 			RangeTblEntry *newrte = copyObject(rte);
+ 			parse->rtable = lappend(parse->rtable, newrte);
+ 			parse->resultRelation = list_length(parse->rtable);
+ 
+ 			/*
+ 			 * Wipe out any copied security barrier quals on the new target to
+ 			 * prevent infinite recursion.
+ 			 */
+ 			newrte->securityQuals = NIL;
+ 
+ 			/*
+ 			 * There's no need to do permissions checks twice, so wipe out the
+ 			 * permissions info for the original RTE (we prefer to keep the
+ 			 * bits set on the result RTE).
+ 			 */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * For the most part, Vars referencing the original relation should
+ 			 * remain as they are, meaning that they pull OLD values from the
+ 			 * expanded RTE.  But in the RETURNING list and in any WITH CHECK
+ 			 * OPTION quals, we want such Vars to represent NEW values, so
+ 			 * change them to reference the new RTE.
+ 			 */
+ 			ChangeVarNodes((Node *) parse->returningList, rt_index,
+ 						   parse->resultRelation, 0);
+ 
+ 			ChangeVarNodes((Node *) parse->withCheckOptions, rt_index,
+ 						   parse->resultRelation, 0);
+ 		}
+ 
+ 		/*
+ 		 * Process each security barrier qual in turn, starting with the
+ 		 * innermost one (the first in the list) and working outwards.
+ 		 *
+ 		 * We remove each qual from the list before processing it, so that its
+ 		 * variables aren't modified by expand_security_qual.  Also we don't
+ 		 * necessarily want the attributes referred to by the qual to be
+ 		 * exposed by the newly built subquery.
+ 		 */
+ 		while (rte->securityQuals != NIL)
+ 		{
+ 			Node   *qual = (Node *) linitial(rte->securityQuals);
+ 			rte->securityQuals = list_delete_first(rte->securityQuals);
+ 
+ 			ChangeVarNodes(qual, rt_index, 1, 0);
+ 			expand_security_qual(parse, tlist, rt_index, qual);
+ 		}
+ 	}
+ }
+ 
+ 
+ /*
+  * expand_security_qual -
+  *	  expand the specified security barrier qual on a query RTE, turning the
+  *	  RTE into a security barrier subquery.
+  */
+ static void
+ expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual)
+ {
+ 	RangeTblEntry  *rte;
+ 	Oid				relid;
+ 	Query		   *subquery;
+ 	RangeTblEntry  *subrte;
+ 	RangeTblRef	   *subrtr;
+ 	security_barrier_replace_vars_context context;
+ 	ListCell	   *cell;
+ 
+ 	rte = rt_fetch(rt_index, parse->rtable);
+ 	relid = rte->relid;
+ 
+ 	/*
+ 	 * There should only be 2 possible cases:
+ 	 *
+ 	 * 1. A relation RTE, which we turn into a subquery RTE containing all
+ 	 * referenced columns.
+ 	 *
+ 	 * 2. A subquery RTE (either from a prior call to this function or from an
+ 	 * expanded view).  In this case we build a new subquery on top of it to
+ 	 * isolate this security barrier qual from any other quals.
+ 	 */
+ 	switch (rte->rtekind)
+ 	{
+ 		case RTE_RELATION:
+ 			/*
+ 			 * Turn the relation RTE into a security barrier subquery RTE,
+ 			 * moving all permissions checks down into the subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 
+ 			subrte = copyObject(rte);
+ 			subrte->inFromCl = true;
+ 			subrte->securityQuals = NIL;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->rtekind = RTE_SUBQUERY;
+ 			rte->relid = InvalidOid;
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 			rte->inh = false;			/* must not be set for a subquery */
+ 
+ 			/* the permissions checks have now been moved down */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * Replace any variables in the outer query that refer to the
+ 			 * original relation RTE with references to columns that we will
+ 			 * expose in the new subquery, building the subquery's targetlist
+ 			 * as we go.
+ 			 */
+ 			context.rt_index = rt_index;
+ 			context.sublevels_up = 0;
+ 			context.rel = heap_open(relid, NoLock);
+ 			context.targetlist = NIL;
+ 			context.colnames = NIL;
+ 			context.vars_processed = NIL;
+ 
+ 			security_barrier_replace_vars((Node *) parse, &context);
+ 			security_barrier_replace_vars((Node *) tlist, &context);
+ 
+ 			heap_close(context.rel, NoLock);
+ 
+ 			/* Now we know what columns the subquery needs to expose */
+ 			rte->subquery->targetList = context.targetlist;
+ 			rte->eref = makeAlias(rte->eref->aliasname, context.colnames);
+ 
+ 			break;
+ 
+ 		case RTE_SUBQUERY:
+ 			/*
+ 			 * Build a new subquery that includes all the same columns as the
+ 			 * original subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 			subquery->targetList = NIL;
+ 
+ 			foreach(cell, rte->subquery->targetList)
+ 			{
+ 				TargetEntry	   *tle;
+ 				Var			   *var;
+ 
+ 				tle = (TargetEntry *) lfirst(cell);
+ 				var = makeVarFromTargetEntry(1, tle);
+ 
+ 				tle = makeTargetEntry((Expr *) var,
+ 									  list_length(subquery->targetList) + 1,
+ 									  pstrdup(tle->resname),
+ 									  tle->resjunk);
+ 				subquery->targetList = lappend(subquery->targetList, tle);
+ 			}
+ 
+ 			subrte = makeNode(RangeTblEntry);
+ 			subrte->rtekind = RTE_SUBQUERY;
+ 			subrte->subquery = rte->subquery;
+ 			subrte->security_barrier = rte->security_barrier;
+ 			subrte->eref = copyObject(rte->eref);
+ 			subrte->inFromCl = true;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 
+ 			break;
+ 
+ 		default:
+ 			elog(ERROR, "invalid range table entry for security barrier qual");
+ 	}
+ }
+ 
+ 
+ /*
+  * security_barrier_replace_vars -
+  *	  Apply security barrier variable replacement to an expression tree.
+  *
+  * This also builds/updates a targetlist with entries for each replacement
+  * variable that needs to be exposed by the security barrier subquery RTE.
+  *
+  * NOTE: although this has the form of a walker, we cheat and modify the
+  * nodes in-place.	The given expression tree should have been copied
+  * earlier to ensure that no unwanted side-effects occur!
+  */
+ static void
+ security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context)
+ {
+ 	/*
+ 	 * Must be prepared to start with a Query or a bare expression tree; if
+ 	 * it's a Query, go straight to query_tree_walker to make sure that
+ 	 * sublevels_up doesn't get incremented prematurely.
+ 	 */
+ 	if (node && IsA(node, Query))
+ 		query_tree_walker((Query *) node,
+ 						  security_barrier_replace_vars_walker,
+ 						  (void *) context, 0);
+ 	else
+ 		security_barrier_replace_vars_walker(node, context);
+ }
+ 
+ static bool
+ security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context)
+ {
+ 	if (node == NULL)
+ 		return false;
+ 	if (IsA(node, Var))
+ 	{
+ 		Var		   *var = (Var *) node;
+ 
+ 		/*
+ 		 * Note that the same Var may be present in different lists, so we
+ 		 * need to take care not to process it multiple times.
+ 		 */
+ 		if (var->varno == context->rt_index &&
+ 			var->varlevelsup == context->sublevels_up &&
+ 			!list_member_ptr(context->vars_processed, var))
+ 		{
+ 			/*
+ 			 * Found a matching variable. Make sure that it is in the subquery
+ 			 * targetlist and map its attno accordingly.
+ 			 */
+ 			AttrNumber	attno;
+ 			ListCell   *l;
+ 			TargetEntry *tle;
+ 			char	   *attname;
+ 			Var		   *newvar;
+ 
+ 			/* Search for the base attribute in the subquery targetlist */
+ 			attno = InvalidAttrNumber;
+ 			foreach(l, context->targetlist)
+ 			{
+ 				tle = (TargetEntry *) lfirst(l);
+ 				attno++;
+ 
+ 				Assert(IsA(tle->expr, Var));
+ 				if (((Var *) tle->expr)->varattno == var->varattno &&
+ 					((Var *) tle->expr)->varcollid == var->varcollid)
+ 				{
+ 					/* Map the variable onto this subquery targetlist entry */
+ 					var->varattno = attno;
+ 					return false;
+ 				}
+ 			}
+ 
+ 			/* Not in the subquery targetlist, so add it. Get its name. */
+ 			if (var->varattno < 0)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = SystemAttributeDefinition(var->varattno,
+ 													context->rel->rd_rel->relhasoids);
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else if (var->varattno == InvalidAttrNumber)
+ 			{
+ 				attname = "wholerow";
+ 			}
+ 			else if (var->varattno <= context->rel->rd_att->natts)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = context->rel->rd_att->attrs[var->varattno - 1];
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else
+ 			{
+ 				elog(ERROR, "invalid attribute number %d in security_barrier_replace_vars", var->varattno);
+ 			}
+ 
+ 			/* New variable for subquery targetlist */
+ 			newvar = copyObject(var);
+ 			newvar->varno = 1;
+ 
+ 			attno = list_length(context->targetlist) + 1;
+ 			tle = makeTargetEntry((Expr *) newvar,
+ 								  attno,
+ 								  pstrdup(attname),
+ 								  false);
+ 
+ 			context->targetlist = lappend(context->targetlist, tle);
+ 
+ 			context->colnames = lappend(context->colnames,
+ 										makeString(pstrdup(attname)));
+ 
+ 			/* Update the outer query's variable */
+ 			var->varattno = attno;
+ 
+ 			/* Remember this Var so that we don't process it again */
+ 			context->vars_processed = lappend(context->vars_processed, var);
+ 		}
+ 		return false;
+ 	}
+ 
+ 	if (IsA(node, Query))
+ 	{
+ 		/* Recurse into subselects */
+ 		bool		result;
+ 
+ 		context->sublevels_up++;
+ 		result = query_tree_walker((Query *) node,
+ 								   security_barrier_replace_vars_walker,
+ 								   (void *) context, 0);
+ 		context->sublevels_up--;
+ 		return result;
+ 	}
+ 	return expression_tree_walker(node, security_barrier_replace_vars_walker,
+ 								  (void *) context);
+ }
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 50cb753..8474cae
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** view_col_is_auto_updatable(RangeTblRef *
*** 1973,1980 ****
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
! 							 bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
--- 1973,1979 ----
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
*************** view_query_is_auto_updatable(Query *view
*** 2048,2061 ****
  		return gettext_noop("Views that return set-returning functions 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 the future.
- 	 */
- 	if (security_barrier)
- 		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.
  	 */
--- 2047,2052 ----
*************** relation_is_updatable(Oid reloid,
*** 2303,2311 ****
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery,
! 										 RelationIsSecurityView(rel),
! 										 false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
--- 2294,2300 ----
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery, false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
*************** rewriteTargetView(Query *parsetree, Rela
*** 2460,2466 ****
  
  	auto_update_detail =
  		view_query_is_auto_updatable(viewquery,
- 									 RelationIsSecurityView(view),
  									 parsetree->commandType != CMD_DELETE);
  
  	if (auto_update_detail)
--- 2449,2454 ----
*************** rewriteTargetView(Query *parsetree, Rela
*** 2664,2669 ****
--- 2652,2665 ----
  												   view_targetlist);
  
  	/*
+ 	 * Move any security barrier quals from the view RTE onto the new target
+ 	 * RTE.  Any such quals should now apply to the new target RTE and will not
+ 	 * reference the original view RTE in the rewritten query.
+ 	 */
+ 	new_rte->securityQuals = view_rte->securityQuals;
+ 	view_rte->securityQuals = NIL;
+ 
+ 	/*
  	 * 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.
*************** rewriteTargetView(Query *parsetree, Rela
*** 2743,2748 ****
--- 2739,2748 ----
  	 * only adjust their varnos to reference the new target (just the same as
  	 * we did with the view targetlist).
  	 *
+ 	 * Note that there is special-case handling for the quals of a security
+ 	 * barrier view, since they need to be kept separate from any user-supplied
+ 	 * quals, so these quals are kept on the new target RTE.
+ 	 *
  	 * For INSERT, the view's quals can be ignored in the main query.
  	 */
  	if (parsetree->commandType != CMD_INSERT &&
*************** rewriteTargetView(Query *parsetree, Rela
*** 2751,2757 ****
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 		AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
--- 2751,2768 ----
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 
! 		if (RelationIsSecurityView(view))
! 		{
! 			/*
! 			 * Note: the parsetree has been mutated, so the new_rte pointer is
! 			 * stale and needs to be re-computed.
! 			 */
! 			new_rte = rt_fetch(new_rt_index, parsetree->rtable);
! 			new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals);
! 		}
! 		else
! 			AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
*************** rewriteTargetView(Query *parsetree, Rela
*** 2812,2824 ****
  				/*
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
! 				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added to the
! 				 * query's WHERE clause, and AddQual will have already done
! 				 * this check.
  				 */
  				if (!parsetree->hasSubLinks &&
! 					parsetree->commandType != CMD_UPDATE)
  					parsetree->hasSubLinks = checkExprHasSubLink(wco->qual);
  			}
  		}
--- 2823,2836 ----
  				/*
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
! 				 * already marked, or if the command is an UPDATE to a
! 				 * non-security-barrier view, in which case the same qual will
! 				 * have already been added to the query's WHERE clause, and
! 				 * AddQual will have already done this check.
  				 */
  				if (!parsetree->hasSubLinks &&
! 					(parsetree->commandType != CMD_UPDATE ||
! 					 RelationIsSecurityView(view)))
  					parsetree->hasSubLinks = checkExprHasSubLink(wco->qual);
  			}
  		}
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index e89d930..36c5c98
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblEntry
*** 801,806 ****
--- 801,807 ----
  	Oid			checkAsUser;	/* if valid, check access as this role */
  	Bitmapset  *selectedCols;	/* columns needing SELECT permission */
  	Bitmapset  *modifiedCols;	/* columns needing INSERT/UPDATE permission */
+ 	List	   *securityQuals;	/* any security barrier quals to apply */
  } RangeTblEntry;
  
  /*
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
new file mode 100644
index 0934e63..e9d865d
*** a/src/include/optimizer/prep.h
--- b/src/include/optimizer/prep.h
*************** extern Node *negate_clause(Node *node);
*** 36,41 ****
--- 36,46 ----
  extern Expr *canonicalize_qual(Expr *qual);
  
  /*
+  * prototypes for prepsecurity.c
+  */
+ extern void expand_security_quals(PlannerInfo *root, List *tlist);
+ 
+ /*
   * prototypes for preptlist.c
   */
  extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
new file mode 100644
index c959590..ce56d7b
*** a/src/include/rewrite/rewriteHandler.h
--- b/src/include/rewrite/rewriteHandler.h
*************** extern void AcquireRewriteLocks(Query *p
*** 23,29 ****
  extern Node *build_column_default(Relation rel, int attrno);
  extern Query *get_view_query(Relation view);
  extern const char *view_query_is_auto_updatable(Query *viewquery,
- 										 bool security_barrier,
  										 bool check_cols);
  extern int	relation_is_updatable(Oid reloid,
  						  bool include_triggers,
--- 23,28 ----
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
new file mode 100644
index 91d1639..f6db582
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
*************** CREATE VIEW mysecview4 WITH (security_ba
*** 252,258 ****
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  security_barrier requires a Boolean value
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
--- 252,258 ----
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  invalid value for boolean option "security_barrier": 100
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 99c9165..d12d384
*** a/src/test/regress/expected/updatable_views.out
--- b/src/test/regress/expected/updatable_views.out
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 22,33 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
--- 22,31 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
*************** SELECT table_name, is_insertable_into
*** 44,50 ****
   ro_view19  | NO
   ro_view2   | NO
   ro_view20  | NO
-  ro_view21  | NO
   ro_view3   | NO
   ro_view4   | NO
   ro_view5   | NO
--- 42,47 ----
*************** SELECT table_name, is_insertable_into
*** 55,61 ****
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (21 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
--- 52,58 ----
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (20 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
*************** SELECT table_name, is_updatable, is_inse
*** 73,79 ****
   ro_view19  | NO           | NO
   ro_view2   | NO           | NO
   ro_view20  | NO           | NO
-  ro_view21  | NO           | NO
   ro_view3   | NO           | NO
   ro_view4   | NO           | NO
   ro_view5   | NO           | NO
--- 70,75 ----
*************** SELECT table_name, is_updatable, is_inse
*** 84,90 ****
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (21 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
--- 80,86 ----
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (20 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
*************** SELECT table_name, column_name, is_updat
*** 103,125 ****
   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_view21  | a             | NO
!  ro_view21  | b             | NO
!  ro_view21  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
--- 99,119 ----
   ro_view17  | a             | NO
   ro_view17  | b             | NO
   ro_view18  | a             | NO
!  ro_view19  | sequence_name | NO
!  ro_view19  | last_value    | NO
!  ro_view19  | start_value   | NO
!  ro_view19  | increment_by  | NO
!  ro_view19  | max_value     | NO
!  ro_view19  | min_value     | NO
!  ro_view19  | cache_value   | NO
!  ro_view19  | log_cnt       | NO
!  ro_view19  | is_cycled     | NO
!  ro_view19  | is_called     | NO
   ro_view2   | a             | NO
   ro_view2   | b             | NO
!  ro_view20  | a             | NO
!  ro_view20  | b             | NO
!  ro_view20  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
*************** SELECT table_name, column_name, is_updat
*** 140,146 ****
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (48 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
--- 134,140 ----
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (46 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
*************** INSERT INTO ro_view17 VALUES (3, 'ROW 3'
*** 268,291 ****
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! 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 enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view21 SET b=upper(b);
! ERROR:  cannot update view "ro_view21"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  DROP TABLE base_tbl CASCADE;
! NOTICE:  drop cascades to 17 other objects
  DETAIL:  drop cascades to view ro_view1
  drop cascades to view ro_view17
  drop cascades to view ro_view2
--- 262,281 ----
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! DELETE FROM ro_view18;
! ERROR:  cannot delete from view "ro_view18"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! UPDATE ro_view19 SET max_value=1000;
! ERROR:  cannot update view "ro_view19"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view20 SET b=upper(b);
! ERROR:  cannot update view "ro_view20"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  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_view11
*** 299,311 ****
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view18
! drop cascades to view ro_view21
  drop cascades to view ro_view4
  drop cascades to view rw_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);
--- 289,300 ----
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view20
  drop cascades to view ro_view4
  drop cascades to view rw_view14
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
! NOTICE:  drop cascades to view ro_view19
  -- 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);
*************** INSERT INTO rw_view2 VALUES (2,3); -- ok
*** 1739,1742 ****
--- 1728,1923 ----
  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
+ -- security barrier view
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+                   QUERY PLAN                   
+ -----------------------------------------------
+  Subquery Scan on rw_view1
+    Filter: snoop(rw_view1.person)
+    ->  Seq Scan on base_tbl
+          Filter: (visibility = 'public'::text)
+ (4 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ -- security barrier view on top of security barrier view
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Subquery Scan on rw_view2
+    Filter: snoop(rw_view2.person)
+    ->  Subquery Scan on rw_view1
+          Filter: snoop(rw_view1.person)
+          ->  Seq Scan on base_tbl
+                Filter: (visibility = 'public'::text)
+ (6 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 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
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index a77cf19..6b352c1
*** a/src/test/regress/sql/updatable_views.sql
--- b/src/test/regress/sql/updatable_views.sql
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 25,36 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
--- 25,34 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
*************** SELECT * FROM base_tbl;
*** 87,99 ****
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  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;
! UPDATE ro_view21 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view19;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
--- 85,96 ----
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  INSERT INTO ro_view17 VALUES (3, 'ROW 3');
! DELETE FROM ro_view18;
! UPDATE ro_view19 SET max_value=1000;
! UPDATE ro_view20 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
*************** CREATE VIEW rw_view2 AS
*** 828,830 ****
--- 825,902 ----
    SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION;
  INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check)
  DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view
+ 
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ 
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ 
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ -- security barrier view on top of security barrier view
+ 
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ DROP TABLE base_tbl CASCADE;
#13Craig Ringer
craig@2ndquadrant.com
In reply to: Dean Rasheed (#12)
Re: WIP patch (v2) for updatable security barrier views

I've been thinking about this some more and I don't think you can
avoid the need to remap vars and attrs.

I agree. I was hoping to let expand_targetlist / preprocess_targetlist
do the job, but I'm no longer convinced that'll do the job without
mangling them in the process.

AIUI, your modified version of rewriteTargetView() will turn an UPDATE
query with a rangetable of the form

The patch I posted is clearly incorrect in several ways. Unfortunately
I'm still coming up to speed with the rewriter / planner / executor at
the same time as trying to make major modifications to them. This tends
to produce some false starts.

(It no longer attempts to use expand_targetlist in the rewriter, btw,
that's long gone).

Since the posted patch I've sorted out RETURNING list rewriting and a
few other issues. There are some major issues remaining with the
approach though:

- If we have nested views, we need to pass the tuple ctid up the chain
of expanded view subqueries so ExecModifyTable can consume it. This
is proving to be a lot harder than I'd hoped.

- expand_targetlist / preprocess_targetlist operate on the
resultRelation. With updatable s.b. views, the resultRelation is no
longer the relation from which tuples are being read for input into
ExecModifyTable.

- In subqueries, resultRelation is only set for the outermost layer.
So preprocess_targetlist doesn't usefully add tlist entries for the
inner subquery layers at all.

- It is necessary to expand DEFAULTs into expression tlist entries in
the innermost query layer so that Vars added further up in the
subquery chain can refer to them. In the current experimental patch
DEFAULTs aren't populated correctly.

So we have the following needs:

- passing ctids up through layers of subqueries

- target-list expansion through layers of subqueries

- Differentiating between resultRelation to heapmodifytuple and the
source of the tuples to feed into heapmodifytuple now that these are no
longer the same thing.

I was originally hoping all this could be dealt with in the rewriter, by
changing resultRelation to point to the next-inner-most RTE whenever a
target view is expanded. This turns out to be too simplistic:

- There is no ctid col on a view, so if we have v2 over v1 over table t,
when we expand v2 there's no way to create a tlist entry to point to
v1's ctid. It won't have one until we've expanded v1 into t in the next
pass. The same issue applies to expanding the tlist to carry cols of "t"
up the subquery chain in the rewrite phase.

- Rowmarks are added to point to resultrelation after rewrite, and these
then add tlist entries in preprocess_targetlist. These TLEs will point
to the base resultRelation, which isn't correct when we're updating
nested subqueries.

So we just can't do this during recursive rewrite, because the required
information is only available once the target view is fully expanded
into nested subqueries.

It seems that tlist fixup and injection of the ctid up the subquery
chain must be done after all recursive rewriting.

To do these fixups later, we need to keep track of which nested series
of subqueries is the "target", i.e. produces tuples including resjunk
ctid cols to feed into ExecModifyTuple. Currently this information is
"resultRelation"

The more I think about this, the more I think that the approach of my
original patch was neater. The idea was to have 2 new pieces of code:

1). In rewriteTargetView() decorate the target RTE with any security
barrier quals (in the new rte->securityQuals field), instead of
merging them with the main query's quals. So the output of this
stage of rewriting would be something like

rtable:
1: relation RTE (base table) <- resultRelation
- securityQuals = [view quals]
fromList: [1]

So you're proposing to still flatten views during rewrite, as the
current code does, but just keep track of s.b. quals separately?

If so, what about multiple S.B. views may be nested and their quals must
be isolated from each other, not just from the outer query?

That's why I see subqueries as such a useful model.

2). After all rewriting is complete, scan the query and turn all
relation RTEs with non-empty securityQuals into subquery RTEs
(making a copy of the original RTE in the case of the result
relation).

I'm not sure I understand how this would work in the face of multiple
levels of nesting s.b. views. Are you thinking of doing recursive expansion?

Another ugly
feature of my earlier patch was the changes it made to
expand_target_list() and the need to track the query's sourceRelation.

I've been fighting the need to add the concept of a "sourceRelation" for
this purpose too.

Both of those things can be avoided simply by moving the subquery
expansion code (2) to after expand_target_list(), and hence also after
expand_inherited_tables().

That's certainly interesting. I'm reading over the patch now.

There is still a lot more testing to be done with my patch, so there
may well be unforeseen problems, but it feels like a cleaner, more
straightforward approach.

Thoughts?

I'm reading it now, but in general, how do you see it solving the issue
of getting the ctid (and any table attrs not present in the views) up
two or more layers of nested view? And default expansion?

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#14Craig Ringer
craig@2ndquadrant.com
In reply to: Craig Ringer (#13)
Re: WIP patch (v2) for updatable security barrier views

On 01/08/2014 02:00 PM, Craig Ringer wrote:

I'm not sure I understand how this would work in the face of multiple
levels of nesting s.b. views. Are you thinking of doing recursive expansion?

Never mind, that part is clearly covered in the patch.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#15Craig Ringer
craig@2ndquadrant.com
In reply to: Dean Rasheed (#12)
Re: WIP patch (v2) for updatable security barrier views

Dean,

Short version
-------------

Looks amazing overall. Very clever to zip up the s.b. quals, let the
rest of the rewriter and planer do their work normally, then unpack them
into subqueries inserted in the planner once inheritance appendrels are
expanded, etc.

My main concern is that the securityQuals appear to bypass all later
rewrite stages, inheritance expansion during planning, etc. I suspect
this might be hard to get around (because these are disembodied quals
which may have nonsense varnos), but I'm looking into it now.

There's also an assertion failure whenever a correlated subquery appears
as a security barrier view qual. Again, looking at it.

Ideas on that issue?

Much longer version: My understanding of how it works
-----------------------------------------------------

My understanding from reading the patch is that this:

- Flattens target views in rewriteTargetView, as in current master. If
the target view is a security barrier view, the view quals are appended
to a list of security barrier quals on the new RTE, instead of appended
to the RTE's normal quals like for normal views.

After rewrite the views are fully flattened down to a RTE_RELATION,
which becomes the resultRelation. An unreferenced RTE for each view
that's been rewritten is preserved in the range-table for permissions
checking purposes only (same as current master).

- Inheritance expansion, tlist expansion, etc then occurrs as normal.

- In planning, in inheritance_planner, if any RTE has any stashed
security quals in its RangeTableEntry, expand_security_qual is invoked.
This iteratively wraps the base relation in a subquery with the saved
security barrier quals, creating nested subqueries around the original
RTE. At each pass resultRelation is changed to point to the new
outer-most subquery.

As a result of this approach everything looks normal to
preprocess_targetlist, row-marking, etc, because they're seeing a normal
RTE_RELATION as resultRelation. The security barrier quals are, at this
stage, stashed aside. If there's inheritance involved, RTEs copied
during appendrel expansion get copies of the security quals on in the
parent RTE.

Problem with inheritance, views, etc in s.b. quals
--------------------------------------------------

After inheritance expansion, tlist expansion, etc, the s.b. quals are
unpacked to create subqueries wrapping the original RTEs.

So, with:

CREATE TABLE t1 (x float, b integer, secret1 text, secret2 text);
CREATE TABLE t1child (z integer) INHERITS (t1);

INSERT INTO t1 (x, b, secret1, secret2)
VALUES
(0,0,'secret0', 'supersecret'),
(1,1,'secret1', 'supersecret'),
(2,2,'secret2', 'supersecret'),
(3,3,'secret3', 'supersecret'),
(4,4,'secret4', 'supersecret'),
(5,6,'secret5', 'supersecret');

INSERT INTO t1child (x, b, secret1, secret2, z)
VALUES
(8,8,'secret8', 'ss', 8),
(9,9,'secret8', 'ss', 9),
(10,10,'secret8', 'ss', 10);

CREATE VIEW v1
WITH (security_barrier)
AS
SELECT b AS b1, x AS x1, secret1
FROM t1 WHERE b % 2 = 0;

CREATE VIEW v2
WITH (security_barrier)
AS
SELECT b1 AS b2, x1 AS x2
FROM v1 WHERE b1 % 4 = 0;

then a statement like:

UPDATE v2
SET x2 = x2 + 32;

will be rewritten into something like (imaginary sql)

UPDATE t1 WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0))
SET x = x + 32

inheritance-expanded and tlist-expanded into something like (imaginary SQL)

UPDATE
(t1 WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0)))
UNION ALL
(t1child WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0)))
SET x = x + 32;

after which security qual expansion occurs, giving us something like:

UPDATE
t1, t1child <--- resultRelations
(
SELECT v2.ctid, v2.*
FROM (
SELECT v1.ctid, v1.*
FROM (
SELECT t1.ctid, t1.*
FROM t1
WHERE b % 2 == 0
) v1
WHERE b % 4 == 0
) v2

UNION ALL

SELECT v2.ctid, v2.*
FROM (
SELECT v1.ctid, v1.*
FROM (
SELECT t1child.ctid, t1child.*
FROM t1child
WHERE b % 2 == 0
) v1
WHERE b % 4 == 0
) v2

)
SET x = x + 32;

Giving a plan looking like:

EXPLAIN UPDATE v2 SET x2 = 32

QUERY PLAN
---------------------------------------------------------------------------
Update on t1 t1_2 (cost=0.00..23.35 rows=2 width=76)
-> Subquery Scan on t1 (cost=0.00..2.18 rows=1 width=74)
-> Subquery Scan on t1_3 (cost=0.00..2.17 rows=1 width=74)
Filter: ((t1_3.b % 4) = 0)
-> Seq Scan on t1 t1_4 (cost=0.00..2.16 rows=1 width=74)
Filter: ((b % 2) = 0)
-> Subquery Scan on t1_1 (cost=0.00..21.17 rows=1 width=78)
-> Subquery Scan on t1_5 (cost=0.00..21.16 rows=1 width=78)
Filter: ((t1_5.b % 4) = 0)
-> Seq Scan on t1child (cost=0.00..21.10 rows=4 width=78)
Filter: ((b % 2) = 0)
(11 rows)

So far this looks like a really clever approach. My only real concern is
that the security quals are currently hidden from rewrite and parsing
before during the period they're hidden away in the security quals RTI
field.

This means they aren't processed for things like inheritance expansion. e.g.

CREATE TABLE rowfilter (remainder integer, userid text);
CREATE TABLE rowfilterchild () INHERITS (rowfilter);
INSERT INTO rowfilterchild(remainder, userid) values (0, current_user);

a view with a security qual that refers to an inherited relation won't work:

CREATE VIEW sqv
WITH (security_barrier)
AS
SELECT x, b FROM t1 WHERE (
SELECT b % 4 = remainder
FROM rowfilter
WHERE userid = current_user
OFFSET 0
);

This is a bit contrived to force the optimiser to treat the subquery as
correlated and thus make sure the ref to rowfilter gets into the
securityQuals list.

I expected zero results (a scan of rowfilter, but not rowfilterchild,
resulting in a null subquery return) but land up with an assertion
failure instead. The assertion triggers for any security qual containing
a correlated subquery, so it's crashing out before we can break due to
failure to expand inheritance.

This isn't just about inheritance. In general, we'd need a way to
process those securityQuals through any rewrite phases and through the
parts of planning before they get merged back in, so they're subject to
things like inheritance appendrel expansion.

Same if the security qual contains a view ref:

CREATE VIEW dumbview(zero)
AS SELECT 0;

CREATE VIEW sqv2
WITH (security_barrier)
AS
SELECT x, b FROM t1
WHERE (SELECT b % 2 = zero FROM dumbview OFFSET 0);

--
Craig Ringer
(Phew!)

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

#16Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#15)
1 attachment(s)
Re: WIP patch (v2) for updatable security barrier views

On 8 January 2014 09:02, Craig Ringer <craig@2ndquadrant.com> wrote:

Dean,

Short version
-------------

Looks amazing overall. Very clever to zip up the s.b. quals, let the
rest of the rewriter and planer do their work normally, then unpack them
into subqueries inserted in the planner once inheritance appendrels are
expanded, etc.

My main concern is that the securityQuals appear to bypass all later
rewrite stages, inheritance expansion during planning, etc. I suspect
this might be hard to get around (because these are disembodied quals
which may have nonsense varnos), but I'm looking into it now.

There's also an assertion failure whenever a correlated subquery appears
as a security barrier view qual. Again, looking at it.

Ideas on that issue?

Much longer version: My understanding of how it works
-----------------------------------------------------

My understanding from reading the patch is that this:

- Flattens target views in rewriteTargetView, as in current master. If
the target view is a security barrier view, the view quals are appended
to a list of security barrier quals on the new RTE, instead of appended
to the RTE's normal quals like for normal views.

After rewrite the views are fully flattened down to a RTE_RELATION,
which becomes the resultRelation. An unreferenced RTE for each view
that's been rewritten is preserved in the range-table for permissions
checking purposes only (same as current master).

- Inheritance expansion, tlist expansion, etc then occurrs as normal.

- In planning, in inheritance_planner, if any RTE has any stashed
security quals in its RangeTableEntry, expand_security_qual is invoked.
This iteratively wraps the base relation in a subquery with the saved
security barrier quals, creating nested subqueries around the original
RTE. At each pass resultRelation is changed to point to the new
outer-most subquery.

As a result of this approach everything looks normal to
preprocess_targetlist, row-marking, etc, because they're seeing a normal
RTE_RELATION as resultRelation. The security barrier quals are, at this
stage, stashed aside. If there's inheritance involved, RTEs copied
during appendrel expansion get copies of the security quals on in the
parent RTE.

Problem with inheritance, views, etc in s.b. quals
--------------------------------------------------

After inheritance expansion, tlist expansion, etc, the s.b. quals are
unpacked to create subqueries wrapping the original RTEs.

So, with:

CREATE TABLE t1 (x float, b integer, secret1 text, secret2 text);
CREATE TABLE t1child (z integer) INHERITS (t1);

INSERT INTO t1 (x, b, secret1, secret2)
VALUES
(0,0,'secret0', 'supersecret'),
(1,1,'secret1', 'supersecret'),
(2,2,'secret2', 'supersecret'),
(3,3,'secret3', 'supersecret'),
(4,4,'secret4', 'supersecret'),
(5,6,'secret5', 'supersecret');

INSERT INTO t1child (x, b, secret1, secret2, z)
VALUES
(8,8,'secret8', 'ss', 8),
(9,9,'secret8', 'ss', 9),
(10,10,'secret8', 'ss', 10);

CREATE VIEW v1
WITH (security_barrier)
AS
SELECT b AS b1, x AS x1, secret1
FROM t1 WHERE b % 2 = 0;

CREATE VIEW v2
WITH (security_barrier)
AS
SELECT b1 AS b2, x1 AS x2
FROM v1 WHERE b1 % 4 = 0;

then a statement like:

UPDATE v2
SET x2 = x2 + 32;

will be rewritten into something like (imaginary sql)

UPDATE t1 WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0))
SET x = x + 32

inheritance-expanded and tlist-expanded into something like (imaginary SQL)

UPDATE
(t1 WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0)))
UNION ALL
(t1child WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0)))
SET x = x + 32;

after which security qual expansion occurs, giving us something like:

UPDATE
t1, t1child <--- resultRelations
(
SELECT v2.ctid, v2.*
FROM (
SELECT v1.ctid, v1.*
FROM (
SELECT t1.ctid, t1.*
FROM t1
WHERE b % 2 == 0
) v1
WHERE b % 4 == 0
) v2

UNION ALL

SELECT v2.ctid, v2.*
FROM (
SELECT v1.ctid, v1.*
FROM (
SELECT t1child.ctid, t1child.*
FROM t1child
WHERE b % 2 == 0
) v1
WHERE b % 4 == 0
) v2

)
SET x = x + 32;

Giving a plan looking like:

EXPLAIN UPDATE v2 SET x2 = 32

QUERY PLAN
---------------------------------------------------------------------------
Update on t1 t1_2 (cost=0.00..23.35 rows=2 width=76)
-> Subquery Scan on t1 (cost=0.00..2.18 rows=1 width=74)
-> Subquery Scan on t1_3 (cost=0.00..2.17 rows=1 width=74)
Filter: ((t1_3.b % 4) = 0)
-> Seq Scan on t1 t1_4 (cost=0.00..2.16 rows=1 width=74)
Filter: ((b % 2) = 0)
-> Subquery Scan on t1_1 (cost=0.00..21.17 rows=1 width=78)
-> Subquery Scan on t1_5 (cost=0.00..21.16 rows=1 width=78)
Filter: ((t1_5.b % 4) = 0)
-> Seq Scan on t1child (cost=0.00..21.10 rows=4 width=78)
Filter: ((b % 2) = 0)
(11 rows)

So far this looks like a really clever approach. My only real concern is
that the security quals are currently hidden from rewrite and parsing
before during the period they're hidden away in the security quals RTI
field.

This means they aren't processed for things like inheritance expansion. e.g.

CREATE TABLE rowfilter (remainder integer, userid text);
CREATE TABLE rowfilterchild () INHERITS (rowfilter);
INSERT INTO rowfilterchild(remainder, userid) values (0, current_user);

a view with a security qual that refers to an inherited relation won't work:

CREATE VIEW sqv
WITH (security_barrier)
AS
SELECT x, b FROM t1 WHERE (
SELECT b % 4 = remainder
FROM rowfilter
WHERE userid = current_user
OFFSET 0
);

This is a bit contrived to force the optimiser to treat the subquery as
correlated and thus make sure the ref to rowfilter gets into the
securityQuals list.

I expected zero results (a scan of rowfilter, but not rowfilterchild,
resulting in a null subquery return) but land up with an assertion
failure instead. The assertion triggers for any security qual containing
a correlated subquery, so it's crashing out before we can break due to
failure to expand inheritance.

This isn't just about inheritance. In general, we'd need a way to
process those securityQuals through any rewrite phases and through the
parts of planning before they get merged back in, so they're subject to
things like inheritance appendrel expansion.

Same if the security qual contains a view ref:

CREATE VIEW dumbview(zero)
AS SELECT 0;

CREATE VIEW sqv2
WITH (security_barrier)
AS
SELECT x, b FROM t1
WHERE (SELECT b % 2 = zero FROM dumbview OFFSET 0);

Thanks for testing, and good catch on the sublinks. There was a
trivial bug in my new code in rewriteTargetView() --- it needs to
check the added security barrier qual for sublinks and mark the parent
query accordingly. After that the rewriter will descend into the
sublinks on the security barrier quals expanding any views they
contain, so the fix for that part is trivial (see the attached
update).

The assertion failure with inheritance and sublinks is a separate
issue --- adjust_appendrel_attrs() is not expecting to find any
unplanned sublinks in the query tree when it is invoked, since they
would normally have all been planned by that point. However, the
addition of the new security barrier subqueries after inheritance
expansion can now insert new sublinks which need to be planned. I'll
look into how best to make that happen.

Regards,
Dean

Attachments:

updatable-sb-views.patchtext/x-diff; charset=US-ASCII; name=updatable-sb-views.patchDownload
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
new file mode 100644
index e0fbe1e..888410f
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
*************** CREATE VIEW vista AS SELECT text 'Hello
*** 323,334 ****
         or set-returning functions.
        </para>
       </listitem>
- 
-      <listitem>
-       <para>
-        The view must not have the <literal>security_barrier</> property.
-       </para>
-      </listitem>
      </itemizedlist>
     </para>
  
--- 323,328 ----
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index b9cd88d..a984c03
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** ATExecSetRelOptions(Relation rel, List *
*** 8806,8812 ****
  		List	   *view_options = untransformRelOptions(newOptions);
  		ListCell   *cell;
  		bool		check_option = false;
- 		bool		security_barrier = false;
  
  		foreach(cell, view_options)
  		{
--- 8806,8811 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8814,8821 ****
  
  			if (pg_strcasecmp(defel->defname, "check_option") == 0)
  				check_option = true;
- 			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 				security_barrier = defGetBoolean(defel);
  		}
  
  		/*
--- 8813,8818 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8825,8832 ****
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query,
! 											 security_barrier, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
--- 8822,8828 ----
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
new file mode 100644
index 0703c05..8a43aa4
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
*************** DefineView(ViewStmt *stmt, const char *q
*** 396,402 ****
  	RangeVar   *view;
  	ListCell   *cell;
  	bool		check_option;
- 	bool		security_barrier;
  
  	/*
  	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
--- 396,401 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 451,457 ****
  	 * specified.
  	 */
  	check_option = false;
- 	security_barrier = false;
  
  	foreach(cell, stmt->options)
  	{
--- 450,455 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 459,466 ****
  
  		if (pg_strcasecmp(defel->defname, "check_option") == 0)
  			check_option = true;
- 		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 			security_barrier = defGetBoolean(defel);
  	}
  
  	/*
--- 457,462 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 470,476 ****
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, security_barrier, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
--- 466,472 ----
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index e4184c5..255ade9
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 1998,2003 ****
--- 1998,2004 ----
  	COPY_SCALAR_FIELD(checkAsUser);
  	COPY_BITMAPSET_FIELD(selectedCols);
  	COPY_BITMAPSET_FIELD(modifiedCols);
+ 	COPY_NODE_FIELD(securityQuals);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 0cdb947..cab71ac
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2278,2283 ****
--- 2278,2284 ----
  	COMPARE_SCALAR_FIELD(checkAsUser);
  	COMPARE_BITMAPSET_FIELD(selectedCols);
  	COMPARE_BITMAPSET_FIELD(modifiedCols);
+ 	COMPARE_NODE_FIELD(securityQuals);
  
  	return true;
  }
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 17626f9..9bb62ba
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** range_table_walker(List *rtable,
*** 2020,2025 ****
--- 2020,2028 ----
  					return true;
  				break;
  		}
+ 
+ 		if (walker(rte->securityQuals, context))
+ 			return true;
  	}
  	return false;
  }
*************** range_table_mutator(List *rtable,
*** 2755,2760 ****
--- 2758,2764 ----
  				MUTATE(newrte->values_lists, rte->values_lists, List *);
  				break;
  		}
+ 		MUTATE(newrte->securityQuals, rte->securityQuals, List *);
  		newrt = lappend(newrt, newrte);
  	}
  	return newrt;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 4f63906..43502b0
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outRangeTblEntry(StringInfo str, const
*** 2409,2414 ****
--- 2409,2415 ----
  	WRITE_OID_FIELD(checkAsUser);
  	WRITE_BITMAPSET_FIELD(selectedCols);
  	WRITE_BITMAPSET_FIELD(modifiedCols);
+ 	WRITE_NODE_FIELD(securityQuals);
  }
  
  static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index aba6d4e..941c038
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readRangeTblEntry(void)
*** 1250,1255 ****
--- 1250,1256 ----
  	READ_OID_FIELD(checkAsUser);
  	READ_BITMAPSET_FIELD(selectedCols);
  	READ_BITMAPSET_FIELD(modifiedCols);
+ 	READ_NODE_FIELD(securityQuals);
  
  	READ_DONE();
  }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 1da4b2f..93097b0
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 916,921 ****
--- 916,927 ----
  		subplan = grouping_planner(&subroot, 0.0 /* retrieve all tuples */ );
  
  		/*
+ 		 * Planning may have modified the query result relation (if there
+ 		 * were security barrier quals on the result RTE).
+ 		 */
+ 		appinfo->child_relid = subroot.parse->resultRelation;
+ 
+ 		/*
  		 * If this child rel was excluded by constraint exclusion, exclude it
  		 * from the result plan.
  		 */
*************** inheritance_planner(PlannerInfo *root)
*** 932,940 ****
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 			final_rtable = list_concat(final_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
--- 938,969 ----
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 		{
! 			List	   *tmp_rtable = NIL;
! 			ListCell   *cell1, *cell2;
! 
! 			/*
! 			 * Planning this new child may have turned some of the original
! 			 * RTEs into subqueries (if they had security barrier quals). If
! 			 * so, we want to use these in the final rtable.
! 			 */
! 			forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
! 			{
! 				RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
! 				RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
! 
! 				if (rte1->rtekind == RTE_RELATION &&
! 					rte1->securityQuals != NIL &&
! 					rte2->rtekind == RTE_SUBQUERY)
! 					tmp_rtable = lappend(tmp_rtable, rte2);
! 				else
! 					tmp_rtable = lappend(tmp_rtable, rte1);
! 			}
! 
! 			final_rtable = list_concat(tmp_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
+ 		}
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
*************** grouping_planner(PlannerInfo *root, doub
*** 1163,1168 ****
--- 1192,1203 ----
  		tlist = preprocess_targetlist(root, tlist);
  
  		/*
+ 		 * Expand any rangetable entries that have security barrier quals.
+ 		 * This may add new security barrier subquery RTEs to the rangetable.
+ 		 */
+ 		expand_security_quals(root, tlist);
+ 
+ 		/*
  		 * Locate any window functions in the tlist.  (We don't need to look
  		 * anywhere else, since expressions used in ORDER BY will be in there
  		 * too.)  Note that they could all have been eliminated by constant
diff --git a/src/backend/optimizer/prep/Makefile b/src/backend/optimizer/prep/Makefile
new file mode 100644
index 86301bf..5195d9b
*** a/src/backend/optimizer/prep/Makefile
--- b/src/backend/optimizer/prep/Makefile
*************** subdir = src/backend/optimizer/prep
*** 12,17 ****
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
--- 12,17 ----
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o prepsecurity.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
new file mode 100644
index ...e7a3f56
*** a/src/backend/optimizer/prep/prepsecurity.c
--- b/src/backend/optimizer/prep/prepsecurity.c
***************
*** 0 ****
--- 1,423 ----
+ /*-------------------------------------------------------------------------
+  *
+  * prepsecurity.c
+  *	  Routines for preprocessing security barrier quals.
+  *
+  * Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
+  * Portions Copyright (c) 1994, Regents of the University of California
+  *
+  *
+  * IDENTIFICATION
+  *	  src/backend/optimizer/prep/prepsecurity.c
+  *
+  *-------------------------------------------------------------------------
+  */
+ #include "postgres.h"
+ 
+ #include "access/heapam.h"
+ #include "access/sysattr.h"
+ #include "catalog/heap.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/prep.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteManip.h"
+ #include "utils/rel.h"
+ 
+ 
+ typedef struct
+ {
+ 	int			rt_index;		/* Index of security barrier RTE */
+ 	int			sublevels_up;	/* Current nesting depth */
+ 	Relation	rel;			/* RTE relation at rt_index */
+ 	List	   *targetlist;		/* Targetlist for new subquery RTE */
+ 	List	   *colnames;		/* Column names in subquery RTE */
+ 	List	   *vars_processed;	/* List of Vars already processed */
+ } security_barrier_replace_vars_context;
+ 
+ static void expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual);
+ 
+ static void security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context);
+ 
+ static bool security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context);
+ 
+ 
+ /*
+  * expand_security_quals -
+  *	  expands any security barrier quals on RTEs in the query rtable, turning
+  *	  them into security barrier subqueries.
+  *
+  * Any given RTE may have multiple security barrier quals in a list, from which
+  * we create a set of nested subqueries to isolate each security barrier from
+  * the others, providing protection against malicious user-defined security
+  * barriers.  The first security barrier qual in the list will be used in the
+  * innermost subquery.
+  */
+ void
+ expand_security_quals(PlannerInfo *root, List *tlist)
+ {
+ 	Query	   *parse = root->parse;
+ 	int			rt_index;
+ 
+ 	/*
+ 	 * Process each RTE in the rtable list.
+ 	 *
+ 	 * Note that this is deliberately not a foreach loop, since the rtable may
+ 	 * be modified each time through the loop.
+ 	 */
+ 	rt_index = 0;
+ 	while (rt_index < list_length(parse->rtable))
+ 	{
+ 		RangeTblEntry *rte;
+ 
+ 		rt_index++;
+ 		rte = rt_fetch(rt_index, parse->rtable);
+ 
+ 		if (rte->securityQuals == NIL)
+ 			continue;
+ 
+ 		/*
+ 		 * Ignore any RTEs that aren't used in the query (such RTEs may be
+ 		 * present for permissions checks).
+ 		 */
+ 		if (rt_index != parse->resultRelation &&
+ 			!rangeTableEntry_used((Node *) parse, rt_index, 0))
+ 			continue;
+ 
+ 		/*
+ 		 * If this RTE is the target then we need to make a copy of it before
+ 		 * expanding it.  The unexpanded copy will become the new target, and
+ 		 * the original RTE will be expanded to become the source of rows to
+ 		 * update/delete.
+ 		 */
+ 		if (rt_index == parse->resultRelation)
+ 		{
+ 			RangeTblEntry *newrte = copyObject(rte);
+ 			parse->rtable = lappend(parse->rtable, newrte);
+ 			parse->resultRelation = list_length(parse->rtable);
+ 
+ 			/*
+ 			 * Wipe out any copied security barrier quals on the new target to
+ 			 * prevent infinite recursion.
+ 			 */
+ 			newrte->securityQuals = NIL;
+ 
+ 			/*
+ 			 * There's no need to do permissions checks twice, so wipe out the
+ 			 * permissions info for the original RTE (we prefer to keep the
+ 			 * bits set on the result RTE).
+ 			 */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * For the most part, Vars referencing the original relation should
+ 			 * remain as they are, meaning that they pull OLD values from the
+ 			 * expanded RTE.  But in the RETURNING list and in any WITH CHECK
+ 			 * OPTION quals, we want such Vars to represent NEW values, so
+ 			 * change them to reference the new RTE.
+ 			 */
+ 			ChangeVarNodes((Node *) parse->returningList, rt_index,
+ 						   parse->resultRelation, 0);
+ 
+ 			ChangeVarNodes((Node *) parse->withCheckOptions, rt_index,
+ 						   parse->resultRelation, 0);
+ 		}
+ 
+ 		/*
+ 		 * Process each security barrier qual in turn, starting with the
+ 		 * innermost one (the first in the list) and working outwards.
+ 		 *
+ 		 * We remove each qual from the list before processing it, so that its
+ 		 * variables aren't modified by expand_security_qual.  Also we don't
+ 		 * necessarily want the attributes referred to by the qual to be
+ 		 * exposed by the newly built subquery.
+ 		 */
+ 		while (rte->securityQuals != NIL)
+ 		{
+ 			Node   *qual = (Node *) linitial(rte->securityQuals);
+ 			rte->securityQuals = list_delete_first(rte->securityQuals);
+ 
+ 			ChangeVarNodes(qual, rt_index, 1, 0);
+ 			expand_security_qual(parse, tlist, rt_index, qual);
+ 		}
+ 	}
+ }
+ 
+ 
+ /*
+  * expand_security_qual -
+  *	  expand the specified security barrier qual on a query RTE, turning the
+  *	  RTE into a security barrier subquery.
+  */
+ static void
+ expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual)
+ {
+ 	RangeTblEntry  *rte;
+ 	Oid				relid;
+ 	Query		   *subquery;
+ 	RangeTblEntry  *subrte;
+ 	RangeTblRef	   *subrtr;
+ 	security_barrier_replace_vars_context context;
+ 	ListCell	   *cell;
+ 
+ 	rte = rt_fetch(rt_index, parse->rtable);
+ 	relid = rte->relid;
+ 
+ 	/*
+ 	 * There should only be 2 possible cases:
+ 	 *
+ 	 * 1. A relation RTE, which we turn into a subquery RTE containing all
+ 	 * referenced columns.
+ 	 *
+ 	 * 2. A subquery RTE (either from a prior call to this function or from an
+ 	 * expanded view).  In this case we build a new subquery on top of it to
+ 	 * isolate this security barrier qual from any other quals.
+ 	 */
+ 	switch (rte->rtekind)
+ 	{
+ 		case RTE_RELATION:
+ 			/*
+ 			 * Turn the relation RTE into a security barrier subquery RTE,
+ 			 * moving all permissions checks down into the subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 
+ 			subrte = copyObject(rte);
+ 			subrte->inFromCl = true;
+ 			subrte->securityQuals = NIL;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->rtekind = RTE_SUBQUERY;
+ 			rte->relid = InvalidOid;
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 			rte->inh = false;			/* must not be set for a subquery */
+ 
+ 			/* the permissions checks have now been moved down */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * Replace any variables in the outer query that refer to the
+ 			 * original relation RTE with references to columns that we will
+ 			 * expose in the new subquery, building the subquery's targetlist
+ 			 * as we go.
+ 			 */
+ 			context.rt_index = rt_index;
+ 			context.sublevels_up = 0;
+ 			context.rel = heap_open(relid, NoLock);
+ 			context.targetlist = NIL;
+ 			context.colnames = NIL;
+ 			context.vars_processed = NIL;
+ 
+ 			security_barrier_replace_vars((Node *) parse, &context);
+ 			security_barrier_replace_vars((Node *) tlist, &context);
+ 
+ 			heap_close(context.rel, NoLock);
+ 
+ 			/* Now we know what columns the subquery needs to expose */
+ 			rte->subquery->targetList = context.targetlist;
+ 			rte->eref = makeAlias(rte->eref->aliasname, context.colnames);
+ 
+ 			break;
+ 
+ 		case RTE_SUBQUERY:
+ 			/*
+ 			 * Build a new subquery that includes all the same columns as the
+ 			 * original subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 			subquery->targetList = NIL;
+ 
+ 			foreach(cell, rte->subquery->targetList)
+ 			{
+ 				TargetEntry	   *tle;
+ 				Var			   *var;
+ 
+ 				tle = (TargetEntry *) lfirst(cell);
+ 				var = makeVarFromTargetEntry(1, tle);
+ 
+ 				tle = makeTargetEntry((Expr *) var,
+ 									  list_length(subquery->targetList) + 1,
+ 									  pstrdup(tle->resname),
+ 									  tle->resjunk);
+ 				subquery->targetList = lappend(subquery->targetList, tle);
+ 			}
+ 
+ 			subrte = makeNode(RangeTblEntry);
+ 			subrte->rtekind = RTE_SUBQUERY;
+ 			subrte->subquery = rte->subquery;
+ 			subrte->security_barrier = rte->security_barrier;
+ 			subrte->eref = copyObject(rte->eref);
+ 			subrte->inFromCl = true;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 
+ 			break;
+ 
+ 		default:
+ 			elog(ERROR, "invalid range table entry for security barrier qual");
+ 	}
+ }
+ 
+ 
+ /*
+  * security_barrier_replace_vars -
+  *	  Apply security barrier variable replacement to an expression tree.
+  *
+  * This also builds/updates a targetlist with entries for each replacement
+  * variable that needs to be exposed by the security barrier subquery RTE.
+  *
+  * NOTE: although this has the form of a walker, we cheat and modify the
+  * nodes in-place.	The given expression tree should have been copied
+  * earlier to ensure that no unwanted side-effects occur!
+  */
+ static void
+ security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context)
+ {
+ 	/*
+ 	 * Must be prepared to start with a Query or a bare expression tree; if
+ 	 * it's a Query, go straight to query_tree_walker to make sure that
+ 	 * sublevels_up doesn't get incremented prematurely.
+ 	 */
+ 	if (node && IsA(node, Query))
+ 		query_tree_walker((Query *) node,
+ 						  security_barrier_replace_vars_walker,
+ 						  (void *) context, 0);
+ 	else
+ 		security_barrier_replace_vars_walker(node, context);
+ }
+ 
+ static bool
+ security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context)
+ {
+ 	if (node == NULL)
+ 		return false;
+ 	if (IsA(node, Var))
+ 	{
+ 		Var		   *var = (Var *) node;
+ 
+ 		/*
+ 		 * Note that the same Var may be present in different lists, so we
+ 		 * need to take care not to process it multiple times.
+ 		 */
+ 		if (var->varno == context->rt_index &&
+ 			var->varlevelsup == context->sublevels_up &&
+ 			!list_member_ptr(context->vars_processed, var))
+ 		{
+ 			/*
+ 			 * Found a matching variable. Make sure that it is in the subquery
+ 			 * targetlist and map its attno accordingly.
+ 			 */
+ 			AttrNumber	attno;
+ 			ListCell   *l;
+ 			TargetEntry *tle;
+ 			char	   *attname;
+ 			Var		   *newvar;
+ 
+ 			/* Search for the base attribute in the subquery targetlist */
+ 			attno = InvalidAttrNumber;
+ 			foreach(l, context->targetlist)
+ 			{
+ 				tle = (TargetEntry *) lfirst(l);
+ 				attno++;
+ 
+ 				Assert(IsA(tle->expr, Var));
+ 				if (((Var *) tle->expr)->varattno == var->varattno &&
+ 					((Var *) tle->expr)->varcollid == var->varcollid)
+ 				{
+ 					/* Map the variable onto this subquery targetlist entry */
+ 					var->varattno = attno;
+ 					return false;
+ 				}
+ 			}
+ 
+ 			/* Not in the subquery targetlist, so add it. Get its name. */
+ 			if (var->varattno < 0)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = SystemAttributeDefinition(var->varattno,
+ 													context->rel->rd_rel->relhasoids);
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else if (var->varattno == InvalidAttrNumber)
+ 			{
+ 				attname = "wholerow";
+ 			}
+ 			else if (var->varattno <= context->rel->rd_att->natts)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = context->rel->rd_att->attrs[var->varattno - 1];
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else
+ 			{
+ 				elog(ERROR, "invalid attribute number %d in security_barrier_replace_vars", var->varattno);
+ 			}
+ 
+ 			/* New variable for subquery targetlist */
+ 			newvar = copyObject(var);
+ 			newvar->varno = 1;
+ 
+ 			attno = list_length(context->targetlist) + 1;
+ 			tle = makeTargetEntry((Expr *) newvar,
+ 								  attno,
+ 								  pstrdup(attname),
+ 								  false);
+ 
+ 			context->targetlist = lappend(context->targetlist, tle);
+ 
+ 			context->colnames = lappend(context->colnames,
+ 										makeString(pstrdup(attname)));
+ 
+ 			/* Update the outer query's variable */
+ 			var->varattno = attno;
+ 
+ 			/* Remember this Var so that we don't process it again */
+ 			context->vars_processed = lappend(context->vars_processed, var);
+ 		}
+ 		return false;
+ 	}
+ 
+ 	if (IsA(node, Query))
+ 	{
+ 		/* Recurse into subselects */
+ 		bool		result;
+ 
+ 		context->sublevels_up++;
+ 		result = query_tree_walker((Query *) node,
+ 								   security_barrier_replace_vars_walker,
+ 								   (void *) context, 0);
+ 		context->sublevels_up--;
+ 		return result;
+ 	}
+ 	return expression_tree_walker(node, security_barrier_replace_vars_walker,
+ 								  (void *) context);
+ }
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 50cb753..c1004f1
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** view_col_is_auto_updatable(RangeTblRef *
*** 1973,1980 ****
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
! 							 bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
--- 1973,1979 ----
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
*************** view_query_is_auto_updatable(Query *view
*** 2048,2061 ****
  		return gettext_noop("Views that return set-returning functions 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 the future.
- 	 */
- 	if (security_barrier)
- 		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.
  	 */
--- 2047,2052 ----
*************** relation_is_updatable(Oid reloid,
*** 2303,2311 ****
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery,
! 										 RelationIsSecurityView(rel),
! 										 false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
--- 2294,2300 ----
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery, false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
*************** rewriteTargetView(Query *parsetree, Rela
*** 2460,2466 ****
  
  	auto_update_detail =
  		view_query_is_auto_updatable(viewquery,
- 									 RelationIsSecurityView(view),
  									 parsetree->commandType != CMD_DELETE);
  
  	if (auto_update_detail)
--- 2449,2454 ----
*************** rewriteTargetView(Query *parsetree, Rela
*** 2664,2669 ****
--- 2652,2665 ----
  												   view_targetlist);
  
  	/*
+ 	 * Move any security barrier quals from the view RTE onto the new target
+ 	 * RTE.  Any such quals should now apply to the new target RTE and will not
+ 	 * reference the original view RTE in the rewritten query.
+ 	 */
+ 	new_rte->securityQuals = view_rte->securityQuals;
+ 	view_rte->securityQuals = NIL;
+ 
+ 	/*
  	 * 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.
*************** rewriteTargetView(Query *parsetree, Rela
*** 2743,2748 ****
--- 2739,2748 ----
  	 * only adjust their varnos to reference the new target (just the same as
  	 * we did with the view targetlist).
  	 *
+ 	 * Note that there is special-case handling for the quals of a security
+ 	 * barrier view, since they need to be kept separate from any user-supplied
+ 	 * quals, so these quals are kept on the new target RTE.
+ 	 *
  	 * For INSERT, the view's quals can be ignored in the main query.
  	 */
  	if (parsetree->commandType != CMD_INSERT &&
*************** rewriteTargetView(Query *parsetree, Rela
*** 2751,2757 ****
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 		AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
--- 2751,2771 ----
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 
! 		if (RelationIsSecurityView(view))
! 		{
! 			/*
! 			 * Note: the parsetree has been mutated, so the new_rte pointer is
! 			 * stale and needs to be re-computed.
! 			 */
! 			new_rte = rt_fetch(new_rt_index, parsetree->rtable);
! 			new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals);
! 
! 			if (!parsetree->hasSubLinks)
! 				parsetree->hasSubLinks = checkExprHasSubLink(viewqual);
! 		}
! 		else
! 			AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
*************** rewriteTargetView(Query *parsetree, Rela
*** 2813,2821 ****
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added to the
! 				 * query's WHERE clause, and AddQual will have already done
! 				 * this check.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
--- 2827,2834 ----
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added, and this
! 				 * check will already have been done.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index e89d930..36c5c98
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblEntry
*** 801,806 ****
--- 801,807 ----
  	Oid			checkAsUser;	/* if valid, check access as this role */
  	Bitmapset  *selectedCols;	/* columns needing SELECT permission */
  	Bitmapset  *modifiedCols;	/* columns needing INSERT/UPDATE permission */
+ 	List	   *securityQuals;	/* any security barrier quals to apply */
  } RangeTblEntry;
  
  /*
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
new file mode 100644
index 0934e63..e9d865d
*** a/src/include/optimizer/prep.h
--- b/src/include/optimizer/prep.h
*************** extern Node *negate_clause(Node *node);
*** 36,41 ****
--- 36,46 ----
  extern Expr *canonicalize_qual(Expr *qual);
  
  /*
+  * prototypes for prepsecurity.c
+  */
+ extern void expand_security_quals(PlannerInfo *root, List *tlist);
+ 
+ /*
   * prototypes for preptlist.c
   */
  extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
new file mode 100644
index c959590..ce56d7b
*** a/src/include/rewrite/rewriteHandler.h
--- b/src/include/rewrite/rewriteHandler.h
*************** extern void AcquireRewriteLocks(Query *p
*** 23,29 ****
  extern Node *build_column_default(Relation rel, int attrno);
  extern Query *get_view_query(Relation view);
  extern const char *view_query_is_auto_updatable(Query *viewquery,
- 										 bool security_barrier,
  										 bool check_cols);
  extern int	relation_is_updatable(Oid reloid,
  						  bool include_triggers,
--- 23,28 ----
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
new file mode 100644
index 91d1639..f6db582
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
*************** CREATE VIEW mysecview4 WITH (security_ba
*** 252,258 ****
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  security_barrier requires a Boolean value
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
--- 252,258 ----
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  invalid value for boolean option "security_barrier": 100
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 99c9165..d12d384
*** a/src/test/regress/expected/updatable_views.out
--- b/src/test/regress/expected/updatable_views.out
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 22,33 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
--- 22,31 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
*************** SELECT table_name, is_insertable_into
*** 44,50 ****
   ro_view19  | NO
   ro_view2   | NO
   ro_view20  | NO
-  ro_view21  | NO
   ro_view3   | NO
   ro_view4   | NO
   ro_view5   | NO
--- 42,47 ----
*************** SELECT table_name, is_insertable_into
*** 55,61 ****
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (21 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
--- 52,58 ----
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (20 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
*************** SELECT table_name, is_updatable, is_inse
*** 73,79 ****
   ro_view19  | NO           | NO
   ro_view2   | NO           | NO
   ro_view20  | NO           | NO
-  ro_view21  | NO           | NO
   ro_view3   | NO           | NO
   ro_view4   | NO           | NO
   ro_view5   | NO           | NO
--- 70,75 ----
*************** SELECT table_name, is_updatable, is_inse
*** 84,90 ****
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (21 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
--- 80,86 ----
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (20 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
*************** SELECT table_name, column_name, is_updat
*** 103,125 ****
   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_view21  | a             | NO
!  ro_view21  | b             | NO
!  ro_view21  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
--- 99,119 ----
   ro_view17  | a             | NO
   ro_view17  | b             | NO
   ro_view18  | a             | NO
!  ro_view19  | sequence_name | NO
!  ro_view19  | last_value    | NO
!  ro_view19  | start_value   | NO
!  ro_view19  | increment_by  | NO
!  ro_view19  | max_value     | NO
!  ro_view19  | min_value     | NO
!  ro_view19  | cache_value   | NO
!  ro_view19  | log_cnt       | NO
!  ro_view19  | is_cycled     | NO
!  ro_view19  | is_called     | NO
   ro_view2   | a             | NO
   ro_view2   | b             | NO
!  ro_view20  | a             | NO
!  ro_view20  | b             | NO
!  ro_view20  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
*************** SELECT table_name, column_name, is_updat
*** 140,146 ****
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (48 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
--- 134,140 ----
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (46 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
*************** INSERT INTO ro_view17 VALUES (3, 'ROW 3'
*** 268,291 ****
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! 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 enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view21 SET b=upper(b);
! ERROR:  cannot update view "ro_view21"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  DROP TABLE base_tbl CASCADE;
! NOTICE:  drop cascades to 17 other objects
  DETAIL:  drop cascades to view ro_view1
  drop cascades to view ro_view17
  drop cascades to view ro_view2
--- 262,281 ----
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! DELETE FROM ro_view18;
! ERROR:  cannot delete from view "ro_view18"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! UPDATE ro_view19 SET max_value=1000;
! ERROR:  cannot update view "ro_view19"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view20 SET b=upper(b);
! ERROR:  cannot update view "ro_view20"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  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_view11
*** 299,311 ****
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view18
! drop cascades to view ro_view21
  drop cascades to view ro_view4
  drop cascades to view rw_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);
--- 289,300 ----
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view20
  drop cascades to view ro_view4
  drop cascades to view rw_view14
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
! NOTICE:  drop cascades to view ro_view19
  -- 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);
*************** INSERT INTO rw_view2 VALUES (2,3); -- ok
*** 1739,1742 ****
--- 1728,1923 ----
  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
+ -- security barrier view
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+                   QUERY PLAN                   
+ -----------------------------------------------
+  Subquery Scan on rw_view1
+    Filter: snoop(rw_view1.person)
+    ->  Seq Scan on base_tbl
+          Filter: (visibility = 'public'::text)
+ (4 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ -- security barrier view on top of security barrier view
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Subquery Scan on rw_view2
+    Filter: snoop(rw_view2.person)
+    ->  Subquery Scan on rw_view1
+          Filter: snoop(rw_view1.person)
+          ->  Seq Scan on base_tbl
+                Filter: (visibility = 'public'::text)
+ (6 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 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
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index a77cf19..6b352c1
*** a/src/test/regress/sql/updatable_views.sql
--- b/src/test/regress/sql/updatable_views.sql
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 25,36 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
--- 25,34 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
*************** SELECT * FROM base_tbl;
*** 87,99 ****
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  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;
! UPDATE ro_view21 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view19;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
--- 85,96 ----
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  INSERT INTO ro_view17 VALUES (3, 'ROW 3');
! DELETE FROM ro_view18;
! UPDATE ro_view19 SET max_value=1000;
! UPDATE ro_view20 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
*************** CREATE VIEW rw_view2 AS
*** 828,830 ****
--- 825,902 ----
    SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION;
  INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check)
  DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view
+ 
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ 
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ 
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ -- security barrier view on top of security barrier view
+ 
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ DROP TABLE base_tbl CASCADE;
#17Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Dean Rasheed (#16)
1 attachment(s)
Re: WIP patch (v2) for updatable security barrier views

On 8 January 2014 10:19, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

The assertion failure with inheritance and sublinks is a separate
issue --- adjust_appendrel_attrs() is not expecting to find any
unplanned sublinks in the query tree when it is invoked, since they
would normally have all been planned by that point. However, the
addition of the new security barrier subqueries after inheritance
expansion can now insert new sublinks which need to be planned. I'll
look into how best to make that happen.

Actually that wasn't quite it. The problem was that an RTE in the
top-level query had a security barrier qual with a sublink in it, and
it wasn't preprocessing those quals, so that sublink was still
unplanned by the time it got to adjust_appendrel_attrs(), which then
complained.

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.
This doesn't affect performance in the normal case, because all other
sublinks in the query will have been turned into subplans by that
point, so it only needs to handle unplanned sublinks in RTE security
barrier quals, which can only happen for updates to s.b. views.

The attached patch does that, which fixes the case you reported.

On 8 January 2014 09:02, Craig Ringer <craig@2ndquadrant.com> wrote:

My main concern is that the securityQuals appear to bypass all later
rewrite stages, inheritance expansion during planning, etc. I suspect
this might be hard to get around (because these are disembodied quals
which may have nonsense varnos), but I'm looking into it now.

Actually that's not the case. The securityQuals are traversed in the
standard walker/mutator functions, so the rewriter *will* recursively
expand any view references they contain (provided the query is
correctly marked with hasSubLinks, which my first patch failed to
do!).

Inheritance expansion of relations in subqueries in the securityQuals
is handled by recursion in the planner --- subquery_planner() invokes
grouping_planner(), expanding securityQuals into new subquery RTEs,
then subquery_planner() is recursively invoked for the new RTE
subquery, which preprocesses its quals, which recursively invokes
subquery_planner() for the sublinks in those quals, which then expands
the inheritance sets they contain.

My understanding from reading the patch is that this:

- Flattens target views in rewriteTargetView, as in current master. If
the target view is a security barrier view, the view quals are appended
to a list of security barrier quals on the new RTE, instead of appended
to the RTE's normal quals like for normal views.

Right.

After rewrite the views are fully flattened down to a RTE_RELATION,
which becomes the resultRelation. An unreferenced RTE for each view
that's been rewritten is preserved in the range-table for permissions
checking purposes only (same as current master).

Right.

- Inheritance expansion, tlist expansion, etc then occurrs as normal.

Right.

- In planning, in inheritance_planner, if any RTE has any stashed
security quals in its RangeTableEntry, expand_security_qual is invoked.
This iteratively wraps the base relation in a subquery with the saved
security barrier quals, creating nested subqueries around the original
RTE. At each pass resultRelation is changed to point to the new
outer-most subquery.

Actually the resultRelation is only changed in the first pass.

Each subsequent pass that creates an additional nested subquery RTE
modifies the old subquery RTE in-place. The new subquery has an
"identity" targetlist, which means that no further rewriting of the
outer query is necessary after the first s.b. subquery is created.
This avoids having multiple levels of attribute rewriting in the case
where s.b. views are nested on top of one another.

As a result of this approach everything looks normal to
preprocess_targetlist, row-marking, etc, because they're seeing a normal
RTE_RELATION as resultRelation. The security barrier quals are, at this
stage, stashed aside. If there's inheritance involved, RTEs copied
during appendrel expansion get copies of the security quals on in the
parent RTE.

Problem with inheritance, views, etc in s.b. quals
--------------------------------------------------

After inheritance expansion, tlist expansion, etc, the s.b. quals are
unpacked to create subqueries wrapping the original RTEs.

So, with:

CREATE TABLE t1 (x float, b integer, secret1 text, secret2 text);
CREATE TABLE t1child (z integer) INHERITS (t1);

INSERT INTO t1 (x, b, secret1, secret2)
VALUES
(0,0,'secret0', 'supersecret'),
(1,1,'secret1', 'supersecret'),
(2,2,'secret2', 'supersecret'),
(3,3,'secret3', 'supersecret'),
(4,4,'secret4', 'supersecret'),
(5,6,'secret5', 'supersecret');

INSERT INTO t1child (x, b, secret1, secret2, z)
VALUES
(8,8,'secret8', 'ss', 8),
(9,9,'secret8', 'ss', 9),
(10,10,'secret8', 'ss', 10);

CREATE VIEW v1
WITH (security_barrier)
AS
SELECT b AS b1, x AS x1, secret1
FROM t1 WHERE b % 2 = 0;

CREATE VIEW v2
WITH (security_barrier)
AS
SELECT b1 AS b2, x1 AS x2
FROM v1 WHERE b1 % 4 = 0;

then a statement like:

UPDATE v2
SET x2 = x2 + 32;

will be rewritten into something like (imaginary sql)

UPDATE t1 WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0))
SET x = x + 32

inheritance-expanded and tlist-expanded into something like (imaginary SQL)

UPDATE
(t1 WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0)))
UNION ALL
(t1child WITH SECURITY QUALS ((b % 2 == 0), (b % 4 == 0)))
SET x = x + 32;

after which security qual expansion occurs, giving us something like:

UPDATE
t1, t1child <--- resultRelations
(
SELECT v2.ctid, v2.*
FROM (
SELECT v1.ctid, v1.*
FROM (
SELECT t1.ctid, t1.*
FROM t1
WHERE b % 2 == 0
) v1
WHERE b % 4 == 0
) v2

UNION ALL

SELECT v2.ctid, v2.*
FROM (
SELECT v1.ctid, v1.*
FROM (
SELECT t1child.ctid, t1child.*
FROM t1child
WHERE b % 2 == 0
) v1
WHERE b % 4 == 0
) v2

)
SET x = x + 32;

Giving a plan looking like:

EXPLAIN UPDATE v2 SET x2 = 32

QUERY PLAN
---------------------------------------------------------------------------
Update on t1 t1_2 (cost=0.00..23.35 rows=2 width=76)
-> Subquery Scan on t1 (cost=0.00..2.18 rows=1 width=74)
-> Subquery Scan on t1_3 (cost=0.00..2.17 rows=1 width=74)
Filter: ((t1_3.b % 4) = 0)
-> Seq Scan on t1 t1_4 (cost=0.00..2.16 rows=1 width=74)
Filter: ((b % 2) = 0)
-> Subquery Scan on t1_1 (cost=0.00..21.17 rows=1 width=78)
-> Subquery Scan on t1_5 (cost=0.00..21.16 rows=1 width=78)
Filter: ((t1_5.b % 4) = 0)
-> Seq Scan on t1child (cost=0.00..21.10 rows=4 width=78)
Filter: ((b % 2) = 0)
(11 rows)

So far this looks like a really clever approach. My only real concern is
that the security quals are currently hidden from rewrite and parsing
before during the period they're hidden away in the security quals RTI
field.

This means they aren't processed for things like inheritance expansion. e.g.

CREATE TABLE rowfilter (remainder integer, userid text);
CREATE TABLE rowfilterchild () INHERITS (rowfilter);
INSERT INTO rowfilterchild(remainder, userid) values (0, current_user);

a view with a security qual that refers to an inherited relation won't work:

CREATE VIEW sqv
WITH (security_barrier)
AS
SELECT x, b FROM t1 WHERE (
SELECT b % 4 = remainder
FROM rowfilter
WHERE userid = current_user
OFFSET 0
);

This is a bit contrived to force the optimiser to treat the subquery as
correlated and thus make sure the ref to rowfilter gets into the
securityQuals list.

I expected zero results (a scan of rowfilter, but not rowfilterchild,
resulting in a null subquery return) but land up with an assertion
failure instead. The assertion triggers for any security qual containing
a correlated subquery, so it's crashing out before we can break due to
failure to expand inheritance.

Having fixed the assertion failure by making adjust_appendrel_attrs()
handle descent into sublink subqueries, this now works, and it does
correctly expand the inheritance in the subquery in the s.b. qual, for
the reasons explained above:

explain (verbose, costs off) update sqv set b=b*10 where b%5=0;
QUERY PLAN
-------------------------------------------------------------------------------------------------------
Update on public.t1 t1_2
-> Subquery Scan on t1
Output: t1.x, (t1.b * 10), t1.secret1, t1.secret2, t1.ctid
Filter: ((t1.b % 5) = 0)
-> Seq Scan on public.t1 t1_3
Output: t1_3.b, t1_3.ctid, t1_3.x, t1_3.secret1, t1_3.secret2
Filter: (SubPlan 1)
SubPlan 1
-> Result
Output: ((t1_3.b % 4) = rowfilter.remainder)
-> Append
-> Seq Scan on public.rowfilter
Output: rowfilter.remainder
Filter: (rowfilter.userid =
("current_user"())::text)
-> Seq Scan on public.rowfilterchild
Output: rowfilterchild.remainder
Filter: (rowfilterchild.userid =
("current_user"())::text)
-> Subquery Scan on t1_1
Output: t1_1.x, (t1_1.b * 10), t1_1.secret1, t1_1.secret2,
t1_1.z, t1_1.ctid
Filter: ((t1_1.b % 5) = 0)
-> Seq Scan on public.t1child
Output: t1child.b, t1child.ctid, t1child.x,
t1child.secret1, t1child.secret2, t1child.z
Filter: (SubPlan 2)
SubPlan 2
-> Result
Output: ((t1child.b % 4) = rowfilter_1.remainder)
-> Append
-> Seq Scan on public.rowfilter rowfilter_1
Output: rowfilter_1.remainder
Filter: (rowfilter_1.userid =
("current_user"())::text)
-> Seq Scan on public.rowfilterchild
rowfilterchild_1
Output: rowfilterchild_1.remainder
Filter: (rowfilterchild_1.userid =
("current_user"())::text)

This isn't just about inheritance. In general, we'd need a way to
process those securityQuals through any rewrite phases and through the
parts of planning before they get merged back in, so they're subject to
things like inheritance appendrel expansion.

I believe that should work because the rewriter traverses the query
tree applying rewrite rules to everything it sees, and that will
include any securityQuals.

In fact one of my concerns about your approach of expanding the s.b.
view in rewriteTargetView() was how that would interact with later
stages of the rewriter if there were more rules on the base relation.
rewriteTargetView() happens very early in the rewriter, so there is
potentially a lot more that can happen to the query tree after that,
which would make it difficult to keep track of the relationship
between the target RTE and expanded subquery RTE. Storing the
securityQuals on the RTE keeps them tied together, which makes it
easier to predict what will happen, although this still does need a
lot more testing.

Regards,
Dean

doc/src/sgml/ref/create_view.sgml | 6
src/backend/commands/tablecmds.c | 6
src/backend/commands/view.c | 6
src/backend/nodes/copyfuncs.c | 1
src/backend/nodes/equalfuncs.c | 1
src/backend/nodes/nodeFuncs.c | 4
src/backend/nodes/outfuncs.c | 1
src/backend/nodes/readfuncs.c | 1
src/backend/optimizer/plan/planner.c | 37 !!
src/backend/optimizer/prep/Makefile | 2
src/backend/optimizer/prep/prepsecurity.c | 423 ++++++++++++++++++++++++++
src/backend/optimizer/prep/prepunion.c | 57 !!!
src/backend/rewrite/rewriteHandler.c | 53 -
src/include/nodes/parsenodes.h | 1
src/include/optimizer/prep.h | 5
src/include/rewrite/rewriteHandler.h | 1
src/test/regress/expected/create_view.out | 2
src/test/regress/expected/updatable_views.out | 261 +++++++++++!!!
src/test/regress/sql/updatable_views.sql | 92 ++++!
19 files changed, 738 insertions(+), 25 deletions(-), 197 modifications(!)

Attachments:

updatable-sb-views.patchtext/x-diff; charset=US-ASCII; name=updatable-sb-views.patchDownload
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
new file mode 100644
index e0fbe1e..888410f
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
*************** CREATE VIEW vista AS SELECT text 'Hello
*** 323,334 ****
         or set-returning functions.
        </para>
       </listitem>
- 
-      <listitem>
-       <para>
-        The view must not have the <literal>security_barrier</> property.
-       </para>
-      </listitem>
      </itemizedlist>
     </para>
  
--- 323,328 ----
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index 466d757..5b7e31a
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** ATExecSetRelOptions(Relation rel, List *
*** 8806,8812 ****
  		List	   *view_options = untransformRelOptions(newOptions);
  		ListCell   *cell;
  		bool		check_option = false;
- 		bool		security_barrier = false;
  
  		foreach(cell, view_options)
  		{
--- 8806,8811 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8814,8821 ****
  
  			if (pg_strcasecmp(defel->defname, "check_option") == 0)
  				check_option = true;
- 			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 				security_barrier = defGetBoolean(defel);
  		}
  
  		/*
--- 8813,8818 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8825,8832 ****
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query,
! 											 security_barrier, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
--- 8822,8828 ----
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
new file mode 100644
index 1735762..bc08566
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
*************** DefineView(ViewStmt *stmt, const char *q
*** 396,402 ****
  	RangeVar   *view;
  	ListCell   *cell;
  	bool		check_option;
- 	bool		security_barrier;
  
  	/*
  	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
--- 396,401 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 451,457 ****
  	 * specified.
  	 */
  	check_option = false;
- 	security_barrier = false;
  
  	foreach(cell, stmt->options)
  	{
--- 450,455 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 459,466 ****
  
  		if (pg_strcasecmp(defel->defname, "check_option") == 0)
  			check_option = true;
- 		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 			security_barrier = defGetBoolean(defel);
  	}
  
  	/*
--- 457,462 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 470,476 ****
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, security_barrier, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
--- 466,472 ----
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index fb4ce2c..e8cdbfb
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 1998,2003 ****
--- 1998,2004 ----
  	COPY_SCALAR_FIELD(checkAsUser);
  	COPY_BITMAPSET_FIELD(selectedCols);
  	COPY_BITMAPSET_FIELD(modifiedCols);
+ 	COPY_NODE_FIELD(securityQuals);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index ccf7267..0411519
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2278,2283 ****
--- 2278,2284 ----
  	COMPARE_SCALAR_FIELD(checkAsUser);
  	COMPARE_BITMAPSET_FIELD(selectedCols);
  	COMPARE_BITMAPSET_FIELD(modifiedCols);
+ 	COMPARE_NODE_FIELD(securityQuals);
  
  	return true;
  }
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 123f2a6..1e48a7f
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** range_table_walker(List *rtable,
*** 2020,2025 ****
--- 2020,2028 ----
  					return true;
  				break;
  		}
+ 
+ 		if (walker(rte->securityQuals, context))
+ 			return true;
  	}
  	return false;
  }
*************** range_table_mutator(List *rtable,
*** 2755,2760 ****
--- 2758,2764 ----
  				MUTATE(newrte->values_lists, rte->values_lists, List *);
  				break;
  		}
+ 		MUTATE(newrte->securityQuals, rte->securityQuals, List *);
  		newrt = lappend(newrt, newrte);
  	}
  	return newrt;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 568c3b8..a0e3286
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outRangeTblEntry(StringInfo str, const
*** 2409,2414 ****
--- 2409,2415 ----
  	WRITE_OID_FIELD(checkAsUser);
  	WRITE_BITMAPSET_FIELD(selectedCols);
  	WRITE_BITMAPSET_FIELD(modifiedCols);
+ 	WRITE_NODE_FIELD(securityQuals);
  }
  
  static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index 8a7a662..c81c5cb
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readRangeTblEntry(void)
*** 1250,1255 ****
--- 1250,1256 ----
  	READ_OID_FIELD(checkAsUser);
  	READ_BITMAPSET_FIELD(selectedCols);
  	READ_BITMAPSET_FIELD(modifiedCols);
+ 	READ_NODE_FIELD(securityQuals);
  
  	READ_DONE();
  }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 35bda67..1beb6c8
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 916,921 ****
--- 916,927 ----
  		subplan = grouping_planner(&subroot, 0.0 /* retrieve all tuples */ );
  
  		/*
+ 		 * Planning may have modified the query result relation (if there
+ 		 * were security barrier quals on the result RTE).
+ 		 */
+ 		appinfo->child_relid = subroot.parse->resultRelation;
+ 
+ 		/*
  		 * If this child rel was excluded by constraint exclusion, exclude it
  		 * from the result plan.
  		 */
*************** inheritance_planner(PlannerInfo *root)
*** 932,940 ****
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 			final_rtable = list_concat(final_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
--- 938,969 ----
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 		{
! 			List	   *tmp_rtable = NIL;
! 			ListCell   *cell1, *cell2;
! 
! 			/*
! 			 * Planning this new child may have turned some of the original
! 			 * RTEs into subqueries (if they had security barrier quals). If
! 			 * so, we want to use these in the final rtable.
! 			 */
! 			forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
! 			{
! 				RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
! 				RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
! 
! 				if (rte1->rtekind == RTE_RELATION &&
! 					rte1->securityQuals != NIL &&
! 					rte2->rtekind == RTE_SUBQUERY)
! 					tmp_rtable = lappend(tmp_rtable, rte2);
! 				else
! 					tmp_rtable = lappend(tmp_rtable, rte1);
! 			}
! 
! 			final_rtable = list_concat(tmp_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
+ 		}
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
*************** grouping_planner(PlannerInfo *root, doub
*** 1163,1168 ****
--- 1192,1203 ----
  		tlist = preprocess_targetlist(root, tlist);
  
  		/*
+ 		 * Expand any rangetable entries that have security barrier quals.
+ 		 * This may add new security barrier subquery RTEs to the rangetable.
+ 		 */
+ 		expand_security_quals(root, tlist);
+ 
+ 		/*
  		 * Locate any window functions in the tlist.  (We don't need to look
  		 * anywhere else, since expressions used in ORDER BY will be in there
  		 * too.)  Note that they could all have been eliminated by constant
diff --git a/src/backend/optimizer/prep/Makefile b/src/backend/optimizer/prep/Makefile
new file mode 100644
index 86301bf..5195d9b
*** a/src/backend/optimizer/prep/Makefile
--- b/src/backend/optimizer/prep/Makefile
*************** subdir = src/backend/optimizer/prep
*** 12,17 ****
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
--- 12,17 ----
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o prepsecurity.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
new file mode 100644
index ...e7a3f56
*** a/src/backend/optimizer/prep/prepsecurity.c
--- b/src/backend/optimizer/prep/prepsecurity.c
***************
*** 0 ****
--- 1,423 ----
+ /*-------------------------------------------------------------------------
+  *
+  * prepsecurity.c
+  *	  Routines for preprocessing security barrier quals.
+  *
+  * Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
+  * Portions Copyright (c) 1994, Regents of the University of California
+  *
+  *
+  * IDENTIFICATION
+  *	  src/backend/optimizer/prep/prepsecurity.c
+  *
+  *-------------------------------------------------------------------------
+  */
+ #include "postgres.h"
+ 
+ #include "access/heapam.h"
+ #include "access/sysattr.h"
+ #include "catalog/heap.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/prep.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteManip.h"
+ #include "utils/rel.h"
+ 
+ 
+ typedef struct
+ {
+ 	int			rt_index;		/* Index of security barrier RTE */
+ 	int			sublevels_up;	/* Current nesting depth */
+ 	Relation	rel;			/* RTE relation at rt_index */
+ 	List	   *targetlist;		/* Targetlist for new subquery RTE */
+ 	List	   *colnames;		/* Column names in subquery RTE */
+ 	List	   *vars_processed;	/* List of Vars already processed */
+ } security_barrier_replace_vars_context;
+ 
+ static void expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual);
+ 
+ static void security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context);
+ 
+ static bool security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context);
+ 
+ 
+ /*
+  * expand_security_quals -
+  *	  expands any security barrier quals on RTEs in the query rtable, turning
+  *	  them into security barrier subqueries.
+  *
+  * Any given RTE may have multiple security barrier quals in a list, from which
+  * we create a set of nested subqueries to isolate each security barrier from
+  * the others, providing protection against malicious user-defined security
+  * barriers.  The first security barrier qual in the list will be used in the
+  * innermost subquery.
+  */
+ void
+ expand_security_quals(PlannerInfo *root, List *tlist)
+ {
+ 	Query	   *parse = root->parse;
+ 	int			rt_index;
+ 
+ 	/*
+ 	 * Process each RTE in the rtable list.
+ 	 *
+ 	 * Note that this is deliberately not a foreach loop, since the rtable may
+ 	 * be modified each time through the loop.
+ 	 */
+ 	rt_index = 0;
+ 	while (rt_index < list_length(parse->rtable))
+ 	{
+ 		RangeTblEntry *rte;
+ 
+ 		rt_index++;
+ 		rte = rt_fetch(rt_index, parse->rtable);
+ 
+ 		if (rte->securityQuals == NIL)
+ 			continue;
+ 
+ 		/*
+ 		 * Ignore any RTEs that aren't used in the query (such RTEs may be
+ 		 * present for permissions checks).
+ 		 */
+ 		if (rt_index != parse->resultRelation &&
+ 			!rangeTableEntry_used((Node *) parse, rt_index, 0))
+ 			continue;
+ 
+ 		/*
+ 		 * If this RTE is the target then we need to make a copy of it before
+ 		 * expanding it.  The unexpanded copy will become the new target, and
+ 		 * the original RTE will be expanded to become the source of rows to
+ 		 * update/delete.
+ 		 */
+ 		if (rt_index == parse->resultRelation)
+ 		{
+ 			RangeTblEntry *newrte = copyObject(rte);
+ 			parse->rtable = lappend(parse->rtable, newrte);
+ 			parse->resultRelation = list_length(parse->rtable);
+ 
+ 			/*
+ 			 * Wipe out any copied security barrier quals on the new target to
+ 			 * prevent infinite recursion.
+ 			 */
+ 			newrte->securityQuals = NIL;
+ 
+ 			/*
+ 			 * There's no need to do permissions checks twice, so wipe out the
+ 			 * permissions info for the original RTE (we prefer to keep the
+ 			 * bits set on the result RTE).
+ 			 */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * For the most part, Vars referencing the original relation should
+ 			 * remain as they are, meaning that they pull OLD values from the
+ 			 * expanded RTE.  But in the RETURNING list and in any WITH CHECK
+ 			 * OPTION quals, we want such Vars to represent NEW values, so
+ 			 * change them to reference the new RTE.
+ 			 */
+ 			ChangeVarNodes((Node *) parse->returningList, rt_index,
+ 						   parse->resultRelation, 0);
+ 
+ 			ChangeVarNodes((Node *) parse->withCheckOptions, rt_index,
+ 						   parse->resultRelation, 0);
+ 		}
+ 
+ 		/*
+ 		 * Process each security barrier qual in turn, starting with the
+ 		 * innermost one (the first in the list) and working outwards.
+ 		 *
+ 		 * We remove each qual from the list before processing it, so that its
+ 		 * variables aren't modified by expand_security_qual.  Also we don't
+ 		 * necessarily want the attributes referred to by the qual to be
+ 		 * exposed by the newly built subquery.
+ 		 */
+ 		while (rte->securityQuals != NIL)
+ 		{
+ 			Node   *qual = (Node *) linitial(rte->securityQuals);
+ 			rte->securityQuals = list_delete_first(rte->securityQuals);
+ 
+ 			ChangeVarNodes(qual, rt_index, 1, 0);
+ 			expand_security_qual(parse, tlist, rt_index, qual);
+ 		}
+ 	}
+ }
+ 
+ 
+ /*
+  * expand_security_qual -
+  *	  expand the specified security barrier qual on a query RTE, turning the
+  *	  RTE into a security barrier subquery.
+  */
+ static void
+ expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual)
+ {
+ 	RangeTblEntry  *rte;
+ 	Oid				relid;
+ 	Query		   *subquery;
+ 	RangeTblEntry  *subrte;
+ 	RangeTblRef	   *subrtr;
+ 	security_barrier_replace_vars_context context;
+ 	ListCell	   *cell;
+ 
+ 	rte = rt_fetch(rt_index, parse->rtable);
+ 	relid = rte->relid;
+ 
+ 	/*
+ 	 * There should only be 2 possible cases:
+ 	 *
+ 	 * 1. A relation RTE, which we turn into a subquery RTE containing all
+ 	 * referenced columns.
+ 	 *
+ 	 * 2. A subquery RTE (either from a prior call to this function or from an
+ 	 * expanded view).  In this case we build a new subquery on top of it to
+ 	 * isolate this security barrier qual from any other quals.
+ 	 */
+ 	switch (rte->rtekind)
+ 	{
+ 		case RTE_RELATION:
+ 			/*
+ 			 * Turn the relation RTE into a security barrier subquery RTE,
+ 			 * moving all permissions checks down into the subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 
+ 			subrte = copyObject(rte);
+ 			subrte->inFromCl = true;
+ 			subrte->securityQuals = NIL;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->rtekind = RTE_SUBQUERY;
+ 			rte->relid = InvalidOid;
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 			rte->inh = false;			/* must not be set for a subquery */
+ 
+ 			/* the permissions checks have now been moved down */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * Replace any variables in the outer query that refer to the
+ 			 * original relation RTE with references to columns that we will
+ 			 * expose in the new subquery, building the subquery's targetlist
+ 			 * as we go.
+ 			 */
+ 			context.rt_index = rt_index;
+ 			context.sublevels_up = 0;
+ 			context.rel = heap_open(relid, NoLock);
+ 			context.targetlist = NIL;
+ 			context.colnames = NIL;
+ 			context.vars_processed = NIL;
+ 
+ 			security_barrier_replace_vars((Node *) parse, &context);
+ 			security_barrier_replace_vars((Node *) tlist, &context);
+ 
+ 			heap_close(context.rel, NoLock);
+ 
+ 			/* Now we know what columns the subquery needs to expose */
+ 			rte->subquery->targetList = context.targetlist;
+ 			rte->eref = makeAlias(rte->eref->aliasname, context.colnames);
+ 
+ 			break;
+ 
+ 		case RTE_SUBQUERY:
+ 			/*
+ 			 * Build a new subquery that includes all the same columns as the
+ 			 * original subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 			subquery->targetList = NIL;
+ 
+ 			foreach(cell, rte->subquery->targetList)
+ 			{
+ 				TargetEntry	   *tle;
+ 				Var			   *var;
+ 
+ 				tle = (TargetEntry *) lfirst(cell);
+ 				var = makeVarFromTargetEntry(1, tle);
+ 
+ 				tle = makeTargetEntry((Expr *) var,
+ 									  list_length(subquery->targetList) + 1,
+ 									  pstrdup(tle->resname),
+ 									  tle->resjunk);
+ 				subquery->targetList = lappend(subquery->targetList, tle);
+ 			}
+ 
+ 			subrte = makeNode(RangeTblEntry);
+ 			subrte->rtekind = RTE_SUBQUERY;
+ 			subrte->subquery = rte->subquery;
+ 			subrte->security_barrier = rte->security_barrier;
+ 			subrte->eref = copyObject(rte->eref);
+ 			subrte->inFromCl = true;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 
+ 			break;
+ 
+ 		default:
+ 			elog(ERROR, "invalid range table entry for security barrier qual");
+ 	}
+ }
+ 
+ 
+ /*
+  * security_barrier_replace_vars -
+  *	  Apply security barrier variable replacement to an expression tree.
+  *
+  * This also builds/updates a targetlist with entries for each replacement
+  * variable that needs to be exposed by the security barrier subquery RTE.
+  *
+  * NOTE: although this has the form of a walker, we cheat and modify the
+  * nodes in-place.	The given expression tree should have been copied
+  * earlier to ensure that no unwanted side-effects occur!
+  */
+ static void
+ security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context)
+ {
+ 	/*
+ 	 * Must be prepared to start with a Query or a bare expression tree; if
+ 	 * it's a Query, go straight to query_tree_walker to make sure that
+ 	 * sublevels_up doesn't get incremented prematurely.
+ 	 */
+ 	if (node && IsA(node, Query))
+ 		query_tree_walker((Query *) node,
+ 						  security_barrier_replace_vars_walker,
+ 						  (void *) context, 0);
+ 	else
+ 		security_barrier_replace_vars_walker(node, context);
+ }
+ 
+ static bool
+ security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context)
+ {
+ 	if (node == NULL)
+ 		return false;
+ 	if (IsA(node, Var))
+ 	{
+ 		Var		   *var = (Var *) node;
+ 
+ 		/*
+ 		 * Note that the same Var may be present in different lists, so we
+ 		 * need to take care not to process it multiple times.
+ 		 */
+ 		if (var->varno == context->rt_index &&
+ 			var->varlevelsup == context->sublevels_up &&
+ 			!list_member_ptr(context->vars_processed, var))
+ 		{
+ 			/*
+ 			 * Found a matching variable. Make sure that it is in the subquery
+ 			 * targetlist and map its attno accordingly.
+ 			 */
+ 			AttrNumber	attno;
+ 			ListCell   *l;
+ 			TargetEntry *tle;
+ 			char	   *attname;
+ 			Var		   *newvar;
+ 
+ 			/* Search for the base attribute in the subquery targetlist */
+ 			attno = InvalidAttrNumber;
+ 			foreach(l, context->targetlist)
+ 			{
+ 				tle = (TargetEntry *) lfirst(l);
+ 				attno++;
+ 
+ 				Assert(IsA(tle->expr, Var));
+ 				if (((Var *) tle->expr)->varattno == var->varattno &&
+ 					((Var *) tle->expr)->varcollid == var->varcollid)
+ 				{
+ 					/* Map the variable onto this subquery targetlist entry */
+ 					var->varattno = attno;
+ 					return false;
+ 				}
+ 			}
+ 
+ 			/* Not in the subquery targetlist, so add it. Get its name. */
+ 			if (var->varattno < 0)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = SystemAttributeDefinition(var->varattno,
+ 													context->rel->rd_rel->relhasoids);
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else if (var->varattno == InvalidAttrNumber)
+ 			{
+ 				attname = "wholerow";
+ 			}
+ 			else if (var->varattno <= context->rel->rd_att->natts)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = context->rel->rd_att->attrs[var->varattno - 1];
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else
+ 			{
+ 				elog(ERROR, "invalid attribute number %d in security_barrier_replace_vars", var->varattno);
+ 			}
+ 
+ 			/* New variable for subquery targetlist */
+ 			newvar = copyObject(var);
+ 			newvar->varno = 1;
+ 
+ 			attno = list_length(context->targetlist) + 1;
+ 			tle = makeTargetEntry((Expr *) newvar,
+ 								  attno,
+ 								  pstrdup(attname),
+ 								  false);
+ 
+ 			context->targetlist = lappend(context->targetlist, tle);
+ 
+ 			context->colnames = lappend(context->colnames,
+ 										makeString(pstrdup(attname)));
+ 
+ 			/* Update the outer query's variable */
+ 			var->varattno = attno;
+ 
+ 			/* Remember this Var so that we don't process it again */
+ 			context->vars_processed = lappend(context->vars_processed, var);
+ 		}
+ 		return false;
+ 	}
+ 
+ 	if (IsA(node, Query))
+ 	{
+ 		/* Recurse into subselects */
+ 		bool		result;
+ 
+ 		context->sublevels_up++;
+ 		result = query_tree_walker((Query *) node,
+ 								   security_barrier_replace_vars_walker,
+ 								   (void *) context, 0);
+ 		context->sublevels_up--;
+ 		return result;
+ 	}
+ 	return expression_tree_walker(node, security_barrier_replace_vars_walker,
+ 								  (void *) context);
+ }
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
new file mode 100644
index 52dcc72..c7e0199
*** a/src/backend/optimizer/prep/prepunion.c
--- b/src/backend/optimizer/prep/prepunion.c
*************** typedef struct
*** 55,60 ****
--- 55,61 ----
  {
  	PlannerInfo *root;
  	AppendRelInfo *appinfo;
+ 	int			sublevels_up;
  } adjust_appendrel_attrs_context;
  
  static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
*************** translate_col_privs(const Bitmapset *par
*** 1580,1587 ****
   *	  child rel instead.  We also update rtindexes appearing outside Vars,
   *	  such as resultRelation and jointree relids.
   *
!  * Note: this is only applied after conversion of sublinks to subplans,
!  * so we don't need to cope with recursion into sub-queries.
   *
   * Note: this is not hugely different from what pullup_replace_vars() does;
   * maybe we should try to fold the two routines together.
--- 1581,1589 ----
   *	  child rel instead.  We also update rtindexes appearing outside Vars,
   *	  such as resultRelation and jointree relids.
   *
!  * Note: this is applied after conversion of sublinks to subplans in the
!  * query jointree, but there may still be sublinks in the security barrier
!  * quals of RTEs, so we do need to cope with recursion into sub-queries.
   *
   * Note: this is not hugely different from what pullup_replace_vars() does;
   * maybe we should try to fold the two routines together.
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1594,1602 ****
  
  	context.root = root;
  	context.appinfo = appinfo;
  
  	/*
! 	 * Must be prepared to start with a Query or a bare expression tree.
  	 */
  	if (node && IsA(node, Query))
  	{
--- 1596,1607 ----
  
  	context.root = root;
  	context.appinfo = appinfo;
+ 	context.sublevels_up = 0;
  
  	/*
! 	 * Must be prepared to start with a Query or a bare expression tree; if
! 	 * it's a Query, go straight to query_tree_walker to make sure that
! 	 * sublevels_up doesn't get incremented prematurely.
  	 */
  	if (node && IsA(node, Query))
  	{
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1635,1641 ****
  	{
  		Var		   *var = (Var *) copyObject(node);
  
! 		if (var->varlevelsup == 0 &&
  			var->varno == appinfo->parent_relid)
  		{
  			var->varno = appinfo->child_relid;
--- 1640,1646 ----
  	{
  		Var		   *var = (Var *) copyObject(node);
  
! 		if (var->varlevelsup == context->sublevels_up &&
  			var->varno == appinfo->parent_relid)
  		{
  			var->varno = appinfo->child_relid;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1652,1657 ****
--- 1657,1663 ----
  				if (newnode == NULL)
  					elog(ERROR, "attribute %d of relation \"%s\" does not exist",
  						 var->varattno, get_rel_name(appinfo->parent_reloid));
+ 				((Var *) newnode)->varlevelsup += context->sublevels_up;
  				return newnode;
  			}
  			else if (var->varattno == 0)
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1694,1703 ****
--- 1700,1715 ----
  					RowExpr    *rowexpr;
  					List	   *fields;
  					RangeTblEntry *rte;
+ 					ListCell   *lc;
  
  					rte = rt_fetch(appinfo->parent_relid,
  								   context->root->parse->rtable);
  					fields = (List *) copyObject(appinfo->translated_vars);
+ 					foreach(lc, fields)
+ 					{
+ 						Var		   *field = (Var *) lfirst(lc);
+ 						field->varlevelsup += context->sublevels_up;
+ 					}
  					rowexpr = makeNode(RowExpr);
  					rowexpr->args = fields;
  					rowexpr->row_typeid = var->vartype;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1716,1722 ****
  	{
  		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
  
! 		if (cexpr->cvarno == appinfo->parent_relid)
  			cexpr->cvarno = appinfo->child_relid;
  		return (Node *) cexpr;
  	}
--- 1728,1735 ----
  	{
  		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
  
! 		if (context->sublevels_up == 0 &&
! 			cexpr->cvarno == appinfo->parent_relid)
  			cexpr->cvarno = appinfo->child_relid;
  		return (Node *) cexpr;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1724,1730 ****
  	{
  		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
  
! 		if (rtr->rtindex == appinfo->parent_relid)
  			rtr->rtindex = appinfo->child_relid;
  		return (Node *) rtr;
  	}
--- 1737,1744 ----
  	{
  		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
  
! 		if (context->sublevels_up == 0 &&
! 			rtr->rtindex == appinfo->parent_relid)
  			rtr->rtindex = appinfo->child_relid;
  		return (Node *) rtr;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1737,1743 ****
  											  adjust_appendrel_attrs_mutator,
  												 (void *) context);
  		/* now fix JoinExpr's rtindex (probably never happens) */
! 		if (j->rtindex == appinfo->parent_relid)
  			j->rtindex = appinfo->child_relid;
  		return (Node *) j;
  	}
--- 1751,1758 ----
  											  adjust_appendrel_attrs_mutator,
  												 (void *) context);
  		/* now fix JoinExpr's rtindex (probably never happens) */
! 		if (context->sublevels_up == 0 &&
! 			j->rtindex == appinfo->parent_relid)
  			j->rtindex = appinfo->child_relid;
  		return (Node *) j;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1750,1756 ****
  											  adjust_appendrel_attrs_mutator,
  														 (void *) context);
  		/* now fix PlaceHolderVar's relid sets */
! 		if (phv->phlevelsup == 0)
  			phv->phrels = adjust_relid_set(phv->phrels,
  										   appinfo->parent_relid,
  										   appinfo->child_relid);
--- 1765,1771 ----
  											  adjust_appendrel_attrs_mutator,
  														 (void *) context);
  		/* now fix PlaceHolderVar's relid sets */
! 		if (phv->phlevelsup == context->sublevels_up)
  			phv->phrels = adjust_relid_set(phv->phrels,
  										   appinfo->parent_relid,
  										   appinfo->child_relid);
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1822,1833 ****
  		return (Node *) newinfo;
  	}
  
! 	/*
! 	 * NOTE: we do not need to recurse into sublinks, because they should
! 	 * already have been converted to subplans before we see them.
! 	 */
! 	Assert(!IsA(node, SubLink));
! 	Assert(!IsA(node, Query));
  
  	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
  								   (void *) context);
--- 1837,1862 ----
  		return (Node *) newinfo;
  	}
  
! 	if (IsA(node, Query))
! 	{
! 		/*
! 		 * Recurse into sublink subqueries. This should only be possible in
! 		 * security barrier quals of top-level RTEs. All other sublinks should
! 		 * have already been converted to subplans during expression
! 		 * preprocessing, but this doesn't happen for security barrier quals,
! 		 * since they are destined to become quals of a subquery RTE, which
! 		 * will be recursively planned, and so should not be preprocessed at
! 		 * this stage.
! 		 */
! 		Query	   *newnode;
! 
! 		context->sublevels_up++;
! 		newnode = query_tree_mutator((Query *) node,
! 									 adjust_appendrel_attrs_mutator,
! 									 (void *) context, 0);
! 		context->sublevels_up--;
! 		return (Node *) newnode;
! 	}
  
  	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
  								   (void *) context);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 0b13645..370b5aa
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** view_col_is_auto_updatable(RangeTblRef *
*** 1973,1980 ****
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
! 							 bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
--- 1973,1979 ----
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
*************** view_query_is_auto_updatable(Query *view
*** 2048,2061 ****
  		return gettext_noop("Views that return set-returning functions 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 the future.
- 	 */
- 	if (security_barrier)
- 		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.
  	 */
--- 2047,2052 ----
*************** relation_is_updatable(Oid reloid,
*** 2303,2311 ****
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery,
! 										 RelationIsSecurityView(rel),
! 										 false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
--- 2294,2300 ----
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery, false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
*************** rewriteTargetView(Query *parsetree, Rela
*** 2460,2466 ****
  
  	auto_update_detail =
  		view_query_is_auto_updatable(viewquery,
- 									 RelationIsSecurityView(view),
  									 parsetree->commandType != CMD_DELETE);
  
  	if (auto_update_detail)
--- 2449,2454 ----
*************** rewriteTargetView(Query *parsetree, Rela
*** 2664,2669 ****
--- 2652,2665 ----
  												   view_targetlist);
  
  	/*
+ 	 * Move any security barrier quals from the view RTE onto the new target
+ 	 * RTE.  Any such quals should now apply to the new target RTE and will not
+ 	 * reference the original view RTE in the rewritten query.
+ 	 */
+ 	new_rte->securityQuals = view_rte->securityQuals;
+ 	view_rte->securityQuals = NIL;
+ 
+ 	/*
  	 * 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.
*************** rewriteTargetView(Query *parsetree, Rela
*** 2743,2748 ****
--- 2739,2748 ----
  	 * only adjust their varnos to reference the new target (just the same as
  	 * we did with the view targetlist).
  	 *
+ 	 * Note that there is special-case handling for the quals of a security
+ 	 * barrier view, since they need to be kept separate from any user-supplied
+ 	 * quals, so these quals are kept on the new target RTE.
+ 	 *
  	 * For INSERT, the view's quals can be ignored in the main query.
  	 */
  	if (parsetree->commandType != CMD_INSERT &&
*************** rewriteTargetView(Query *parsetree, Rela
*** 2751,2757 ****
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 		AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
--- 2751,2775 ----
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 
! 		if (RelationIsSecurityView(view))
! 		{
! 			/*
! 			 * Note: the parsetree has been mutated, so the new_rte pointer is
! 			 * stale and needs to be re-computed.
! 			 */
! 			new_rte = rt_fetch(new_rt_index, parsetree->rtable);
! 			new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals);
! 
! 			/*
! 			 * Make sure that the query is marked correctly if the added qual
! 			 * has sublinks.
! 			 */
! 			if (!parsetree->hasSubLinks)
! 				parsetree->hasSubLinks = checkExprHasSubLink(viewqual);
! 		}
! 		else
! 			AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
*************** rewriteTargetView(Query *parsetree, Rela
*** 2813,2821 ****
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added to the
! 				 * query's WHERE clause, and AddQual will have already done
! 				 * this check.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
--- 2831,2838 ----
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added, and this
! 				 * check will already have been done.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index 9a3a5d7..9cdef61
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblEntry
*** 801,806 ****
--- 801,807 ----
  	Oid			checkAsUser;	/* if valid, check access as this role */
  	Bitmapset  *selectedCols;	/* columns needing SELECT permission */
  	Bitmapset  *modifiedCols;	/* columns needing INSERT/UPDATE permission */
+ 	List	   *securityQuals;	/* any security barrier quals to apply */
  } RangeTblEntry;
  
  /*
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
new file mode 100644
index 0f5a7d3..f5fc7e8
*** a/src/include/optimizer/prep.h
--- b/src/include/optimizer/prep.h
*************** extern Node *negate_clause(Node *node);
*** 36,41 ****
--- 36,46 ----
  extern Expr *canonicalize_qual(Expr *qual);
  
  /*
+  * prototypes for prepsecurity.c
+  */
+ extern void expand_security_quals(PlannerInfo *root, List *tlist);
+ 
+ /*
   * prototypes for preptlist.c
   */
  extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
new file mode 100644
index e4027bd..8da0af5
*** a/src/include/rewrite/rewriteHandler.h
--- b/src/include/rewrite/rewriteHandler.h
*************** extern void AcquireRewriteLocks(Query *p
*** 23,29 ****
  extern Node *build_column_default(Relation rel, int attrno);
  extern Query *get_view_query(Relation view);
  extern const char *view_query_is_auto_updatable(Query *viewquery,
- 										 bool security_barrier,
  										 bool check_cols);
  extern int	relation_is_updatable(Oid reloid,
  						  bool include_triggers,
--- 23,28 ----
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
new file mode 100644
index 91d1639..f6db582
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
*************** CREATE VIEW mysecview4 WITH (security_ba
*** 252,258 ****
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  security_barrier requires a Boolean value
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
--- 252,258 ----
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  invalid value for boolean option "security_barrier": 100
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 99c9165..d12d384
*** a/src/test/regress/expected/updatable_views.out
--- b/src/test/regress/expected/updatable_views.out
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 22,33 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
--- 22,31 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
*************** SELECT table_name, is_insertable_into
*** 44,50 ****
   ro_view19  | NO
   ro_view2   | NO
   ro_view20  | NO
-  ro_view21  | NO
   ro_view3   | NO
   ro_view4   | NO
   ro_view5   | NO
--- 42,47 ----
*************** SELECT table_name, is_insertable_into
*** 55,61 ****
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (21 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
--- 52,58 ----
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (20 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
*************** SELECT table_name, is_updatable, is_inse
*** 73,79 ****
   ro_view19  | NO           | NO
   ro_view2   | NO           | NO
   ro_view20  | NO           | NO
-  ro_view21  | NO           | NO
   ro_view3   | NO           | NO
   ro_view4   | NO           | NO
   ro_view5   | NO           | NO
--- 70,75 ----
*************** SELECT table_name, is_updatable, is_inse
*** 84,90 ****
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (21 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
--- 80,86 ----
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (20 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
*************** SELECT table_name, column_name, is_updat
*** 103,125 ****
   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_view21  | a             | NO
!  ro_view21  | b             | NO
!  ro_view21  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
--- 99,119 ----
   ro_view17  | a             | NO
   ro_view17  | b             | NO
   ro_view18  | a             | NO
!  ro_view19  | sequence_name | NO
!  ro_view19  | last_value    | NO
!  ro_view19  | start_value   | NO
!  ro_view19  | increment_by  | NO
!  ro_view19  | max_value     | NO
!  ro_view19  | min_value     | NO
!  ro_view19  | cache_value   | NO
!  ro_view19  | log_cnt       | NO
!  ro_view19  | is_cycled     | NO
!  ro_view19  | is_called     | NO
   ro_view2   | a             | NO
   ro_view2   | b             | NO
!  ro_view20  | a             | NO
!  ro_view20  | b             | NO
!  ro_view20  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
*************** SELECT table_name, column_name, is_updat
*** 140,146 ****
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (48 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
--- 134,140 ----
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (46 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
*************** INSERT INTO ro_view17 VALUES (3, 'ROW 3'
*** 268,291 ****
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! 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 enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view21 SET b=upper(b);
! ERROR:  cannot update view "ro_view21"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  DROP TABLE base_tbl CASCADE;
! NOTICE:  drop cascades to 17 other objects
  DETAIL:  drop cascades to view ro_view1
  drop cascades to view ro_view17
  drop cascades to view ro_view2
--- 262,281 ----
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! DELETE FROM ro_view18;
! ERROR:  cannot delete from view "ro_view18"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! UPDATE ro_view19 SET max_value=1000;
! ERROR:  cannot update view "ro_view19"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view20 SET b=upper(b);
! ERROR:  cannot update view "ro_view20"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  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_view11
*** 299,311 ****
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view18
! drop cascades to view ro_view21
  drop cascades to view ro_view4
  drop cascades to view rw_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);
--- 289,300 ----
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view20
  drop cascades to view ro_view4
  drop cascades to view rw_view14
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
! NOTICE:  drop cascades to view ro_view19
  -- 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);
*************** INSERT INTO rw_view2 VALUES (2,3); -- ok
*** 1739,1742 ****
--- 1728,1923 ----
  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
+ -- security barrier view
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+                   QUERY PLAN                   
+ -----------------------------------------------
+  Subquery Scan on rw_view1
+    Filter: snoop(rw_view1.person)
+    ->  Seq Scan on base_tbl
+          Filter: (visibility = 'public'::text)
+ (4 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ -- security barrier view on top of security barrier view
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Subquery Scan on rw_view2
+    Filter: snoop(rw_view2.person)
+    ->  Subquery Scan on rw_view1
+          Filter: snoop(rw_view1.person)
+          ->  Seq Scan on base_tbl
+                Filter: (visibility = 'public'::text)
+ (6 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 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
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index a77cf19..6b352c1
*** a/src/test/regress/sql/updatable_views.sql
--- b/src/test/regress/sql/updatable_views.sql
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 25,36 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
--- 25,34 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
*************** SELECT * FROM base_tbl;
*** 87,99 ****
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  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;
! UPDATE ro_view21 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view19;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
--- 85,96 ----
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  INSERT INTO ro_view17 VALUES (3, 'ROW 3');
! DELETE FROM ro_view18;
! UPDATE ro_view19 SET max_value=1000;
! UPDATE ro_view20 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
*************** CREATE VIEW rw_view2 AS
*** 828,830 ****
--- 825,902 ----
    SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION;
  INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check)
  DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view
+ 
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ 
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ 
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ -- security barrier view on top of security barrier view
+ 
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ DROP TABLE base_tbl CASCADE;
#18Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dean Rasheed (#17)
Re: WIP patch (v2) for updatable security barrier views

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.

TBH, this sounds like doubling down on a wrong design choice. I see
no good reason that updatable security views should require any
fundamental rearrangements of the order of operations in the planner;
and I doubt that this is the last bug you'll have if you insist on
doing that.

regards, tom lane

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

#19Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Tom Lane (#18)
Re: WIP patch (v2) for updatable security barrier views

On 9 January 2014 15:19, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.

TBH, this sounds like doubling down on a wrong design choice.

Perhaps, but it's a design choice informed by all the problems that
arose from the previous attempts.

Right now I don't have any other ideas how to tackle this, so perhaps
continued testing to find where this falls down will inform a better
approach. If nothing else, we're collecting a useful set of test cases
that the final patch will need to pass.

Regards,
Dean

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

#20Craig Ringer
craig@2ndquadrant.com
In reply to: Dean Rasheed (#17)
Re: WIP patch (v2) for updatable security barrier views

On 01/09/2014 06:48 PM, Dean Rasheed wrote:

On 8 January 2014 10:19, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

The assertion failure with inheritance and sublinks is a separate
issue --- adjust_appendrel_attrs() is not expecting to find any
unplanned sublinks in the query tree when it is invoked, since they
would normally have all been planned by that point. However, the
addition of the new security barrier subqueries after inheritance
expansion can now insert new sublinks which need to be planned. I'll
look into how best to make that happen.

The attached patch does that, which fixes the case you reported.

Dean, any objections to adding this to the current CF, or to my doing so?

I want to adjust the RLS patch to build on top of this patch, splitting
the RLS patch up into a series that can be considered separately. To
have any hope of doing that, I'm going to need to be able to base it on
this patch.

Even if -hackers collectively decides the approach you've posted for
updatable s.b. views isn't the best way at some future point, we can
replace it with a better one then without upsetting users. RLS only
needs quite a high level interface over this, so it should adapt well to
anything that lets it wrap a table into a s.b. qualified subquery.

If there's no better approach forthcoming, then I think this proposal
should be committed. I'll do further testing to see if I can find
anything that breaks it, of course.

I've been bashing my head against this for weeks without great
inspiration - everything I try when doing this in the rewriter creates
three problems for every one it fixes. That's not to say it can't be
done, just that I haven't been able to do it while trying to learn the
basics of the rewriter, planner and executor at the same time ;-)

I've been consistently stuck on how to expand the tlist and inject ctid
(and oid, where required) Vars back *up* the chain of expanded
subqueries after views are expanded in rewrite. We only know the
required tlist and can only access the ctid and oid attrs once we expand
the inner-most view, but we've got no way to walk back up the tree of
subqueries (Query*) to inject Var tlis entries pointing to the
next-inner-most subquery. Need some way to walk back up the nested tree
of queries injecting this info. (I've had another idea about this that I
need to explore tonight, but every previous approach I've tried has
circled back to this same problem).

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#21Craig Ringer
craig@2ndquadrant.com
In reply to: Tom Lane (#18)
Re: WIP patch (v2) for updatable security barrier views

On 01/09/2014 11:19 PM, Tom Lane wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.

TBH, this sounds like doubling down on a wrong design choice. I see
no good reason that updatable security views should require any
fundamental rearrangements of the order of operations in the planner;
and I doubt that this is the last bug you'll have if you insist on
doing that.

I'd be quite happy to do this entirely within the rewriter. I've found
two persistent obstacles to that, and frankly I'm stuck. I'm going to be
reworking the RLS patches on top of Dean's functional patch unless I can
find some way to progress with a rewriter based approach.

The key problems are:

1. preprocess_targetlist in the planner assumes that the resultRelation
is the correct RTE to set as the varno in a new Var it adds to fetch the
row ctid (with label "ctid1") as a resjunk attr for row-marking. This
causes the tlist to have entries pointing to different RTE to the one
being scanned by the eventual seqscan / indexscan, though the underlying
Relation is the same. tlist validation checks don't like that.

There may be other places that need to add tlist entries pointing to the
relation we're reading rows from. They'll also need to be able to deal
with the fact that this no longer the resultRelation.

2. Despite bashing my head against it for ages, I haven't figured out
how to inject references to the base-rel's ctid, oid (if WITH OIDS), and
any tlist entries not specified in the DML statement into the subquery
tree. These are only accessible at the deepest level of rewriting, when
the final view is expanded into a subquery and processed with
rewriteTargetListUD(..). At this point we don't have "breadcrumbs" to
use to walk back up the nested subqueries adding the required tlist entries.

I keep on exploring ideas for this one, and get stuck in a dead end for
every one.

Without a way to move on these, I don't have much hope of adding
updatable security barrier views support using work done in the rewriter.

It seems inevitable that we'll have to add the separate concepts of
"source relation" (tuples to feed into HeapModifyTable for ctid, and for
heap_modify_table after junkfiltering) and "result relation" (target
Relation of heap_modify_table to actually write tuples to, target of row
level locking operations).

There's also going to need to be some kind of breadcrumb chain to allow
us to walk from the inner-most expanded view's RTE_RELATION back up the
expanded view subquery tlists, adding next-inner-most refs to resjunk
"ctid" and (if needed) "oid", injecting defaults, and expanding the
target list with Vars to match non-referenced attributes of the
inner-most RTE_RELATION. So far I haven't come up with a sensible form
for that breadcrumb trail.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#22Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#20)
Re: WIP patch (v2) for updatable security barrier views

On 12 January 2014 10:12, Craig Ringer <craig@2ndquadrant.com> wrote:

On 01/09/2014 06:48 PM, Dean Rasheed wrote:

On 8 January 2014 10:19, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

The assertion failure with inheritance and sublinks is a separate
issue --- adjust_appendrel_attrs() is not expecting to find any
unplanned sublinks in the query tree when it is invoked, since they
would normally have all been planned by that point. However, the
addition of the new security barrier subqueries after inheritance
expansion can now insert new sublinks which need to be planned. I'll
look into how best to make that happen.

The attached patch does that, which fixes the case you reported.

Dean, any objections to adding this to the current CF, or to my doing so?

OK, I'll do that.

I've added a page to the wiki with a more in-depth description of how
the patch works, and the test cases I've tried so far:

https://wiki.postgresql.org/wiki/Making_security_barrier_views_automatically_updatable

there's obviously still a lot more testing to do, but the early signs
are encouraging.

Regards,
Dean

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

#23Craig Ringer
craig@2ndquadrant.com
In reply to: Tom Lane (#18)
Re: WIP patch (v2) for updatable security barrier views

On 01/09/2014 11:19 PM, Tom Lane wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.

TBH, this sounds like doubling down on a wrong design choice. I see
no good reason that updatable security views should require any
fundamental rearrangements of the order of operations in the planner.

In that case, would you mind offerign a quick sanity check on the
following alternative idea:

- Add "sourceRelation" to Query. This refers to the RTE that supplies
tuple projections to feed into ExecModifyTable, with appropriate resjunk
"ctid" and (if requ'd) "oid" cols present.

- When expanding a target view into a subquery, set "sourceRelation" on
the outer view to the index of the RTE of the newly expanded subquery.

- In rewriteTargetView, as now, reassign resultRelation to the target
view's base rel. This is required so that do any RETURNING and WITH
CHECK OPTION fixups required to adjust the RETURNING list to the new
result relation, so they act on the final tuple after any BEFORE
triggers act. Do not flatten the view subquery and merge the quals (as
currently happens); allow it to be expanded as a subquery by the
rewriter instead. Don't mess with the view tlist at this point except by
removing the whole-row Var added by rewriteTargetListUD.

- When doing tlist expansion in preprocess_targetlist, when we process
the outer Query (the only one for which query type is not SELECT, and
the only one that has a non-zero resultRelation), if resultRelation !=
sourceRelation recursively follow the chain of sourceRelation s to the
bottom one with type RTE_RELATION. Do tlist expansion on that inner-most
Query first, using sourceRelation to supply the varno for injected TLEs,
including injecting "ctid", "oid" if req'd, etc. During call stack
unwind, have each intermediate layer do regular tlist expansion, adding
a Var pointing to each tlist entry of the inner subquery.

At the outer level of preprocess_targetlist, sort the tlist, now
expanded to include all required vars, into attribute order for the
resultRelation. (this level is the only one that has resultRelation set).

Avoid invoking preprocess_targetlist on the inner Query again when it's
processed in turn, or just bail out when we see sourceRelation set since
we know it's already been done.

(Alternately, it might be possible to run preprocess_targetlist
depth-first instead of the current outermost-first, but I haven't looked
at that).

The optimizer can still flatten non-security-barrier updatable views,
following the chain of Vars as it collapses each layer. That's
effectively what the current rewriteTargetView code is doing manually at
each pass right now.

I'm sure there are some holes in this outline, but it's struck me as
possibly workable. The key is to set sourceRelation on every inner
subquery in the target query chain, not just the outer one, so it can be
easily followed from the outer query though the subqueries into the
innermost query with RTE_RELATION type.

The only alternative I've looked at is looking clumsier the longer I
examine it: adding a back-reference in each subquery's Query struct, to
the Query containing it and the RTI of the subquery within the outer
Query. That way, once rewriting hits the innermost rel with RTE_RELATION
type, the rewriter can walk back up the Query tree doing tlist
rewriting. I'm not sure if this is workable yet, and it creates ugly
pointer-based backrefs *up* the Query chain, making what was previously
a tree of Query* into a graph. That could get exciting, though there'd
never be any need for mutators to follow the parent query pointer so it
wouldn't make tree rewrites harder.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#24KaiGai Kohei
kaigai@ak.jp.nec.com
In reply to: Craig Ringer (#23)
Re: WIP patch (v2) for updatable security barrier views

(2014/01/13 22:53), Craig Ringer wrote:

On 01/09/2014 11:19 PM, Tom Lane wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.

TBH, this sounds like doubling down on a wrong design choice. I see
no good reason that updatable security views should require any
fundamental rearrangements of the order of operations in the planner.

In that case, would you mind offerign a quick sanity check on the
following alternative idea:

- Add "sourceRelation" to Query. This refers to the RTE that supplies
tuple projections to feed into ExecModifyTable, with appropriate resjunk
"ctid" and (if requ'd) "oid" cols present.

- When expanding a target view into a subquery, set "sourceRelation" on
the outer view to the index of the RTE of the newly expanded subquery.

I have sane opinion. Existing assumption, "resultRelation" is RTE of the
table to be read not only modified, makes the implementation complicated.
If we would have a separate "sourceRelation", it allows to have flexible
form including sub-query with security_barrier on the reader side.

Could you tell me the direction you're inclined right now?
I wonder whether I should take the latest patch submitted on 09-Jan for
a deep code reviewing and testing.

Thanks,

- In rewriteTargetView, as now, reassign resultRelation to the target
view's base rel. This is required so that do any RETURNING and WITH
CHECK OPTION fixups required to adjust the RETURNING list to the new
result relation, so they act on the final tuple after any BEFORE
triggers act. Do not flatten the view subquery and merge the quals (as
currently happens); allow it to be expanded as a subquery by the
rewriter instead. Don't mess with the view tlist at this point except by
removing the whole-row Var added by rewriteTargetListUD.

- When doing tlist expansion in preprocess_targetlist, when we process
the outer Query (the only one for which query type is not SELECT, and
the only one that has a non-zero resultRelation), if resultRelation !=
sourceRelation recursively follow the chain of sourceRelation s to the
bottom one with type RTE_RELATION. Do tlist expansion on that inner-most
Query first, using sourceRelation to supply the varno for injected TLEs,
including injecting "ctid", "oid" if req'd, etc. During call stack
unwind, have each intermediate layer do regular tlist expansion, adding
a Var pointing to each tlist entry of the inner subquery.

At the outer level of preprocess_targetlist, sort the tlist, now
expanded to include all required vars, into attribute order for the
resultRelation. (this level is the only one that has resultRelation set).

Avoid invoking preprocess_targetlist on the inner Query again when it's
processed in turn, or just bail out when we see sourceRelation set since
we know it's already been done.

(Alternately, it might be possible to run preprocess_targetlist
depth-first instead of the current outermost-first, but I haven't looked
at that).

The optimizer can still flatten non-security-barrier updatable views,
following the chain of Vars as it collapses each layer. That's
effectively what the current rewriteTargetView code is doing manually at
each pass right now.

I'm sure there are some holes in this outline, but it's struck me as
possibly workable. The key is to set sourceRelation on every inner
subquery in the target query chain, not just the outer one, so it can be
easily followed from the outer query though the subqueries into the
innermost query with RTE_RELATION type.

The only alternative I've looked at is looking clumsier the longer I
examine it: adding a back-reference in each subquery's Query struct, to
the Query containing it and the RTI of the subquery within the outer
Query. That way, once rewriting hits the innermost rel with RTE_RELATION
type, the rewriter can walk back up the Query tree doing tlist
rewriting. I'm not sure if this is workable yet, and it creates ugly
pointer-based backrefs *up* the Query chain, making what was previously
a tree of Query* into a graph. That could get exciting, though there'd
never be any need for mutators to follow the parent query pointer so it
wouldn't make tree rewrites harder.

--
OSS Promotion Center / The PG-Strom Project
KaiGai Kohei <kaigai@ak.jp.nec.com>

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

#25Craig Ringer
craig@2ndquadrant.com
In reply to: KaiGai Kohei (#24)
Re: WIP patch (v2) for updatable security barrier views

On 01/21/2014 09:09 AM, KaiGai Kohei wrote:

(2014/01/13 22:53), Craig Ringer wrote:

On 01/09/2014 11:19 PM, Tom Lane wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.

TBH, this sounds like doubling down on a wrong design choice. I see
no good reason that updatable security views should require any
fundamental rearrangements of the order of operations in the planner.

In that case, would you mind offerign a quick sanity check on the
following alternative idea:

- Add "sourceRelation" to Query. This refers to the RTE that supplies
tuple projections to feed into ExecModifyTable, with appropriate resjunk
"ctid" and (if requ'd) "oid" cols present.

- When expanding a target view into a subquery, set "sourceRelation" on
the outer view to the index of the RTE of the newly expanded subquery.

I have sane opinion. Existing assumption, "resultRelation" is RTE of the
table to be read not only modified, makes the implementation complicated.
If we would have a separate "sourceRelation", it allows to have flexible
form including sub-query with security_barrier on the reader side.

Could you tell me the direction you're inclined right now?

My focus right now is getting your original RLS patch rebased on top of
head, separated into a series of patches that can be understood as
separate functional units, then rewritten on top of updatable security
barrier views.

( See
/messages/by-id/52DCBEF1.3010004@2ndquadrant.com )

I don't really care which updatable security barrier views
implementation it is. Dean's has the advantage of actually working, so
I'm basing it on his.

It sounds like Tom objects to Dean's approach to some degree, but we'll
have to see whether that turns into concrete issues. If it does, it
should be possible to replace the underlying updatable security barrier
views implementation without upsetting the rewritten RLS significantly.
That's part of why I've gone down this path - it separates "filtering
rows according to security predicates" from the rest of RLS, into a
largely functionally separate patch.

I wonder whether I should take the latest patch submitted on 09-Jan for
a deep code reviewing and testing.

That would be extremely valuable. Please break it, or show that you
could not figure out how to break it.

If you find an unfixable flaw, we'll go back to the approach outlined in
this mail - split resultRelation, insert ctids down the subquery chain, etc.

If you don't find a major flaw, then that's one less thing to worry
about, and we can focus on the RLS layer.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#26Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: KaiGai Kohei (#24)
Re: WIP patch (v2) for updatable security barrier views

On 21 January 2014 01:09, KaiGai Kohei <kaigai@ak.jp.nec.com> wrote:

(2014/01/13 22:53), Craig Ringer wrote:

On 01/09/2014 11:19 PM, Tom Lane wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.

TBH, this sounds like doubling down on a wrong design choice. I see
no good reason that updatable security views should require any
fundamental rearrangements of the order of operations in the planner.

In that case, would you mind offerign a quick sanity check on the
following alternative idea:

- Add "sourceRelation" to Query. This refers to the RTE that supplies
tuple projections to feed into ExecModifyTable, with appropriate resjunk
"ctid" and (if requ'd) "oid" cols present.

- When expanding a target view into a subquery, set "sourceRelation" on
the outer view to the index of the RTE of the newly expanded subquery.

I have sane opinion. Existing assumption, "resultRelation" is RTE of the
table to be read not only modified, makes the implementation complicated.
If we would have a separate "sourceRelation", it allows to have flexible
form including sub-query with security_barrier on the reader side.

Could you tell me the direction you're inclined right now?
I wonder whether I should take the latest patch submitted on 09-Jan for
a deep code reviewing and testing.

Yes, please review the patch from 09-Jan
(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg18ToTggP8zOBq6QnQHQ@mail.gmail.com).

The idea behind that patch is to avoid a lot of the complication that
leads to and then arises from having a separate "sourceRelation" in
the query.

If you go down the route of expanding the subquery in the rewriter and
making it the "sourceRelation", then you have to make extensive
changes to preprocess_targetlist so that it recursively descends into
the subquery to pull out variables needed by an update.

Also you would probably need additional changes in the rewriter so
that later stages didn't trample on the "sourceRelation", and
correctly updated it in the case of views on top of other views.

Also, you would need to make changes to the inheritance planner code,
because you'd now have 2 RTEs referring to the target relation
("resultRelation" and "sourceRelation" wrapping it in a subquery).
Referring to the comment in inheritance_planner():

* Source inheritance is expanded at the bottom of the
* plan tree (see allpaths.c), but target inheritance has to be expanded at
* the top.

except that in the case of the "sourceRelation", it is actually
effectively the target, which means it shouldn't be expanded,
otherwise you'd get plans like the ones I complained about upthread
(see the final explain output in
/messages/by-id/CAEZATCU3VcDKJpnHGFkRMrkz0axKCUH4CE_kQq3Z2HzkNhi5iA@mail.gmail.com).

Perhaps all of that could be made to work, but I think that it would
end up being a far more invasive patch than my 09-Jan patch. My patch
avoids both those issues by not doing the subquery expansion until
after inheritance expansion, and after targetlist preprocessing.

Regards,
Dean

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

#27KaiGai Kohei
kaigai@ak.jp.nec.com
In reply to: Dean Rasheed (#26)
Re: WIP patch (v2) for updatable security barrier views

(2014/01/21 18:18), Dean Rasheed wrote:

On 21 January 2014 01:09, KaiGai Kohei <kaigai@ak.jp.nec.com> wrote:

(2014/01/13 22:53), Craig Ringer wrote:

On 01/09/2014 11:19 PM, Tom Lane wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

My first thought was that it should just preprocess any security
barrier quals in subquery_planner() in the same way as other quals are
preprocessed. But thinking about it further, those quals are destined
to become the quals of subqueries in the range table, so we don't
actually want to preprocess them at that stage --- that will happen
later when the new subquery is planned by recursion back into
subquery_planner(). So I think the right answer is to make
adjust_appendrel_attrs() handle recursion into sublink subqueries.

TBH, this sounds like doubling down on a wrong design choice. I see
no good reason that updatable security views should require any
fundamental rearrangements of the order of operations in the planner.

In that case, would you mind offerign a quick sanity check on the
following alternative idea:

- Add "sourceRelation" to Query. This refers to the RTE that supplies
tuple projections to feed into ExecModifyTable, with appropriate resjunk
"ctid" and (if requ'd) "oid" cols present.

- When expanding a target view into a subquery, set "sourceRelation" on
the outer view to the index of the RTE of the newly expanded subquery.

I have sane opinion. Existing assumption, "resultRelation" is RTE of the
table to be read not only modified, makes the implementation complicated.
If we would have a separate "sourceRelation", it allows to have flexible
form including sub-query with security_barrier on the reader side.

Could you tell me the direction you're inclined right now?
I wonder whether I should take the latest patch submitted on 09-Jan for
a deep code reviewing and testing.

Yes, please review the patch from 09-Jan
(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg18ToTggP8zOBq6QnQHQ@mail.gmail.com).

The idea behind that patch is to avoid a lot of the complication that
leads to and then arises from having a separate "sourceRelation" in
the query.

If you go down the route of expanding the subquery in the rewriter and
making it the "sourceRelation", then you have to make extensive
changes to preprocess_targetlist so that it recursively descends into
the subquery to pull out variables needed by an update.

Also you would probably need additional changes in the rewriter so
that later stages didn't trample on the "sourceRelation", and
correctly updated it in the case of views on top of other views.

Also, you would need to make changes to the inheritance planner code,
because you'd now have 2 RTEs referring to the target relation
("resultRelation" and "sourceRelation" wrapping it in a subquery).
Referring to the comment in inheritance_planner():

* Source inheritance is expanded at the bottom of the
* plan tree (see allpaths.c), but target inheritance has to be expanded at
* the top.

except that in the case of the "sourceRelation", it is actually
effectively the target, which means it shouldn't be expanded,
otherwise you'd get plans like the ones I complained about upthread
(see the final explain output in
/messages/by-id/CAEZATCU3VcDKJpnHGFkRMrkz0axKCUH4CE_kQq3Z2HzkNhi5iA@mail.gmail.com).

Perhaps all of that could be made to work, but I think that it would
end up being a far more invasive patch than my 09-Jan patch. My patch
avoids both those issues by not doing the subquery expansion until
after inheritance expansion, and after targetlist preprocessing.

Probably, I could get your point.

Even though the sub-query being replaced from a relation with
securityQuals is eventually planned according to the usual
manner, usual order as a part of regular sub-query planning,
however, adjust_appendrel_attrs() is called by inheritance_planner()
earlier than sub-query's planning.

Maybe, we originally had this problem but not appeared on the
surface, because child relations don't have qualifiers on this
phase. (It shall be distributed later.)
But now, this assumption was broken because of a relation with
securityQuals being replaced by a sub-query with SubLink.
So, I'm inclined to revise the assumption side, rather than
existing assertion checks.

Shall we investigate what assumption should be revised if child-
relation, being expanded at expand_inherited_tables(), would be
a sub-query having Sub-Link?

A minor comment is below:

! /*
! * Make sure that the query is marked correctly if the added qual
! * has sublinks.
! */
! if (!parsetree->hasSubLinks)
! parsetree->hasSubLinks = checkExprHasSubLink(viewqual);

Is this logic really needed? This flag says the top-level query
contains SubLinks, however, the above condition checks whether
a sub-query to be constructed shall contain SubLinks.
Also, securityQuals is not attached to the parse tree right now.

Thanks,
--
OSS Promotion Center / The PG-Strom Project
KaiGai Kohei <kaigai@ak.jp.nec.com>

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

#28Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Dean Rasheed (#26)
1 attachment(s)
Re: WIP patch (v2) for updatable security barrier views

On 21 January 2014 09:18, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Yes, please review the patch from 09-Jan
(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg18ToTggP8zOBq6QnQHQ@mail.gmail.com).

After further testing I found a bug --- it involves having a security
barrier view on top of a base relation that has a rule that rewrites
the query to have a different result relation, and possibly also a
different command type, so that the securityQuals are no longer on the
result relation, which is a code path not previously tested and the
rowmark handling was wrong. That's probably a pretty obscure case in
the context of security barrier views, but that code path would be
used much more commonly if RLS were built on top of this. Fortunately
the fix is trivial --- updated patch attached.

Regards,
Dean

Attachments:

updatable-sb-views.patchtext/x-diff; charset=US-ASCII; name=updatable-sb-views.patchDownload
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
new file mode 100644
index e0fbe1e..888410f
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
*************** CREATE VIEW vista AS SELECT text 'Hello
*** 323,334 ****
         or set-returning functions.
        </para>
       </listitem>
- 
-      <listitem>
-       <para>
-        The view must not have the <literal>security_barrier</> property.
-       </para>
-      </listitem>
      </itemizedlist>
     </para>
  
--- 323,328 ----
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index 26a4613..6add0e3
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** ATExecSetRelOptions(Relation rel, List *
*** 8811,8817 ****
  		List	   *view_options = untransformRelOptions(newOptions);
  		ListCell   *cell;
  		bool		check_option = false;
- 		bool		security_barrier = false;
  
  		foreach(cell, view_options)
  		{
--- 8811,8816 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8819,8826 ****
  
  			if (pg_strcasecmp(defel->defname, "check_option") == 0)
  				check_option = true;
- 			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 				security_barrier = defGetBoolean(defel);
  		}
  
  		/*
--- 8818,8823 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8830,8837 ****
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query,
! 											 security_barrier, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
--- 8827,8833 ----
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
new file mode 100644
index 1735762..bc08566
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
*************** DefineView(ViewStmt *stmt, const char *q
*** 396,402 ****
  	RangeVar   *view;
  	ListCell   *cell;
  	bool		check_option;
- 	bool		security_barrier;
  
  	/*
  	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
--- 396,401 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 451,457 ****
  	 * specified.
  	 */
  	check_option = false;
- 	security_barrier = false;
  
  	foreach(cell, stmt->options)
  	{
--- 450,455 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 459,466 ****
  
  		if (pg_strcasecmp(defel->defname, "check_option") == 0)
  			check_option = true;
- 		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 			security_barrier = defGetBoolean(defel);
  	}
  
  	/*
--- 457,462 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 470,476 ****
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, security_barrier, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
--- 466,472 ----
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index bb356d0..1e49a71
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 1998,2003 ****
--- 1998,2004 ----
  	COPY_SCALAR_FIELD(checkAsUser);
  	COPY_BITMAPSET_FIELD(selectedCols);
  	COPY_BITMAPSET_FIELD(modifiedCols);
+ 	COPY_NODE_FIELD(securityQuals);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 5908d9a..ab055d5
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2290,2295 ****
--- 2290,2296 ----
  	COMPARE_SCALAR_FIELD(checkAsUser);
  	COMPARE_BITMAPSET_FIELD(selectedCols);
  	COMPARE_BITMAPSET_FIELD(modifiedCols);
+ 	COMPARE_NODE_FIELD(securityQuals);
  
  	return true;
  }
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 123f2a6..1e48a7f
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** range_table_walker(List *rtable,
*** 2020,2025 ****
--- 2020,2028 ----
  					return true;
  				break;
  		}
+ 
+ 		if (walker(rte->securityQuals, context))
+ 			return true;
  	}
  	return false;
  }
*************** range_table_mutator(List *rtable,
*** 2755,2760 ****
--- 2758,2764 ----
  				MUTATE(newrte->values_lists, rte->values_lists, List *);
  				break;
  		}
+ 		MUTATE(newrte->securityQuals, rte->securityQuals, List *);
  		newrt = lappend(newrt, newrte);
  	}
  	return newrt;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 568c3b8..a0e3286
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outRangeTblEntry(StringInfo str, const
*** 2409,2414 ****
--- 2409,2415 ----
  	WRITE_OID_FIELD(checkAsUser);
  	WRITE_BITMAPSET_FIELD(selectedCols);
  	WRITE_BITMAPSET_FIELD(modifiedCols);
+ 	WRITE_NODE_FIELD(securityQuals);
  }
  
  static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index c22acd5..1e9baf5
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readRangeTblEntry(void)
*** 1252,1257 ****
--- 1252,1258 ----
  	READ_OID_FIELD(checkAsUser);
  	READ_BITMAPSET_FIELD(selectedCols);
  	READ_BITMAPSET_FIELD(modifiedCols);
+ 	READ_NODE_FIELD(securityQuals);
  
  	READ_DONE();
  }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 35bda67..a1498ba
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 916,921 ****
--- 916,927 ----
  		subplan = grouping_planner(&subroot, 0.0 /* retrieve all tuples */ );
  
  		/*
+ 		 * Planning may have modified the query result relation (if there
+ 		 * were security barrier quals on the result RTE).
+ 		 */
+ 		appinfo->child_relid = subroot.parse->resultRelation;
+ 
+ 		/*
  		 * If this child rel was excluded by constraint exclusion, exclude it
  		 * from the result plan.
  		 */
*************** inheritance_planner(PlannerInfo *root)
*** 932,940 ****
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 			final_rtable = list_concat(final_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
--- 938,969 ----
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 		{
! 			List	   *tmp_rtable = NIL;
! 			ListCell   *cell1, *cell2;
! 
! 			/*
! 			 * Planning this new child may have turned some of the original
! 			 * RTEs into subqueries (if they had security barrier quals). If
! 			 * so, we want to use these in the final rtable.
! 			 */
! 			forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
! 			{
! 				RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
! 				RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
! 
! 				if (rte1->rtekind == RTE_RELATION &&
! 					rte1->securityQuals != NIL &&
! 					rte2->rtekind == RTE_SUBQUERY)
! 					tmp_rtable = lappend(tmp_rtable, rte2);
! 				else
! 					tmp_rtable = lappend(tmp_rtable, rte1);
! 			}
! 
! 			final_rtable = list_concat(tmp_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
+ 		}
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
*************** grouping_planner(PlannerInfo *root, doub
*** 1163,1168 ****
--- 1192,1203 ----
  		tlist = preprocess_targetlist(root, tlist);
  
  		/*
+ 		 * Expand any rangetable entries that have security barrier quals.
+ 		 * This may add new security barrier subquery RTEs to the rangetable.
+ 		 */
+ 		expand_security_quals(root, tlist);
+ 
+ 		/*
  		 * Locate any window functions in the tlist.  (We don't need to look
  		 * anywhere else, since expressions used in ORDER BY will be in there
  		 * too.)  Note that they could all have been eliminated by constant
*************** preprocess_rowmarks(PlannerInfo *root)
*** 2150,2158 ****
  		 * Ignore RowMarkClauses for subqueries; they aren't real tables and
  		 * can't support true locking.  Subqueries that got flattened into the
  		 * main query should be ignored completely.  Any that didn't will get
! 		 * ROW_MARK_COPY items in the next loop.
  		 */
! 		if (rte->rtekind != RTE_RELATION)
  			continue;
  
  		/*
--- 2185,2195 ----
  		 * Ignore RowMarkClauses for subqueries; they aren't real tables and
  		 * can't support true locking.  Subqueries that got flattened into the
  		 * main query should be ignored completely.  Any that didn't will get
! 		 * ROW_MARK_COPY items in the next loop.  Relations with security
! 		 * barrier quals will be later expanded into subqueries, so we treat
! 		 * them as such here too.
  		 */
! 		if (rte->rtekind != RTE_RELATION || rte->securityQuals != NIL)
  			continue;
  
  		/*
*************** preprocess_rowmarks(PlannerInfo *root)
*** 2208,2214 ****
  		newrc->rowmarkId = ++(root->glob->lastRowMarkId);
  		/* real tables support REFERENCE, anything else needs COPY */
  		if (rte->rtekind == RTE_RELATION &&
! 			rte->relkind != RELKIND_FOREIGN_TABLE)
  			newrc->markType = ROW_MARK_REFERENCE;
  		else
  			newrc->markType = ROW_MARK_COPY;
--- 2245,2252 ----
  		newrc->rowmarkId = ++(root->glob->lastRowMarkId);
  		/* real tables support REFERENCE, anything else needs COPY */
  		if (rte->rtekind == RTE_RELATION &&
! 			rte->relkind != RELKIND_FOREIGN_TABLE &&
! 			rte->securityQuals == NIL)
  			newrc->markType = ROW_MARK_REFERENCE;
  		else
  			newrc->markType = ROW_MARK_COPY;
diff --git a/src/backend/optimizer/prep/Makefile b/src/backend/optimizer/prep/Makefile
new file mode 100644
index 86301bf..5195d9b
*** a/src/backend/optimizer/prep/Makefile
--- b/src/backend/optimizer/prep/Makefile
*************** subdir = src/backend/optimizer/prep
*** 12,17 ****
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
--- 12,17 ----
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o prepsecurity.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
new file mode 100644
index ...e7a3f56
*** a/src/backend/optimizer/prep/prepsecurity.c
--- b/src/backend/optimizer/prep/prepsecurity.c
***************
*** 0 ****
--- 1,423 ----
+ /*-------------------------------------------------------------------------
+  *
+  * prepsecurity.c
+  *	  Routines for preprocessing security barrier quals.
+  *
+  * Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
+  * Portions Copyright (c) 1994, Regents of the University of California
+  *
+  *
+  * IDENTIFICATION
+  *	  src/backend/optimizer/prep/prepsecurity.c
+  *
+  *-------------------------------------------------------------------------
+  */
+ #include "postgres.h"
+ 
+ #include "access/heapam.h"
+ #include "access/sysattr.h"
+ #include "catalog/heap.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/prep.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteManip.h"
+ #include "utils/rel.h"
+ 
+ 
+ typedef struct
+ {
+ 	int			rt_index;		/* Index of security barrier RTE */
+ 	int			sublevels_up;	/* Current nesting depth */
+ 	Relation	rel;			/* RTE relation at rt_index */
+ 	List	   *targetlist;		/* Targetlist for new subquery RTE */
+ 	List	   *colnames;		/* Column names in subquery RTE */
+ 	List	   *vars_processed;	/* List of Vars already processed */
+ } security_barrier_replace_vars_context;
+ 
+ static void expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual);
+ 
+ static void security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context);
+ 
+ static bool security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context);
+ 
+ 
+ /*
+  * expand_security_quals -
+  *	  expands any security barrier quals on RTEs in the query rtable, turning
+  *	  them into security barrier subqueries.
+  *
+  * Any given RTE may have multiple security barrier quals in a list, from which
+  * we create a set of nested subqueries to isolate each security barrier from
+  * the others, providing protection against malicious user-defined security
+  * barriers.  The first security barrier qual in the list will be used in the
+  * innermost subquery.
+  */
+ void
+ expand_security_quals(PlannerInfo *root, List *tlist)
+ {
+ 	Query	   *parse = root->parse;
+ 	int			rt_index;
+ 
+ 	/*
+ 	 * Process each RTE in the rtable list.
+ 	 *
+ 	 * Note that this is deliberately not a foreach loop, since the rtable may
+ 	 * be modified each time through the loop.
+ 	 */
+ 	rt_index = 0;
+ 	while (rt_index < list_length(parse->rtable))
+ 	{
+ 		RangeTblEntry *rte;
+ 
+ 		rt_index++;
+ 		rte = rt_fetch(rt_index, parse->rtable);
+ 
+ 		if (rte->securityQuals == NIL)
+ 			continue;
+ 
+ 		/*
+ 		 * Ignore any RTEs that aren't used in the query (such RTEs may be
+ 		 * present for permissions checks).
+ 		 */
+ 		if (rt_index != parse->resultRelation &&
+ 			!rangeTableEntry_used((Node *) parse, rt_index, 0))
+ 			continue;
+ 
+ 		/*
+ 		 * If this RTE is the target then we need to make a copy of it before
+ 		 * expanding it.  The unexpanded copy will become the new target, and
+ 		 * the original RTE will be expanded to become the source of rows to
+ 		 * update/delete.
+ 		 */
+ 		if (rt_index == parse->resultRelation)
+ 		{
+ 			RangeTblEntry *newrte = copyObject(rte);
+ 			parse->rtable = lappend(parse->rtable, newrte);
+ 			parse->resultRelation = list_length(parse->rtable);
+ 
+ 			/*
+ 			 * Wipe out any copied security barrier quals on the new target to
+ 			 * prevent infinite recursion.
+ 			 */
+ 			newrte->securityQuals = NIL;
+ 
+ 			/*
+ 			 * There's no need to do permissions checks twice, so wipe out the
+ 			 * permissions info for the original RTE (we prefer to keep the
+ 			 * bits set on the result RTE).
+ 			 */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * For the most part, Vars referencing the original relation should
+ 			 * remain as they are, meaning that they pull OLD values from the
+ 			 * expanded RTE.  But in the RETURNING list and in any WITH CHECK
+ 			 * OPTION quals, we want such Vars to represent NEW values, so
+ 			 * change them to reference the new RTE.
+ 			 */
+ 			ChangeVarNodes((Node *) parse->returningList, rt_index,
+ 						   parse->resultRelation, 0);
+ 
+ 			ChangeVarNodes((Node *) parse->withCheckOptions, rt_index,
+ 						   parse->resultRelation, 0);
+ 		}
+ 
+ 		/*
+ 		 * Process each security barrier qual in turn, starting with the
+ 		 * innermost one (the first in the list) and working outwards.
+ 		 *
+ 		 * We remove each qual from the list before processing it, so that its
+ 		 * variables aren't modified by expand_security_qual.  Also we don't
+ 		 * necessarily want the attributes referred to by the qual to be
+ 		 * exposed by the newly built subquery.
+ 		 */
+ 		while (rte->securityQuals != NIL)
+ 		{
+ 			Node   *qual = (Node *) linitial(rte->securityQuals);
+ 			rte->securityQuals = list_delete_first(rte->securityQuals);
+ 
+ 			ChangeVarNodes(qual, rt_index, 1, 0);
+ 			expand_security_qual(parse, tlist, rt_index, qual);
+ 		}
+ 	}
+ }
+ 
+ 
+ /*
+  * expand_security_qual -
+  *	  expand the specified security barrier qual on a query RTE, turning the
+  *	  RTE into a security barrier subquery.
+  */
+ static void
+ expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual)
+ {
+ 	RangeTblEntry  *rte;
+ 	Oid				relid;
+ 	Query		   *subquery;
+ 	RangeTblEntry  *subrte;
+ 	RangeTblRef	   *subrtr;
+ 	security_barrier_replace_vars_context context;
+ 	ListCell	   *cell;
+ 
+ 	rte = rt_fetch(rt_index, parse->rtable);
+ 	relid = rte->relid;
+ 
+ 	/*
+ 	 * There should only be 2 possible cases:
+ 	 *
+ 	 * 1. A relation RTE, which we turn into a subquery RTE containing all
+ 	 * referenced columns.
+ 	 *
+ 	 * 2. A subquery RTE (either from a prior call to this function or from an
+ 	 * expanded view).  In this case we build a new subquery on top of it to
+ 	 * isolate this security barrier qual from any other quals.
+ 	 */
+ 	switch (rte->rtekind)
+ 	{
+ 		case RTE_RELATION:
+ 			/*
+ 			 * Turn the relation RTE into a security barrier subquery RTE,
+ 			 * moving all permissions checks down into the subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 
+ 			subrte = copyObject(rte);
+ 			subrte->inFromCl = true;
+ 			subrte->securityQuals = NIL;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->rtekind = RTE_SUBQUERY;
+ 			rte->relid = InvalidOid;
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 			rte->inh = false;			/* must not be set for a subquery */
+ 
+ 			/* the permissions checks have now been moved down */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * Replace any variables in the outer query that refer to the
+ 			 * original relation RTE with references to columns that we will
+ 			 * expose in the new subquery, building the subquery's targetlist
+ 			 * as we go.
+ 			 */
+ 			context.rt_index = rt_index;
+ 			context.sublevels_up = 0;
+ 			context.rel = heap_open(relid, NoLock);
+ 			context.targetlist = NIL;
+ 			context.colnames = NIL;
+ 			context.vars_processed = NIL;
+ 
+ 			security_barrier_replace_vars((Node *) parse, &context);
+ 			security_barrier_replace_vars((Node *) tlist, &context);
+ 
+ 			heap_close(context.rel, NoLock);
+ 
+ 			/* Now we know what columns the subquery needs to expose */
+ 			rte->subquery->targetList = context.targetlist;
+ 			rte->eref = makeAlias(rte->eref->aliasname, context.colnames);
+ 
+ 			break;
+ 
+ 		case RTE_SUBQUERY:
+ 			/*
+ 			 * Build a new subquery that includes all the same columns as the
+ 			 * original subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 			subquery->targetList = NIL;
+ 
+ 			foreach(cell, rte->subquery->targetList)
+ 			{
+ 				TargetEntry	   *tle;
+ 				Var			   *var;
+ 
+ 				tle = (TargetEntry *) lfirst(cell);
+ 				var = makeVarFromTargetEntry(1, tle);
+ 
+ 				tle = makeTargetEntry((Expr *) var,
+ 									  list_length(subquery->targetList) + 1,
+ 									  pstrdup(tle->resname),
+ 									  tle->resjunk);
+ 				subquery->targetList = lappend(subquery->targetList, tle);
+ 			}
+ 
+ 			subrte = makeNode(RangeTblEntry);
+ 			subrte->rtekind = RTE_SUBQUERY;
+ 			subrte->subquery = rte->subquery;
+ 			subrte->security_barrier = rte->security_barrier;
+ 			subrte->eref = copyObject(rte->eref);
+ 			subrte->inFromCl = true;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 
+ 			break;
+ 
+ 		default:
+ 			elog(ERROR, "invalid range table entry for security barrier qual");
+ 	}
+ }
+ 
+ 
+ /*
+  * security_barrier_replace_vars -
+  *	  Apply security barrier variable replacement to an expression tree.
+  *
+  * This also builds/updates a targetlist with entries for each replacement
+  * variable that needs to be exposed by the security barrier subquery RTE.
+  *
+  * NOTE: although this has the form of a walker, we cheat and modify the
+  * nodes in-place.	The given expression tree should have been copied
+  * earlier to ensure that no unwanted side-effects occur!
+  */
+ static void
+ security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context)
+ {
+ 	/*
+ 	 * Must be prepared to start with a Query or a bare expression tree; if
+ 	 * it's a Query, go straight to query_tree_walker to make sure that
+ 	 * sublevels_up doesn't get incremented prematurely.
+ 	 */
+ 	if (node && IsA(node, Query))
+ 		query_tree_walker((Query *) node,
+ 						  security_barrier_replace_vars_walker,
+ 						  (void *) context, 0);
+ 	else
+ 		security_barrier_replace_vars_walker(node, context);
+ }
+ 
+ static bool
+ security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context)
+ {
+ 	if (node == NULL)
+ 		return false;
+ 	if (IsA(node, Var))
+ 	{
+ 		Var		   *var = (Var *) node;
+ 
+ 		/*
+ 		 * Note that the same Var may be present in different lists, so we
+ 		 * need to take care not to process it multiple times.
+ 		 */
+ 		if (var->varno == context->rt_index &&
+ 			var->varlevelsup == context->sublevels_up &&
+ 			!list_member_ptr(context->vars_processed, var))
+ 		{
+ 			/*
+ 			 * Found a matching variable. Make sure that it is in the subquery
+ 			 * targetlist and map its attno accordingly.
+ 			 */
+ 			AttrNumber	attno;
+ 			ListCell   *l;
+ 			TargetEntry *tle;
+ 			char	   *attname;
+ 			Var		   *newvar;
+ 
+ 			/* Search for the base attribute in the subquery targetlist */
+ 			attno = InvalidAttrNumber;
+ 			foreach(l, context->targetlist)
+ 			{
+ 				tle = (TargetEntry *) lfirst(l);
+ 				attno++;
+ 
+ 				Assert(IsA(tle->expr, Var));
+ 				if (((Var *) tle->expr)->varattno == var->varattno &&
+ 					((Var *) tle->expr)->varcollid == var->varcollid)
+ 				{
+ 					/* Map the variable onto this subquery targetlist entry */
+ 					var->varattno = attno;
+ 					return false;
+ 				}
+ 			}
+ 
+ 			/* Not in the subquery targetlist, so add it. Get its name. */
+ 			if (var->varattno < 0)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = SystemAttributeDefinition(var->varattno,
+ 													context->rel->rd_rel->relhasoids);
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else if (var->varattno == InvalidAttrNumber)
+ 			{
+ 				attname = "wholerow";
+ 			}
+ 			else if (var->varattno <= context->rel->rd_att->natts)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = context->rel->rd_att->attrs[var->varattno - 1];
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else
+ 			{
+ 				elog(ERROR, "invalid attribute number %d in security_barrier_replace_vars", var->varattno);
+ 			}
+ 
+ 			/* New variable for subquery targetlist */
+ 			newvar = copyObject(var);
+ 			newvar->varno = 1;
+ 
+ 			attno = list_length(context->targetlist) + 1;
+ 			tle = makeTargetEntry((Expr *) newvar,
+ 								  attno,
+ 								  pstrdup(attname),
+ 								  false);
+ 
+ 			context->targetlist = lappend(context->targetlist, tle);
+ 
+ 			context->colnames = lappend(context->colnames,
+ 										makeString(pstrdup(attname)));
+ 
+ 			/* Update the outer query's variable */
+ 			var->varattno = attno;
+ 
+ 			/* Remember this Var so that we don't process it again */
+ 			context->vars_processed = lappend(context->vars_processed, var);
+ 		}
+ 		return false;
+ 	}
+ 
+ 	if (IsA(node, Query))
+ 	{
+ 		/* Recurse into subselects */
+ 		bool		result;
+ 
+ 		context->sublevels_up++;
+ 		result = query_tree_walker((Query *) node,
+ 								   security_barrier_replace_vars_walker,
+ 								   (void *) context, 0);
+ 		context->sublevels_up--;
+ 		return result;
+ 	}
+ 	return expression_tree_walker(node, security_barrier_replace_vars_walker,
+ 								  (void *) context);
+ }
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
new file mode 100644
index 52dcc72..c7e0199
*** a/src/backend/optimizer/prep/prepunion.c
--- b/src/backend/optimizer/prep/prepunion.c
*************** typedef struct
*** 55,60 ****
--- 55,61 ----
  {
  	PlannerInfo *root;
  	AppendRelInfo *appinfo;
+ 	int			sublevels_up;
  } adjust_appendrel_attrs_context;
  
  static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
*************** translate_col_privs(const Bitmapset *par
*** 1580,1587 ****
   *	  child rel instead.  We also update rtindexes appearing outside Vars,
   *	  such as resultRelation and jointree relids.
   *
!  * Note: this is only applied after conversion of sublinks to subplans,
!  * so we don't need to cope with recursion into sub-queries.
   *
   * Note: this is not hugely different from what pullup_replace_vars() does;
   * maybe we should try to fold the two routines together.
--- 1581,1589 ----
   *	  child rel instead.  We also update rtindexes appearing outside Vars,
   *	  such as resultRelation and jointree relids.
   *
!  * Note: this is applied after conversion of sublinks to subplans in the
!  * query jointree, but there may still be sublinks in the security barrier
!  * quals of RTEs, so we do need to cope with recursion into sub-queries.
   *
   * Note: this is not hugely different from what pullup_replace_vars() does;
   * maybe we should try to fold the two routines together.
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1594,1602 ****
  
  	context.root = root;
  	context.appinfo = appinfo;
  
  	/*
! 	 * Must be prepared to start with a Query or a bare expression tree.
  	 */
  	if (node && IsA(node, Query))
  	{
--- 1596,1607 ----
  
  	context.root = root;
  	context.appinfo = appinfo;
+ 	context.sublevels_up = 0;
  
  	/*
! 	 * Must be prepared to start with a Query or a bare expression tree; if
! 	 * it's a Query, go straight to query_tree_walker to make sure that
! 	 * sublevels_up doesn't get incremented prematurely.
  	 */
  	if (node && IsA(node, Query))
  	{
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1635,1641 ****
  	{
  		Var		   *var = (Var *) copyObject(node);
  
! 		if (var->varlevelsup == 0 &&
  			var->varno == appinfo->parent_relid)
  		{
  			var->varno = appinfo->child_relid;
--- 1640,1646 ----
  	{
  		Var		   *var = (Var *) copyObject(node);
  
! 		if (var->varlevelsup == context->sublevels_up &&
  			var->varno == appinfo->parent_relid)
  		{
  			var->varno = appinfo->child_relid;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1652,1657 ****
--- 1657,1663 ----
  				if (newnode == NULL)
  					elog(ERROR, "attribute %d of relation \"%s\" does not exist",
  						 var->varattno, get_rel_name(appinfo->parent_reloid));
+ 				((Var *) newnode)->varlevelsup += context->sublevels_up;
  				return newnode;
  			}
  			else if (var->varattno == 0)
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1694,1703 ****
--- 1700,1715 ----
  					RowExpr    *rowexpr;
  					List	   *fields;
  					RangeTblEntry *rte;
+ 					ListCell   *lc;
  
  					rte = rt_fetch(appinfo->parent_relid,
  								   context->root->parse->rtable);
  					fields = (List *) copyObject(appinfo->translated_vars);
+ 					foreach(lc, fields)
+ 					{
+ 						Var		   *field = (Var *) lfirst(lc);
+ 						field->varlevelsup += context->sublevels_up;
+ 					}
  					rowexpr = makeNode(RowExpr);
  					rowexpr->args = fields;
  					rowexpr->row_typeid = var->vartype;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1716,1722 ****
  	{
  		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
  
! 		if (cexpr->cvarno == appinfo->parent_relid)
  			cexpr->cvarno = appinfo->child_relid;
  		return (Node *) cexpr;
  	}
--- 1728,1735 ----
  	{
  		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
  
! 		if (context->sublevels_up == 0 &&
! 			cexpr->cvarno == appinfo->parent_relid)
  			cexpr->cvarno = appinfo->child_relid;
  		return (Node *) cexpr;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1724,1730 ****
  	{
  		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
  
! 		if (rtr->rtindex == appinfo->parent_relid)
  			rtr->rtindex = appinfo->child_relid;
  		return (Node *) rtr;
  	}
--- 1737,1744 ----
  	{
  		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
  
! 		if (context->sublevels_up == 0 &&
! 			rtr->rtindex == appinfo->parent_relid)
  			rtr->rtindex = appinfo->child_relid;
  		return (Node *) rtr;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1737,1743 ****
  											  adjust_appendrel_attrs_mutator,
  												 (void *) context);
  		/* now fix JoinExpr's rtindex (probably never happens) */
! 		if (j->rtindex == appinfo->parent_relid)
  			j->rtindex = appinfo->child_relid;
  		return (Node *) j;
  	}
--- 1751,1758 ----
  											  adjust_appendrel_attrs_mutator,
  												 (void *) context);
  		/* now fix JoinExpr's rtindex (probably never happens) */
! 		if (context->sublevels_up == 0 &&
! 			j->rtindex == appinfo->parent_relid)
  			j->rtindex = appinfo->child_relid;
  		return (Node *) j;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1750,1756 ****
  											  adjust_appendrel_attrs_mutator,
  														 (void *) context);
  		/* now fix PlaceHolderVar's relid sets */
! 		if (phv->phlevelsup == 0)
  			phv->phrels = adjust_relid_set(phv->phrels,
  										   appinfo->parent_relid,
  										   appinfo->child_relid);
--- 1765,1771 ----
  											  adjust_appendrel_attrs_mutator,
  														 (void *) context);
  		/* now fix PlaceHolderVar's relid sets */
! 		if (phv->phlevelsup == context->sublevels_up)
  			phv->phrels = adjust_relid_set(phv->phrels,
  										   appinfo->parent_relid,
  										   appinfo->child_relid);
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1822,1833 ****
  		return (Node *) newinfo;
  	}
  
! 	/*
! 	 * NOTE: we do not need to recurse into sublinks, because they should
! 	 * already have been converted to subplans before we see them.
! 	 */
! 	Assert(!IsA(node, SubLink));
! 	Assert(!IsA(node, Query));
  
  	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
  								   (void *) context);
--- 1837,1862 ----
  		return (Node *) newinfo;
  	}
  
! 	if (IsA(node, Query))
! 	{
! 		/*
! 		 * Recurse into sublink subqueries. This should only be possible in
! 		 * security barrier quals of top-level RTEs. All other sublinks should
! 		 * have already been converted to subplans during expression
! 		 * preprocessing, but this doesn't happen for security barrier quals,
! 		 * since they are destined to become quals of a subquery RTE, which
! 		 * will be recursively planned, and so should not be preprocessed at
! 		 * this stage.
! 		 */
! 		Query	   *newnode;
! 
! 		context->sublevels_up++;
! 		newnode = query_tree_mutator((Query *) node,
! 									 adjust_appendrel_attrs_mutator,
! 									 (void *) context, 0);
! 		context->sublevels_up--;
! 		return (Node *) newnode;
! 	}
  
  	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
  								   (void *) context);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 0b13645..370b5aa
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** view_col_is_auto_updatable(RangeTblRef *
*** 1973,1980 ****
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
! 							 bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
--- 1973,1979 ----
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
*************** view_query_is_auto_updatable(Query *view
*** 2048,2061 ****
  		return gettext_noop("Views that return set-returning functions 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 the future.
- 	 */
- 	if (security_barrier)
- 		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.
  	 */
--- 2047,2052 ----
*************** relation_is_updatable(Oid reloid,
*** 2303,2311 ****
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery,
! 										 RelationIsSecurityView(rel),
! 										 false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
--- 2294,2300 ----
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery, false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
*************** rewriteTargetView(Query *parsetree, Rela
*** 2460,2466 ****
  
  	auto_update_detail =
  		view_query_is_auto_updatable(viewquery,
- 									 RelationIsSecurityView(view),
  									 parsetree->commandType != CMD_DELETE);
  
  	if (auto_update_detail)
--- 2449,2454 ----
*************** rewriteTargetView(Query *parsetree, Rela
*** 2664,2669 ****
--- 2652,2665 ----
  												   view_targetlist);
  
  	/*
+ 	 * Move any security barrier quals from the view RTE onto the new target
+ 	 * RTE.  Any such quals should now apply to the new target RTE and will not
+ 	 * reference the original view RTE in the rewritten query.
+ 	 */
+ 	new_rte->securityQuals = view_rte->securityQuals;
+ 	view_rte->securityQuals = NIL;
+ 
+ 	/*
  	 * 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.
*************** rewriteTargetView(Query *parsetree, Rela
*** 2743,2748 ****
--- 2739,2748 ----
  	 * only adjust their varnos to reference the new target (just the same as
  	 * we did with the view targetlist).
  	 *
+ 	 * Note that there is special-case handling for the quals of a security
+ 	 * barrier view, since they need to be kept separate from any user-supplied
+ 	 * quals, so these quals are kept on the new target RTE.
+ 	 *
  	 * For INSERT, the view's quals can be ignored in the main query.
  	 */
  	if (parsetree->commandType != CMD_INSERT &&
*************** rewriteTargetView(Query *parsetree, Rela
*** 2751,2757 ****
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 		AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
--- 2751,2775 ----
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 
! 		if (RelationIsSecurityView(view))
! 		{
! 			/*
! 			 * Note: the parsetree has been mutated, so the new_rte pointer is
! 			 * stale and needs to be re-computed.
! 			 */
! 			new_rte = rt_fetch(new_rt_index, parsetree->rtable);
! 			new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals);
! 
! 			/*
! 			 * Make sure that the query is marked correctly if the added qual
! 			 * has sublinks.
! 			 */
! 			if (!parsetree->hasSubLinks)
! 				parsetree->hasSubLinks = checkExprHasSubLink(viewqual);
! 		}
! 		else
! 			AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
*************** rewriteTargetView(Query *parsetree, Rela
*** 2813,2821 ****
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added to the
! 				 * query's WHERE clause, and AddQual will have already done
! 				 * this check.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
--- 2831,2838 ----
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added, and this
! 				 * check will already have been done.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index 846c31a..a197a86
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblEntry
*** 801,806 ****
--- 801,807 ----
  	Oid			checkAsUser;	/* if valid, check access as this role */
  	Bitmapset  *selectedCols;	/* columns needing SELECT permission */
  	Bitmapset  *modifiedCols;	/* columns needing INSERT/UPDATE permission */
+ 	List	   *securityQuals;	/* any security barrier quals to apply */
  } RangeTblEntry;
  
  /*
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
new file mode 100644
index 0f5a7d3..f5fc7e8
*** a/src/include/optimizer/prep.h
--- b/src/include/optimizer/prep.h
*************** extern Node *negate_clause(Node *node);
*** 36,41 ****
--- 36,46 ----
  extern Expr *canonicalize_qual(Expr *qual);
  
  /*
+  * prototypes for prepsecurity.c
+  */
+ extern void expand_security_quals(PlannerInfo *root, List *tlist);
+ 
+ /*
   * prototypes for preptlist.c
   */
  extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
new file mode 100644
index e4027bd..8da0af5
*** a/src/include/rewrite/rewriteHandler.h
--- b/src/include/rewrite/rewriteHandler.h
*************** extern void AcquireRewriteLocks(Query *p
*** 23,29 ****
  extern Node *build_column_default(Relation rel, int attrno);
  extern Query *get_view_query(Relation view);
  extern const char *view_query_is_auto_updatable(Query *viewquery,
- 										 bool security_barrier,
  										 bool check_cols);
  extern int	relation_is_updatable(Oid reloid,
  						  bool include_triggers,
--- 23,28 ----
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
new file mode 100644
index 91d1639..f6db582
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
*************** CREATE VIEW mysecview4 WITH (security_ba
*** 252,258 ****
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  security_barrier requires a Boolean value
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
--- 252,258 ----
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  invalid value for boolean option "security_barrier": 100
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 99c9165..261b0c5
*** a/src/test/regress/expected/updatable_views.out
--- b/src/test/regress/expected/updatable_views.out
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 22,33 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
--- 22,31 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
*************** SELECT table_name, is_insertable_into
*** 44,50 ****
   ro_view19  | NO
   ro_view2   | NO
   ro_view20  | NO
-  ro_view21  | NO
   ro_view3   | NO
   ro_view4   | NO
   ro_view5   | NO
--- 42,47 ----
*************** SELECT table_name, is_insertable_into
*** 55,61 ****
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (21 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
--- 52,58 ----
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (20 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
*************** SELECT table_name, is_updatable, is_inse
*** 73,79 ****
   ro_view19  | NO           | NO
   ro_view2   | NO           | NO
   ro_view20  | NO           | NO
-  ro_view21  | NO           | NO
   ro_view3   | NO           | NO
   ro_view4   | NO           | NO
   ro_view5   | NO           | NO
--- 70,75 ----
*************** SELECT table_name, is_updatable, is_inse
*** 84,90 ****
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (21 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
--- 80,86 ----
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (20 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
*************** SELECT table_name, column_name, is_updat
*** 103,125 ****
   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_view21  | a             | NO
!  ro_view21  | b             | NO
!  ro_view21  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
--- 99,119 ----
   ro_view17  | a             | NO
   ro_view17  | b             | NO
   ro_view18  | a             | NO
!  ro_view19  | sequence_name | NO
!  ro_view19  | last_value    | NO
!  ro_view19  | start_value   | NO
!  ro_view19  | increment_by  | NO
!  ro_view19  | max_value     | NO
!  ro_view19  | min_value     | NO
!  ro_view19  | cache_value   | NO
!  ro_view19  | log_cnt       | NO
!  ro_view19  | is_cycled     | NO
!  ro_view19  | is_called     | NO
   ro_view2   | a             | NO
   ro_view2   | b             | NO
!  ro_view20  | a             | NO
!  ro_view20  | b             | NO
!  ro_view20  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
*************** SELECT table_name, column_name, is_updat
*** 140,146 ****
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (48 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
--- 134,140 ----
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (46 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
*************** INSERT INTO ro_view17 VALUES (3, 'ROW 3'
*** 268,291 ****
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! 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 enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view21 SET b=upper(b);
! ERROR:  cannot update view "ro_view21"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  DROP TABLE base_tbl CASCADE;
! NOTICE:  drop cascades to 17 other objects
  DETAIL:  drop cascades to view ro_view1
  drop cascades to view ro_view17
  drop cascades to view ro_view2
--- 262,281 ----
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! DELETE FROM ro_view18;
! ERROR:  cannot delete from view "ro_view18"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! UPDATE ro_view19 SET max_value=1000;
! ERROR:  cannot update view "ro_view19"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view20 SET b=upper(b);
! ERROR:  cannot update view "ro_view20"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  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_view11
*** 299,311 ****
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view18
! drop cascades to view ro_view21
  drop cascades to view ro_view4
  drop cascades to view rw_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);
--- 289,300 ----
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view20
  drop cascades to view ro_view4
  drop cascades to view rw_view14
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
! NOTICE:  drop cascades to view ro_view19
  -- 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);
*************** DROP TABLE base_tbl CASCADE;
*** 1740,1742 ****
--- 1729,1987 ----
  NOTICE:  drop cascades to 2 other objects
  DETAIL:  drop cascades to view rw_view1
  drop cascades to view rw_view2
+ -- security barrier view
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+                   QUERY PLAN                   
+ -----------------------------------------------
+  Subquery Scan on rw_view1
+    Filter: snoop(rw_view1.person)
+    ->  Seq Scan on base_tbl
+          Filter: (visibility = 'public'::text)
+ (4 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ -- security barrier view on top of security barrier view
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Subquery Scan on rw_view2
+    Filter: snoop(rw_view2.person)
+    ->  Subquery Scan on rw_view1
+          Filter: snoop(rw_view1.person)
+          ->  Seq Scan on base_tbl
+                Filter: (visibility = 'public'::text)
+ (6 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 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
+ -- security barrier view on top of table with rules
+ CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean);
+ INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true);
+ CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl
+   WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id)
+   DO INSTEAD
+     UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id;
+ CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl
+   DO INSTEAD
+     UPDATE base_tbl SET deleted = true WHERE id = old.id;
+ CREATE VIEW rw_view1 WITH (security_barrier=true) AS
+   SELECT id, data FROM base_tbl WHERE NOT deleted;
+ SELECT * FROM rw_view1;
+  id | data  
+ ----+-------
+   1 | Row 1
+ (1 row)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+                                QUERY PLAN                                
+ -------------------------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Nested Loop
+          ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_1
+                Index Cond: (id = 1)
+          ->  Subquery Scan on base_tbl
+                Filter: snoop(base_tbl.data)
+                ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_2
+                      Index Cond: (id = 1)
+                      Filter: (NOT deleted)
+ (9 rows)
+ 
+ DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ NOTICE:  snooped value: Row 1
+ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Insert on base_tbl
+    InitPlan 1 (returns $0)
+      ->  Index Only Scan using base_tbl_pkey on base_tbl t
+            Index Cond: (id = 2)
+    ->  Result
+          One-Time Filter: ($0 IS NOT TRUE)
+  
+  Update on base_tbl
+    InitPlan 1 (returns $0)
+      ->  Index Only Scan using base_tbl_pkey on base_tbl t
+            Index Cond: (id = 2)
+    ->  Result
+          One-Time Filter: $0
+          ->  Index Scan using base_tbl_pkey on base_tbl
+                Index Cond: (id = 2)
+ (15 rows)
+ 
+ INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ SELECT * FROM base_tbl;
+  id |   data    | deleted 
+ ----+-----------+---------
+   1 | Row 1     | t
+   2 | New row 2 | f
+ (2 rows)
+ 
+ DROP TABLE base_tbl CASCADE;
+ NOTICE:  drop cascades to view rw_view1
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index a77cf19..08f7504
*** a/src/test/regress/sql/updatable_views.sql
--- b/src/test/regress/sql/updatable_views.sql
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 25,36 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
--- 25,34 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
*************** SELECT * FROM base_tbl;
*** 87,99 ****
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  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;
! UPDATE ro_view21 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view19;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
--- 85,96 ----
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  INSERT INTO ro_view17 VALUES (3, 'ROW 3');
! DELETE FROM ro_view18;
! UPDATE ro_view19 SET max_value=1000;
! UPDATE ro_view20 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
*************** CREATE VIEW rw_view2 AS
*** 828,830 ****
--- 825,931 ----
    SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION;
  INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check)
  DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view
+ 
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ 
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ 
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ -- security barrier view on top of security barrier view
+ 
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view on top of table with rules
+ 
+ CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean);
+ INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true);
+ 
+ CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl
+   WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id)
+   DO INSTEAD
+     UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id;
+ 
+ CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl
+   DO INSTEAD
+     UPDATE base_tbl SET deleted = true WHERE id = old.id;
+ 
+ CREATE VIEW rw_view1 WITH (security_barrier=true) AS
+   SELECT id, data FROM base_tbl WHERE NOT deleted;
+ 
+ SELECT * FROM rw_view1;
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ 
+ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ 
+ SELECT * FROM base_tbl;
+ 
+ DROP TABLE base_tbl CASCADE;
#29Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: KaiGai Kohei (#27)
Re: WIP patch (v2) for updatable security barrier views

On 23 January 2014 06:13, KaiGai Kohei <kaigai@ak.jp.nec.com> wrote:

A minor comment is below:

! /*
! * Make sure that the query is marked correctly if the added
qual
! * has sublinks.
! */
! if (!parsetree->hasSubLinks)
! parsetree->hasSubLinks = checkExprHasSubLink(viewqual);

Is this logic really needed? This flag says the top-level query
contains SubLinks, however, the above condition checks whether
a sub-query to be constructed shall contain SubLinks.
Also, securityQuals is not attached to the parse tree right now.

Thanks for looking at this.

Yes that logic is needed. Without it the top-level query wouldn't be
marked as having sublinks, which could cause all sorts of things to go
wrong --- for example, without it fireRIRrules() wouldn't walk the
query tree looking for SELECT rules in sublinks to expand, so a
security barrier qual with a sublink subquery that selected from
another view wouldn't work.

It is not true to say that "securityQuals is not attached to the parse
tree". The securityQuals *are* part of the parse tree, they just
happen to be held in a different place to keep them isolated from
other quals. So the remaining rewriter code that walks or mutates the
parse tree will process them just like any other quals, recursively
expanding any rules they contain.

Regards,
Dean

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

#30Kouhei Kaigai
kaigai@ak.jp.nec.com
In reply to: Dean Rasheed (#28)
Re: WIP patch (v2) for updatable security barrier views

Hello,

I checked the latest updatable security barrier view patch.
Even though I couldn't find a major design problem in this revision,
here are two minor comments below.

I think, it needs to be reviewed by committer to stick direction
to implement this feature. Of course, even I know Tom argued the
current design of this feature on the up-thread, it does not seem
to me Dean's design is not reasonable.

Below is minor comments of mine:

@@ -932,9 +938,32 @@ inheritance_planner(PlannerInfo *root)
        if (final_rtable == NIL)
            final_rtable = subroot.parse->rtable;
        else
-           final_rtable = list_concat(final_rtable,
+       {
+           List       *tmp_rtable = NIL;
+           ListCell   *cell1, *cell2;
+
+           /*
+            * Planning this new child may have turned some of the original
+            * RTEs into subqueries (if they had security barrier quals). If
+            * so, we want to use these in the final rtable.
+            */
+           forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
+           {
+               RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
+               RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
+
+               if (rte1->rtekind == RTE_RELATION &&
+                   rte1->securityQuals != NIL &&
+                   rte2->rtekind == RTE_SUBQUERY)
+                   tmp_rtable = lappend(tmp_rtable, rte2);
+               else
+                   tmp_rtable = lappend(tmp_rtable, rte1);
+           }

Do we have a case if rte1 is regular relation with securityQuals but
rte2 is not a sub-query? If so, rte2->rtekind == RTE_SUBQUERY should
be a condition in Assert, but the third condition in if-block.

In case when a sub-query is simple enough; no qualifier and no projection
towards underlying scan, is it pulled-up even if this sub-query has
security-barrier attribute, isn't it?
See the example below. The view v2 is defined as follows.

postgres=# CREATE VIEW v2 WITH (security_barrier) AS SELECT * FROM t2 WHERE x % 10 = 5;
CREATE VIEW
postgres=# EXPLAIN SELECT * FROM v2 WHERE f_leak(z);
QUERY PLAN
---------------------------------------------------------
Subquery Scan on v2 (cost=0.00..3.76 rows=1 width=41)
Filter: f_leak(v2.z)
-> Seq Scan on t2 (cost=0.00..3.50 rows=1 width=41)
Filter: ((x % 10) = 5)
(4 rows)

postgres=# EXPLAIN SELECT * FROM v2;
QUERY PLAN
---------------------------------------------------
Seq Scan on t2 (cost=0.00..3.50 rows=1 width=41)
Filter: ((x % 10) = 5)
(2 rows)

The second explain result shows the underlying sub-query is
pulled-up even though it has security-barrier attribute.
(IIRC, it was a new feature in v9.3.)

On the other hand, this kind of optimization was not applied
on a sub-query being extracted from a relation with securityQuals

postgres=# EXPLAIN UPDATE v2 SET z = z;
QUERY PLAN
--------------------------------------------------------------------
Update on t2 t2_1 (cost=0.00..3.51 rows=1 width=47)
-> Subquery Scan on t2 (cost=0.00..3.51 rows=1 width=47)
-> Seq Scan on t2 t2_2 (cost=0.00..3.50 rows=1 width=47)
Filter: ((x % 10) = 5)
(4 rows)

If it has no security_barrier option, the view reference is extracted
in the rewriter stage, it was pulled up as we expected.

postgres=# ALTER VIEW v2 RESET (security_barrier);
ALTER VIEW
postgres=# EXPLAIN UPDATE t2 SET z = z;
QUERY PLAN
-----------------------------------------------------------
Update on t2 (cost=0.00..3.00 rows=100 width=47)
-> Seq Scan on t2 (cost=0.00..3.00 rows=100 width=47)
(2 rows)

Probably, it misses something to be checked and applied when we extract
a sub-query in prepsecurity.c.
# BTW, which code does it pull up? pull_up_subqueries() is not.

Thanks,

-----Original Message-----
From: pgsql-hackers-owner@postgresql.org
[mailto:pgsql-hackers-owner@postgresql.org] On Behalf Of Dean Rasheed
Sent: Thursday, January 23, 2014 7:06 PM
To: Kaigai, Kouhei(海外, 浩平)
Cc: Craig Ringer; Tom Lane; PostgreSQL Hackers; Kohei KaiGai; Robert Haas;
Simon Riggs
Subject: Re: [HACKERS] WIP patch (v2) for updatable security barrier views

On 21 January 2014 09:18, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Yes, please review the patch from 09-Jan

(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg
18ToTggP8zOBq6QnQHQ@mail.gmail.com).

After further testing I found a bug --- it involves having a security barrier
view on top of a base relation that has a rule that rewrites the query to
have a different result relation, and possibly also a different command
type, so that the securityQuals are no longer on the result relation, which
is a code path not previously tested and the rowmark handling was wrong.
That's probably a pretty obscure case in the context of security barrier
views, but that code path would be used much more commonly if RLS were built
on top of this. Fortunately the fix is trivial --- updated patch attached.

Regards,
Dean

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

#31Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Kouhei Kaigai (#30)
Re: WIP patch (v2) for updatable security barrier views

On 27 January 2014 07:54, Kouhei Kaigai <kaigai@ak.jp.nec.com> wrote:

Hello,

I checked the latest updatable security barrier view patch.
Even though I couldn't find a major design problem in this revision,
here are two minor comments below.

I think, it needs to be reviewed by committer to stick direction
to implement this feature. Of course, even I know Tom argued the
current design of this feature on the up-thread, it does not seem
to me Dean's design is not reasonable.

Thanks for looking at this.

Below is minor comments of mine:

@@ -932,9 +938,32 @@ inheritance_planner(PlannerInfo *root)
if (final_rtable == NIL)
final_rtable = subroot.parse->rtable;
else
-           final_rtable = list_concat(final_rtable,
+       {
+           List       *tmp_rtable = NIL;
+           ListCell   *cell1, *cell2;
+
+           /*
+            * Planning this new child may have turned some of the original
+            * RTEs into subqueries (if they had security barrier quals). If
+            * so, we want to use these in the final rtable.
+            */
+           forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
+           {
+               RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
+               RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
+
+               if (rte1->rtekind == RTE_RELATION &&
+                   rte1->securityQuals != NIL &&
+                   rte2->rtekind == RTE_SUBQUERY)
+                   tmp_rtable = lappend(tmp_rtable, rte2);
+               else
+                   tmp_rtable = lappend(tmp_rtable, rte1);
+           }

Do we have a case if rte1 is regular relation with securityQuals but
rte2 is not a sub-query? If so, rte2->rtekind == RTE_SUBQUERY should
be a condition in Assert, but the third condition in if-block.

Yes it is possible for rte1 to be a RTE_RELATION with securityQuals
and rte2 to be something other than a RTE_SUBQUERY because the
subquery expansion code in expand_security_quals() only expands RTEs
that are actually used in the query.

So for example, when planning the query to update an inheritance
child, the rtable will contain an RTE for the parent, but it will not
be referenced in the parse tree, and so it will not be expanded while
planning the child update.

In case when a sub-query is simple enough; no qualifier and no projection
towards underlying scan, is it pulled-up even if this sub-query has
security-barrier attribute, isn't it?
See the example below. The view v2 is defined as follows.

postgres=# CREATE VIEW v2 WITH (security_barrier) AS SELECT * FROM t2 WHERE x % 10 = 5;
CREATE VIEW
postgres=# EXPLAIN SELECT * FROM v2 WHERE f_leak(z);
QUERY PLAN
---------------------------------------------------------
Subquery Scan on v2 (cost=0.00..3.76 rows=1 width=41)
Filter: f_leak(v2.z)
-> Seq Scan on t2 (cost=0.00..3.50 rows=1 width=41)
Filter: ((x % 10) = 5)
(4 rows)

postgres=# EXPLAIN SELECT * FROM v2;
QUERY PLAN
---------------------------------------------------
Seq Scan on t2 (cost=0.00..3.50 rows=1 width=41)
Filter: ((x % 10) = 5)
(2 rows)

The second explain result shows the underlying sub-query is
pulled-up even though it has security-barrier attribute.
(IIRC, it was a new feature in v9.3.)

Actually what happens is that it is planned as a subquery scan, then
at the very end of the planning process, it detects that the subquery
scan is trivial (has no quals, and has a no-op targetlist) and it
removes that plan node --- see set_subqueryscan_references() and
trivial_subqueryscan().

That subquery scan removal code requires the targetlist to have
exactly the same number of attributes, in exactly the same order as
the underlying relation. As soon as you add anything non-trivial to
the select list in the above queries, or even just change the order of
its attributes, the subquery scan node is no longer removed.

On the other hand, this kind of optimization was not applied
on a sub-query being extracted from a relation with securityQuals

postgres=# EXPLAIN UPDATE v2 SET z = z;
QUERY PLAN
--------------------------------------------------------------------
Update on t2 t2_1 (cost=0.00..3.51 rows=1 width=47)
-> Subquery Scan on t2 (cost=0.00..3.51 rows=1 width=47)
-> Seq Scan on t2 t2_2 (cost=0.00..3.50 rows=1 width=47)
Filter: ((x % 10) = 5)
(4 rows)

If it has no security_barrier option, the view reference is extracted
in the rewriter stage, it was pulled up as we expected.

postgres=# ALTER VIEW v2 RESET (security_barrier);
ALTER VIEW
postgres=# EXPLAIN UPDATE t2 SET z = z;
QUERY PLAN
-----------------------------------------------------------
Update on t2 (cost=0.00..3.00 rows=100 width=47)
-> Seq Scan on t2 (cost=0.00..3.00 rows=100 width=47)
(2 rows)

Probably, it misses something to be checked and applied when we extract
a sub-query in prepsecurity.c.
# BTW, which code does it pull up? pull_up_subqueries() is not.

For UPDATE and DELETE the subquery scan node removal code will never
be able to do anything because the targetlist will not be a no-op (it
might just be possible to make it a no-op for an identity UPDATE, but
that wouldn't be of much practical use).

The main way in which it will attempt to optimise queries against
security barrier views is by pushing quals down into the subquery,
where it is safe to do so.

Regards,
Dean

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

#32Simon Riggs
simon@2ndQuadrant.com
In reply to: Dean Rasheed (#31)
Re: WIP patch (v2) for updatable security barrier views

On 27 January 2014 15:04, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

So for example, when planning the query to update an inheritance
child, the rtable will contain an RTE for the parent, but it will not
be referenced in the parse tree, and so it will not be expanded while
planning the child update.

Am I right in thinking that we have this fully working now?

If we commit this aspect soon, we stand a chance of also touching upon RLS.

AFAICS the only area of objection is the handling of inherited
relations, which occurs within the planner in the current patch. I can
see that would be a cause for concern since the planner is pluggable
and it would then be possible to bypass security checks. Obviously
installing a new planner isn't trivial, but doing so shouldn't cause
collateral damage.

We have long had restrictions around updateable views. My suggestion
from here is that we accept the restriction that we cannot yet have
the 3-way combination of updateable views, security views and views on
inherited tables.

Most people aren't using inherited tables and people that are have
special measures in place for their apps. We won't lose much by
accepting that restriction for 9.4 and re-addressing the issue in a
later release. We need not adopt an all or nothing approach. Perhaps
we might yet find a solution for 9.4, but again, that need not delay
the rest of the patch.

From a review perspective, I'd want to see some greatly expanded
README comments, but given the Wiki entry, I think we can do that
quickly. Other than that, the code seems clear, modular and well
tested, so is something I could see me committing the uncontentious
parts of.

Thoughts?

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#33Tom Lane
tgl@sss.pgh.pa.us
In reply to: Simon Riggs (#32)
Re: WIP patch (v2) for updatable security barrier views

Simon Riggs <simon@2ndQuadrant.com> writes:

Am I right in thinking that we have this fully working now?

I will look at this at some point during the CF, but have not yet,
and probably won't as long as it's not marked "ready for committer".

regards, tom lane

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

#34Simon Riggs
simon@2ndQuadrant.com
In reply to: Tom Lane (#33)
Re: WIP patch (v2) for updatable security barrier views

On 27 January 2014 16:11, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Simon Riggs <simon@2ndQuadrant.com> writes:

Am I right in thinking that we have this fully working now?

I will look at this at some point during the CF, but have not yet,
and probably won't as long as it's not marked "ready for committer".

I've marked it Ready for Committer, to indicate my personal opinion.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#35Craig Ringer
craig@2ndquadrant.com
In reply to: Simon Riggs (#32)
Re: WIP patch (v2) for updatable security barrier views

On 01/28/2014 12:09 AM, Simon Riggs wrote:

On 27 January 2014 15:04, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

So for example, when planning the query to update an inheritance
child, the rtable will contain an RTE for the parent, but it will not
be referenced in the parse tree, and so it will not be expanded while
planning the child update.

Am I right in thinking that we have this fully working now?

I haven't found any further problems, though I've been focusing more on
reworking RLS on top of it.

AFAICS the only area of objection is the handling of inherited
relations, which occurs within the planner in the current patch. I can
see that would be a cause for concern since the planner is pluggable
and it would then be possible to bypass security checks. Obviously
installing a new planner isn't trivial, but doing so shouldn't cause
collateral damage.

FWIW, I don't see any way _not_ to do that in RLS. If there are security
quals on a child table, they must be added, and that can only happen
once inheritance expansion happens. That's in the planner.

I don't see it as acceptable to ignore security quals on child tables,
and if we can't, we've got to do some work in the planner.

(I'm starting to really loathe inheritance).

We have long had restrictions around updateable views. My suggestion
from here is that we accept the restriction that we cannot yet have
the 3-way combination of updateable views, security views and views on
inherited tables.

That prevents the use of updatable security barrier views over
partitioned tables, and therefore prevents row-security use on inherited
tables.

That seems like a very big thing to close off. I'm perfectly happy
having that limitation for 9.4, we just need to make it possible to
remove the limitation later.

Most people aren't using inherited tables

Again, because we (ab)use them for paritioning, I'm not sure they're as
little-used as I'd like.

and people that are have
special measures in place for their apps. We won't lose much by
accepting that restriction for 9.4 and re-addressing the issue in a
later release.

Yep, I'd be happy with that.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#36Kouhei Kaigai
kaigai@ak.jp.nec.com
In reply to: Craig Ringer (#35)
Re: WIP patch (v2) for updatable security barrier views

AFAICS the only area of objection is the handling of inherited
relations, which occurs within the planner in the current patch. I can
see that would be a cause for concern since the planner is pluggable
and it would then be possible to bypass security checks. Obviously
installing a new planner isn't trivial, but doing so shouldn't cause
collateral damage.

FWIW, I don't see any way _not_ to do that in RLS. If there are security
quals on a child table, they must be added, and that can only happen once
inheritance expansion happens. That's in the planner.

I don't see it as acceptable to ignore security quals on child tables, and
if we can't, we've got to do some work in the planner.

(I'm starting to really loathe inheritance).

Let me ask an elemental question. What is the reason why inheritance
expansion logic should be located on the planner stage, not on the tail
of rewriter?
Reference to a relation with children is very similar to reference of
multiple tables using UNION ALL. Isn't it a crappy idea to move the
logic into rewriter stage (if we have no technical reason here)?

Thanks,
--
NEC OSS Promotion Center / PG-Strom Project
KaiGai Kohei <kaigai@ak.jp.nec.com>

-----Original Message-----
From: pgsql-hackers-owner@postgresql.org
[mailto:pgsql-hackers-owner@postgresql.org] On Behalf Of Craig Ringer
Sent: Tuesday, January 28, 2014 12:35 PM
To: Simon Riggs; Dean Rasheed
Cc: Kaigai, Kouhei(海外, 浩平); Tom Lane; PostgreSQL Hackers; Kohei KaiGai;
Robert Haas
Subject: Re: [HACKERS] WIP patch (v2) for updatable security barrier views

On 01/28/2014 12:09 AM, Simon Riggs wrote:

On 27 January 2014 15:04, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

So for example, when planning the query to update an inheritance
child, the rtable will contain an RTE for the parent, but it will not
be referenced in the parse tree, and so it will not be expanded while
planning the child update.

Am I right in thinking that we have this fully working now?

I haven't found any further problems, though I've been focusing more on
reworking RLS on top of it.

AFAICS the only area of objection is the handling of inherited
relations, which occurs within the planner in the current patch. I can
see that would be a cause for concern since the planner is pluggable
and it would then be possible to bypass security checks. Obviously
installing a new planner isn't trivial, but doing so shouldn't cause
collateral damage.

FWIW, I don't see any way _not_ to do that in RLS. If there are security
quals on a child table, they must be added, and that can only happen once
inheritance expansion happens. That's in the planner.

I don't see it as acceptable to ignore security quals on child tables, and
if we can't, we've got to do some work in the planner.

(I'm starting to really loathe inheritance).

We have long had restrictions around updateable views. My suggestion
from here is that we accept the restriction that we cannot yet have
the 3-way combination of updateable views, security views and views on
inherited tables.

That prevents the use of updatable security barrier views over partitioned
tables, and therefore prevents row-security use on inherited tables.

That seems like a very big thing to close off. I'm perfectly happy having
that limitation for 9.4, we just need to make it possible to remove the
limitation later.

Most people aren't using inherited tables

Again, because we (ab)use them for paritioning, I'm not sure they're as
little-used as I'd like.

and people that are have
special measures in place for their apps. We won't lose much by
accepting that restriction for 9.4 and re-addressing the issue in a
later release.

Yep, I'd be happy with that.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

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

#37Simon Riggs
simon@2ndQuadrant.com
In reply to: Kouhei Kaigai (#36)
Re: WIP patch (v2) for updatable security barrier views

On 28 January 2014 04:10, Kouhei Kaigai <kaigai@ak.jp.nec.com> wrote:

AFAICS the only area of objection is the handling of inherited
relations, which occurs within the planner in the current patch. I can
see that would be a cause for concern since the planner is pluggable
and it would then be possible to bypass security checks. Obviously
installing a new planner isn't trivial, but doing so shouldn't cause
collateral damage.

FWIW, I don't see any way _not_ to do that in RLS. If there are security
quals on a child table, they must be added, and that can only happen once
inheritance expansion happens. That's in the planner.

I don't see it as acceptable to ignore security quals on child tables, and
if we can't, we've got to do some work in the planner.

(I'm starting to really loathe inheritance).

Let me ask an elemental question. What is the reason why inheritance
expansion logic should be located on the planner stage, not on the tail
of rewriter?
Reference to a relation with children is very similar to reference of
multiple tables using UNION ALL. Isn't it a crappy idea to move the
logic into rewriter stage (if we have no technical reason here)?

I agree that this is being seen the wrong way around. The planner
contains things it should not do, and as a result we are now
discussing enhancing the code that is in the wrong place, which of
course brings objections.

I think we would be best served by stopping inheritance in its tracks
and killing it off. It keeps getting in the way. What we need is real
partitioning. Other uses are pretty obscure and we should re-examine
things.

In the absence of that, releasing this updateable-security views
without suppport for inheritance is a good move. It gives us most of
what we want now and continuing to have some form of restriction is
better than having a much greater restriction of it not working at
all.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#38Robert Haas
robertmhaas@gmail.com
In reply to: Simon Riggs (#37)
Re: WIP patch (v2) for updatable security barrier views

On Tue, Jan 28, 2014 at 5:02 AM, Simon Riggs <simon@2ndquadrant.com> wrote:

I agree that this is being seen the wrong way around. The planner
contains things it should not do, and as a result we are now
discussing enhancing the code that is in the wrong place, which of
course brings objections.

I think we would be best served by stopping inheritance in its tracks
and killing it off. It keeps getting in the way. What we need is real
partitioning. Other uses are pretty obscure and we should re-examine
things.

I actually think that inheritance is a pretty good foundation for real
partitioning. If we were to get rid of it, we'd likely end up needing
to add most of the same functionality back when we tried to do some
kind of real partitioning later, and that doesn't sound fun. I don't
see any reason why some kind of partitioning syntax couldn't be added
that leverages the existing inheritance mechanism but stores
additional metadata allowing for better optimization.

Well... I'm lying, a little bit. If our chosen implementation of
"real partitioning" involved some kind of partition objects that could
be created and dropped but never directly accessed via DML commands,
then we might not need anything that looks like the current planner
support for partitioned tables. But I think that would be a
surprising choice for this community. IMV, the problem with the
planner and inheritance isn't that there's too much cruft in there
already, but that there are still key optimizations that are missing.
Still, I'd rather try to fix that than start over.

In the absence of that, releasing this updateable-security views
without suppport for inheritance is a good move. It gives us most of
what we want now and continuing to have some form of restriction is
better than having a much greater restriction of it not working at
all.

-1. Inheritance may be a crappy substitute for real partitioning, but
there are plenty of people using it that way.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

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

#39Tom Lane
tgl@sss.pgh.pa.us
In reply to: Kouhei Kaigai (#36)
Re: WIP patch (v2) for updatable security barrier views

Kouhei Kaigai <kaigai@ak.jp.nec.com> writes:

Let me ask an elemental question. What is the reason why inheritance
expansion logic should be located on the planner stage, not on the tail
of rewriter?

I think it's mostly historical. You would however have to think of a
way to preserve the inheritance relationships in the parsetree
representation. In the current code, expand_inherited_tables() adds
AppendRelInfo nodes to the planner's data structures as it does the
expansion; but I don't think AppendRelInfo is a suitable structure
for the rewriter to work with, and in any case there's no place to
put it in the Query representation.

Actually though, isn't this issue mostly about inheritance of a query
*target* table? Moving that expansion to the rewriter is a totally
different and perhaps more tractable change. It's certainly horribly ugly
as it is; hard to see how doing it at the rewriter could be worse.

regards, tom lane

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

#40Kouhei Kaigai
kaigai@ak.jp.nec.com
In reply to: Tom Lane (#39)
Re: WIP patch (v2) for updatable security barrier views

Kouhei Kaigai <kaigai@ak.jp.nec.com> writes:

Let me ask an elemental question. What is the reason why inheritance
expansion logic should be located on the planner stage, not on the
tail of rewriter?

I think it's mostly historical. You would however have to think of a way
to preserve the inheritance relationships in the parsetree representation.
In the current code, expand_inherited_tables() adds AppendRelInfo nodes
to the planner's data structures as it does the expansion; but I don't think
AppendRelInfo is a suitable structure for the rewriter to work with, and
in any case there's no place to put it in the Query representation.

Actually though, isn't this issue mostly about inheritance of a query
*target* table? Moving that expansion to the rewriter is a totally
different and perhaps more tractable change. It's certainly horribly ugly
as it is; hard to see how doing it at the rewriter could be worse.

It's just an idea, so might not be a deep consideration.

Isn't ii available to describe a parse tree as if some UPDATE/DELETE statements
are combined with UNION ALL? Of course, even if it is only internal form.

UPDATE parent SET x = 2*x, y = y || '_update' WHERE x % 10 = 5
UNION ALL
UPDATE child_1 SET x = 2*x, y = y || '_update' WHERE x % 10 = 5
:

Right now, only SELECT statement is allowed being placed under set-operations.
If rewriter can expand inherited relations into multiple individual selects
with UNION ALL, it may be a reasonable internal representation.

In similar way, both of UPDATE/DELETE takes a scan on relation once, then
it modifies the target relation. Probably, here is no significant different
on the earlier half; that performs as if multiple SELECTs with UNION ALL are
running, except for it fetches ctid system column as junk attribute and
acquires row-level locks.

On the other hand, it may need to adjust planner code to construct
ModifyTable node running on multiple relations. Existing code pulls
resultRelations from AppendRelInfo, doesn't it? It needs to take the list
of result relations using recursion of set operations, if not flatten.
Once we can construct ModifyTable with multiple relations on behalf of
multiple relation's scan, here is no difference from what we're doing now.

How about your opinion?

Thanks,
--
NEC OSS Promotion Center / PG-Strom Project
KaiGai Kohei <kaigai@ak.jp.nec.com>

-----Original Message-----
From: Tom Lane [mailto:tgl@sss.pgh.pa.us]
Sent: Wednesday, January 29, 2014 3:43 PM
To: Kaigai, Kouhei(海外, 浩平)
Cc: Craig Ringer; Simon Riggs; Dean Rasheed; PostgreSQL Hackers; Kohei
KaiGai; Robert Haas
Subject: Re: [HACKERS] WIP patch (v2) for updatable security barrier views

Kouhei Kaigai <kaigai@ak.jp.nec.com> writes:

Let me ask an elemental question. What is the reason why inheritance
expansion logic should be located on the planner stage, not on the
tail of rewriter?

I think it's mostly historical. You would however have to think of a way
to preserve the inheritance relationships in the parsetree representation.
In the current code, expand_inherited_tables() adds AppendRelInfo nodes
to the planner's data structures as it does the expansion; but I don't think
AppendRelInfo is a suitable structure for the rewriter to work with, and
in any case there's no place to put it in the Query representation.

Actually though, isn't this issue mostly about inheritance of a query
*target* table? Moving that expansion to the rewriter is a totally
different and perhaps more tractable change. It's certainly horribly ugly
as it is; hard to see how doing it at the rewriter could be worse.

regards, tom lane

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

#41Craig Ringer
craig@2ndquadrant.com
In reply to: Kouhei Kaigai (#40)
Re: WIP patch (v2) for updatable security barrier views

On 01/29/2014 03:34 PM, Kouhei Kaigai wrote:

Kouhei Kaigai <kaigai@ak.jp.nec.com> writes:

Let me ask an elemental question. What is the reason why inheritance
expansion logic should be located on the planner stage, not on the
tail of rewriter?

I think it's mostly historical. You would however have to think of a way
to preserve the inheritance relationships in the parsetree representation.
In the current code, expand_inherited_tables() adds AppendRelInfo nodes
to the planner's data structures as it does the expansion; but I don't think
AppendRelInfo is a suitable structure for the rewriter to work with, and
in any case there's no place to put it in the Query representation.

Actually though, isn't this issue mostly about inheritance of a query
*target* table? Moving that expansion to the rewriter is a totally
different and perhaps more tractable change. It's certainly horribly ugly
as it is; hard to see how doing it at the rewriter could be worse.

It's just an idea, so might not be a deep consideration.

Isn't ii available to describe a parse tree as if some UPDATE/DELETE statements
are combined with UNION ALL? Of course, even if it is only internal form.

UPDATE parent SET x = 2*x, y = y || '_update' WHERE x % 10 = 5
UNION ALL
UPDATE child_1 SET x = 2*x, y = y || '_update' WHERE x % 10 = 5
:

Right now, only SELECT statement is allowed being placed under set-operations.
If rewriter can expand inherited relations into multiple individual selects
with UNION ALL, it may be a reasonable internal representation.

In similar way, both of UPDATE/DELETE takes a scan on relation once, then
it modifies the target relation. Probably, here is no significant different
on the earlier half; that performs as if multiple SELECTs with UNION ALL are
running, except for it fetches ctid system column as junk attribute and
acquires row-level locks.

On the other hand, it may need to adjust planner code to construct
ModifyTable node running on multiple relations. Existing code pulls
resultRelations from AppendRelInfo, doesn't it? It needs to take the list
of result relations using recursion of set operations, if not flatten.
Once we can construct ModifyTable with multiple relations on behalf of
multiple relation's scan, here is no difference from what we're doing now.

How about your opinion?

My worry here is that a fair bit of work gets done before inheritance
expansion. Partitioning already performs pretty poorly for anything but
small numbers of partitions.

If we're expanding inheritance in the rewriter, won't that further
increase the already large amount of duplicate work involved in planning
a query that involves inheritance?

Or am I misunderstanding you?

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#42Simon Riggs
simon@2ndQuadrant.com
In reply to: Robert Haas (#38)
Re: WIP patch (v2) for updatable security barrier views

On 28 January 2014 21:28, Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Jan 28, 2014 at 5:02 AM, Simon Riggs <simon@2ndquadrant.com> wrote:

I agree that this is being seen the wrong way around. The planner
contains things it should not do, and as a result we are now
discussing enhancing the code that is in the wrong place, which of
course brings objections.

I think we would be best served by stopping inheritance in its tracks
and killing it off. It keeps getting in the way. What we need is real
partitioning. Other uses are pretty obscure and we should re-examine
things.

I actually think that inheritance is a pretty good foundation for real
partitioning. If we were to get rid of it, we'd likely end up needing
to add most of the same functionality back when we tried to do some
kind of real partitioning later, and that doesn't sound fun. I don't
see any reason why some kind of partitioning syntax couldn't be added
that leverages the existing inheritance mechanism but stores
additional metadata allowing for better optimization.

Well... I'm lying, a little bit. If our chosen implementation of
"real partitioning" involved some kind of partition objects that could
be created and dropped but never directly accessed via DML commands,
then we might not need anything that looks like the current planner
support for partitioned tables. But I think that would be a
surprising choice for this community. IMV, the problem with the
planner and inheritance isn't that there's too much cruft in there
already, but that there are still key optimizations that are missing.
Still, I'd rather try to fix that than start over.

In the absence of that, releasing this updateable-security views
without suppport for inheritance is a good move. It gives us most of
what we want now and continuing to have some form of restriction is
better than having a much greater restriction of it not working at
all.

-1. Inheritance may be a crappy substitute for real partitioning, but
there are plenty of people using it that way.

When I talk about removing inheritance, of course I see some need for
partitioning.

Given the way generalised inheritance works, it is possible to come up
with some monstrous designs that are then hard to rewrite and plan.

What I propose is that we remove the user-visible generalised
inheritance feature and only allow a more structured version which we
call partitioning. If all target/result relations are identical it
will be much easier to handle things because there'll be no special
purpose code to juggle.

Yes, key optimizations are missing. Overburdening ourselves with
complications that slow us down from delivering more useful features
is sensible. Think of it as feature-level refactoring, rather than the
normal code-level refactoring we frequently discuss.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#43Simon Riggs
simon@2ndQuadrant.com
In reply to: Tom Lane (#39)
Re: WIP patch (v2) for updatable security barrier views

On 29 January 2014 06:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Actually though, isn't this issue mostly about inheritance of a query
*target* table?

Exactly. IMHO updateable views on inheritance sets will have multiple
other performance problems, so trying to support them here will not
make their usage seamless.

We allowed updateable views to work with restrictions in earlier
releases, so I can't see why continuing with a much reduced
restriction would be a problem in this release. We don't need to
remove the remaining restriction all in one release, so we?

Moving that expansion to the rewriter is a totally
different and perhaps more tractable change. It's certainly horribly ugly
as it is; hard to see how doing it at the rewriter could be worse.

I see the patch adding some changes to inheritance_planner that might
well get moved to rewriter.
As long as the ugliness all stays in one place, does it matter where
that is -- for this patch -- ? Just asking whether moving it will
reduce the net ugliness of our inheritance support.

@Craig: I don't think this would have much effect on partitioning
performance. The main cost of that is constraint exclusion, which we
wuld still perform only once. All other copying and re-jigging still
gets performed even for excluded relations, so doing it earlier
wouldn't hurt as much as you might think.

@Dean: If we moved that to rewriter, would expand_security_quals() and
preprocess_rowmarks() also move?

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#44Craig Ringer
craig@2ndquadrant.com
In reply to: Dean Rasheed (#28)
Re: WIP patch (v2) for updatable security barrier views

On 01/23/2014 06:06 PM, Dean Rasheed wrote:

On 21 January 2014 09:18, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Yes, please review the patch from 09-Jan
(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg18ToTggP8zOBq6QnQHQ@mail.gmail.com).

After further testing I found a bug --- it involves having a security
barrier view on top of a base relation that has a rule that rewrites
the query to have a different result relation, and possibly also a
different command type, so that the securityQuals are no longer on the
result relation, which is a code path not previously tested and the
rowmark handling was wrong. That's probably a pretty obscure case in
the context of security barrier views, but that code path would be
used much more commonly if RLS were built on top of this. Fortunately
the fix is trivial --- updated patch attached.

This is the most recent patch I see, and the one I've been working on
top of.

Are there any known tests that this patch fails?

Can we construct any tests that this patch fails? If so, can we make it
pass them, or error out cleanly?

The discussion has gone a bit off the wall a bit - partly my fault I
think - I mentioned inheritance. Lets try to refocus on the immediate
patch at hand, and whether it's good to go.

Right now, I'm not personally aware of tests cases that cause this code
to fail.

There's a good-taste complaint about handling of inheritance, but
frankly, there's not much about inheritance that _is_ good taste. I
don't see that this patch makes it worse, and it's functional.

It might be interesting to revisit some of these broader questions in
9.5, but what can we do to get this functionality in place for 9.4?

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#45Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Simon Riggs (#43)
Re: WIP patch (v2) for updatable security barrier views

On 29 January 2014 11:27, Simon Riggs <simon@2ndquadrant.com> wrote:

On 29 January 2014 06:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Actually though, isn't this issue mostly about inheritance of a query
*target* table?

Exactly. IMHO updateable views on inheritance sets will have multiple
other performance problems, so trying to support them here will not
make their usage seamless.

We allowed updateable views to work with restrictions in earlier
releases, so I can't see why continuing with a much reduced
restriction would be a problem in this release. We don't need to
remove the remaining restriction all in one release, so we?

Moving that expansion to the rewriter is a totally
different and perhaps more tractable change. It's certainly horribly ugly
as it is; hard to see how doing it at the rewriter could be worse.

I see the patch adding some changes to inheritance_planner that might
well get moved to rewriter.
As long as the ugliness all stays in one place, does it matter where
that is -- for this patch -- ? Just asking whether moving it will
reduce the net ugliness of our inheritance support.

@Craig: I don't think this would have much effect on partitioning
performance. The main cost of that is constraint exclusion, which we
wuld still perform only once. All other copying and re-jigging still
gets performed even for excluded relations, so doing it earlier
wouldn't hurt as much as you might think.

@Dean: If we moved that to rewriter, would expand_security_quals() and
preprocess_rowmarks() also move?

Actually I tend to think that expand_security_quals() should remain
where it is, regardless of where inheritance expansion happens. One of
the things that simplifies the job that expand_security_quals() has to
do is that it takes place after preprocess_targetlist(), so the
targetlist for the security barrier subqueries that it constructs is
known. Calling expand_security_quals() earlier would require
additional surgery on preprocess_targetlist() and expand_targetlist(),
and would probably also mean that the Query would need to record the
sourceRelation subquery RTE, as discussed upthread.

Regards,
Dean

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

#46Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#44)
Re: WIP patch (v2) for updatable security barrier views

On 29 January 2014 11:34, Craig Ringer <craig@2ndquadrant.com> wrote:

On 01/23/2014 06:06 PM, Dean Rasheed wrote:

On 21 January 2014 09:18, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Yes, please review the patch from 09-Jan
(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg18ToTggP8zOBq6QnQHQ@mail.gmail.com).

After further testing I found a bug --- it involves having a security
barrier view on top of a base relation that has a rule that rewrites
the query to have a different result relation, and possibly also a
different command type, so that the securityQuals are no longer on the
result relation, which is a code path not previously tested and the
rowmark handling was wrong. That's probably a pretty obscure case in
the context of security barrier views, but that code path would be
used much more commonly if RLS were built on top of this. Fortunately
the fix is trivial --- updated patch attached.

This is the most recent patch I see, and the one I've been working on
top of.

Are there any known tests that this patch fails?

None that I've been able to come up with.

Can we construct any tests that this patch fails? If so, can we make it
pass them, or error out cleanly?

Sounds sensible. Feel free to add any test cases you think up to the
wiki page. Even if we don't like this design, any alternative must at
least pass all the tests listed there.

https://wiki.postgresql.org/wiki/Making_security_barrier_views_automatically_updatable

Regards,
Dean

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

#47Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Tom Lane (#39)
Re: WIP patch (v2) for updatable security barrier views

On 29 January 2014 06:43, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Kouhei Kaigai <kaigai@ak.jp.nec.com> writes:

Let me ask an elemental question. What is the reason why inheritance
expansion logic should be located on the planner stage, not on the tail
of rewriter?

I think it's mostly historical. You would however have to think of a
way to preserve the inheritance relationships in the parsetree
representation. In the current code, expand_inherited_tables() adds
AppendRelInfo nodes to the planner's data structures as it does the
expansion; but I don't think AppendRelInfo is a suitable structure
for the rewriter to work with, and in any case there's no place to
put it in the Query representation.

Actually though, isn't this issue mostly about inheritance of a query
*target* table? Moving that expansion to the rewriter is a totally
different and perhaps more tractable change. It's certainly horribly ugly
as it is; hard to see how doing it at the rewriter could be worse.

That's interesting. Presumably then we could make rules work properly
on inheritance children. I'm not sure if anyone has actually
complained that that currently doesn't work.

Thinking about that though, it does potentially open up a whole other
can of worms --- a single update query could be turned into multiple
other queries of different command types. Perhaps that's not so
different from what currently happens in the rewriter, except that
you'd need a way to track which of those queries counts towards the
statement's final row count. And how many ModifyTable nodes would the
resulting plan have?

Regards,
Dean

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

#48Simon Riggs
simon@2ndQuadrant.com
In reply to: Dean Rasheed (#45)
Re: WIP patch (v2) for updatable security barrier views

On 29 January 2014 12:20, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

@Dean: If we moved that to rewriter, would expand_security_quals() and
preprocess_rowmarks() also move?

Actually I tend to think that expand_security_quals() should remain
where it is, regardless of where inheritance expansion happens. One of
the things that simplifies the job that expand_security_quals() has to
do is that it takes place after preprocess_targetlist(), so the
targetlist for the security barrier subqueries that it constructs is
known. Calling expand_security_quals() earlier would require
additional surgery on preprocess_targetlist() and expand_targetlist(),
and would probably also mean that the Query would need to record the
sourceRelation subquery RTE, as discussed upthread.

Looks to me that preprocess_targetlist() could be done earlier also,
if necessary, since it appears to contain checks and additional
columns, not anything related to optimization.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#49Craig Ringer
craig@2ndquadrant.com
In reply to: Dean Rasheed (#46)
2 attachment(s)
Re: WIP patch (v2) for updatable security barrier views

On 01/29/2014 08:29 PM, Dean Rasheed wrote:

On 29 January 2014 11:34, Craig Ringer <craig@2ndquadrant.com> wrote:

On 01/23/2014 06:06 PM, Dean Rasheed wrote:

On 21 January 2014 09:18, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Yes, please review the patch from 09-Jan
(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg18ToTggP8zOBq6QnQHQ@mail.gmail.com).

After further testing I found a bug --- it involves having a security
barrier view on top of a base relation that has a rule that rewrites
the query to have a different result relation, and possibly also a
different command type, so that the securityQuals are no longer on the
result relation, which is a code path not previously tested and the
rowmark handling was wrong. That's probably a pretty obscure case in
the context of security barrier views, but that code path would be
used much more commonly if RLS were built on top of this. Fortunately
the fix is trivial --- updated patch attached.

This is the most recent patch I see, and the one I've been working on
top of.

Are there any known tests that this patch fails?

None that I've been able to come up with.

I've found an issue. I'm not sure if it can be triggered from SQL, but
it affects in-code users who add their own securityQuals.

expand_security_quals fails to update any rowMarks that may point to a
relation being expanded. If the relation isn't the target relation, this
causes a rowmark to refer to a RTE with no relid, and thus failure in
the executor.

Relative patch against updatable s.b. views attached (for easier
reading), along with a new revision of updatable s.b. views that
incorporates the patch.

This isn't triggered by FOR SHARE / FOR UPDATE rowmark on a security
barrier view because the flattening to securityQuals and re-expansion in
the optimizer is only done for _target_ security barrier views. For
target views, different rowmark handling applies, so they don't trigger
it either.

This is triggered by adding securityQuals to a non-target relation
during row-security processing, though.

Sounds sensible. Feel free to add any test cases you think up to the
wiki page. Even if we don't like this design, any alternative must at
least pass all the tests listed there.

Eh, much better to add directly to src/regress/ IMO. If they're on the
wiki they can get overlooked/forgotten.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

relative-upd-sb-views-fixup-rowmarks.difftext/x-patch; name=relative-upd-sb-views-fixup-rowmarks.diffDownload
>From 74b25d933ffb3b363cc584f0e85bf050a776cbb4 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Thu, 30 Jan 2014 12:15:40 +0800
Subject: [PATCH] (against upd.s.b. views) Updatable s.b. views code needs to
 adjust rowmarks

This oversight was not triggered when using security barrier views directly,
because only security barrier views that were DML targets were collapsed into
securityQuals, and special RowMark handling was already in place for these
during rewriting.
---
 src/backend/optimizer/prep/prepsecurity.c     | 60 +++++++++++++++++++++++----
 src/test/regress/expected/updatable_views.out |  4 +-
 2 files changed, 54 insertions(+), 10 deletions(-)

diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
index ad682bb..7c4ab0b 100644
--- a/src/backend/optimizer/prep/prepsecurity.c
+++ b/src/backend/optimizer/prep/prepsecurity.c
@@ -46,6 +46,9 @@ static void security_barrier_replace_vars(Node *node,
 static bool security_barrier_replace_vars_walker(Node *node,
 									 security_barrier_replace_vars_context *context);
 
+static void change_rowmarks(PlanRowMark *parent, int old_rt_index,
+							int new_rt_index, List *rowMarks);
+
 
 /*
  * expand_security_quals -
@@ -61,8 +64,9 @@ static bool security_barrier_replace_vars_walker(Node *node,
 void
 expand_security_quals(PlannerInfo *root, List *tlist)
 {
-	Query	   *parse = root->parse;
-	int			rt_index;
+	Query	       *parse = root->parse;
+	int				rt_index, new_rt_index;
+	PlanRowMark	   *rowmark;
 
 	/*
 	 * Process each RTE in the rtable list.
@@ -102,16 +106,17 @@ expand_security_quals(PlannerInfo *root, List *tlist)
 			continue;
 
 		/*
-		 * If this RTE is the target then we need to make a copy of it before
-		 * expanding it.  The unexpanded copy will become the new target, and
-		 * the original RTE will be expanded to become the source of rows to
-		 * update/delete.
+		 * If this RTE is the target relation or is the subject of any RowMarks
+		 * then we need to make a copy of it before expanding it.  The
+		 * unexpanded copy will become the new target, and the original RTE
+		 * will be expanded to become the source of rows to update/delete.
 		 */
-		if (rt_index == parse->resultRelation)
+		rowmark = get_plan_rowmark(root->rowMarks, rt_index);
+		if (rt_index == parse->resultRelation || rowmark != NULL )
 		{
 			RangeTblEntry *newrte = copyObject(rte);
 			parse->rtable = lappend(parse->rtable, newrte);
-			parse->resultRelation = list_length(parse->rtable);
+			parse->resultRelation = new_rt_index = list_length(parse->rtable);
 
 			/*
 			 * Wipe out any copied security barrier quals on the new target to
@@ -141,6 +146,14 @@ expand_security_quals(PlannerInfo *root, List *tlist)
 
 			ChangeVarNodes((Node *) parse->withCheckOptions, rt_index,
 						   parse->resultRelation, 0);
+
+			/*
+			 * The PlanRowMark for this RTI, if present, must also
+			 * point to the underlying RTE_RELATION we copied,
+			 * not to the subquery RTE.
+			 */
+			if (rowmark)
+				change_rowmarks(rowmark, rt_index, new_rt_index, root->rowMarks);
 		}
 
 		/*
@@ -163,6 +176,37 @@ expand_security_quals(PlannerInfo *root, List *tlist)
 	}
 }
 
+/*
+ * When the rangetable entry for a rowmark target is copied and replaced with a
+ * subquery, rowmarks referring to the replaced RTE must be rewritten to point to
+ * its new index.
+ *
+ * Takes the rowmark to adjust ("parent"), the old and new rtis for the RTE we
+ * just copied, and the list of all rowmarks in the plan.
+ */
+static void
+change_rowmarks(PlanRowMark *parent, int old_rt_index,
+				int new_rt_index, List *rowMarks)
+{
+	ListCell	   *lc;
+	PlanRowMark	   *rowmark;
+
+	/* Adjust rti and (if self) prti to point to the copied RTE. */
+	parent->rti = new_rt_index;
+	if (parent->prti == old_rt_index)
+		parent->prti = new_rt_index;
+	/* If this is a parent RowMark, fix up prti on its children. */
+	if (parent->isParent) {
+		foreach (lc, rowMarks)
+		{
+			rowmark = (PlanRowMark*) lfirst(lc);
+			if (rowmark->prti == old_rt_index)
+				rowmark->prti = new_rt_index;
+		}
+	}
+}
+
+
 
 /*
  * expand_security_qual -
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 261b0c5..9a34819 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -1942,13 +1942,13 @@ SELECT * FROM rw_view1;
 EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
                                QUERY PLAN                                
 -------------------------------------------------------------------------
- Update on base_tbl base_tbl_1
+ Update on base_tbl base_tbl_2
    ->  Nested Loop
          ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_1
                Index Cond: (id = 1)
          ->  Subquery Scan on base_tbl
                Filter: snoop(base_tbl.data)
-               ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_2
+               ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_3
                      Index Cond: (id = 1)
                      Filter: (NOT deleted)
 (9 rows)
-- 
1.8.3.1

0001-Updatable-S-B-Views-Dean-With-RowMark-Fixes.difftext/x-patch; name=0001-Updatable-S-B-Views-Dean-With-RowMark-Fixes.diffDownload
>From 855e5cb41058324efebaecf6c77bb70ef993d1fc Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Fri, 24 Jan 2014 12:02:18 +0800
Subject: [PATCH] Dean Rasheed's RLS patch v3, amended

CAEZATCVAqJV5WTjLmyObP21n+CzhbEx2AOzH4e6qmTcueVDjdQ@mail.gmail.com
plus amendments posted 2014-01-30 by Craig to fix rowmarking

2014-01-23
---
 doc/src/sgml/ref/create_view.sgml             |   6 -
 src/backend/commands/tablecmds.c              |   6 +-
 src/backend/commands/view.c                   |   6 +-
 src/backend/nodes/copyfuncs.c                 |   1 +
 src/backend/nodes/equalfuncs.c                |   1 +
 src/backend/nodes/nodeFuncs.c                 |   4 +
 src/backend/nodes/outfuncs.c                  |   1 +
 src/backend/nodes/readfuncs.c                 |   1 +
 src/backend/optimizer/plan/planner.c          |  46 ++-
 src/backend/optimizer/prep/Makefile           |   2 +-
 src/backend/optimizer/prep/prepsecurity.c     | 467 ++++++++++++++++++++++++++
 src/backend/optimizer/prep/prepunion.c        |  57 +++-
 src/backend/rewrite/rewriteHandler.c          |  53 ++-
 src/include/nodes/parsenodes.h                |   1 +
 src/include/optimizer/prep.h                  |   5 +
 src/include/rewrite/rewriteHandler.h          |   1 -
 src/test/regress/expected/create_view.out     |   2 +-
 src/test/regress/expected/updatable_views.out | 325 +++++++++++++++---
 src/test/regress/sql/updatable_views.sql      | 121 ++++++-
 19 files changed, 1001 insertions(+), 105 deletions(-)
 create mode 100644 src/backend/optimizer/prep/prepsecurity.c

diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
index e0fbe1e..888410f 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -323,12 +323,6 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
        or set-returning functions.
       </para>
      </listitem>
-
-     <listitem>
-      <para>
-       The view must not have the <literal>security_barrier</> property.
-      </para>
-     </listitem>
     </itemizedlist>
    </para>
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 08b037e..b48c8d3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -8829,7 +8829,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 		List	   *view_options = untransformRelOptions(newOptions);
 		ListCell   *cell;
 		bool		check_option = false;
-		bool		security_barrier = false;
 
 		foreach(cell, view_options)
 		{
@@ -8837,8 +8836,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 
 			if (pg_strcasecmp(defel->defname, "check_option") == 0)
 				check_option = true;
-			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
-				security_barrier = defGetBoolean(defel);
 		}
 
 		/*
@@ -8848,8 +8845,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
 		if (check_option)
 		{
 			const char *view_updatable_error =
-				view_query_is_auto_updatable(view_query,
-											 security_barrier, true);
+				view_query_is_auto_updatable(view_query, true);
 
 			if (view_updatable_error)
 				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index 1735762..bc08566 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -396,7 +396,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	RangeVar   *view;
 	ListCell   *cell;
 	bool		check_option;
-	bool		security_barrier;
 
 	/*
 	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
@@ -451,7 +450,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	 * specified.
 	 */
 	check_option = false;
-	security_barrier = false;
 
 	foreach(cell, stmt->options)
 	{
@@ -459,8 +457,6 @@ DefineView(ViewStmt *stmt, const char *queryString)
 
 		if (pg_strcasecmp(defel->defname, "check_option") == 0)
 			check_option = true;
-		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
-			security_barrier = defGetBoolean(defel);
 	}
 
 	/*
@@ -470,7 +466,7 @@ DefineView(ViewStmt *stmt, const char *queryString)
 	if (check_option)
 	{
 		const char *view_updatable_error =
-			view_query_is_auto_updatable(viewParse, security_barrier, true);
+			view_query_is_auto_updatable(viewParse, true);
 
 		if (view_updatable_error)
 			ereport(ERROR,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index bb356d0..1e49a71 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1998,6 +1998,7 @@ _copyRangeTblEntry(const RangeTblEntry *from)
 	COPY_SCALAR_FIELD(checkAsUser);
 	COPY_BITMAPSET_FIELD(selectedCols);
 	COPY_BITMAPSET_FIELD(modifiedCols);
+	COPY_NODE_FIELD(securityQuals);
 
 	return newnode;
 }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 5908d9a..ab055d5 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2290,6 +2290,7 @@ _equalRangeTblEntry(const RangeTblEntry *a, const RangeTblEntry *b)
 	COMPARE_SCALAR_FIELD(checkAsUser);
 	COMPARE_BITMAPSET_FIELD(selectedCols);
 	COMPARE_BITMAPSET_FIELD(modifiedCols);
+	COMPARE_NODE_FIELD(securityQuals);
 
 	return true;
 }
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 123f2a6..1e48a7f 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2020,6 +2020,9 @@ range_table_walker(List *rtable,
 					return true;
 				break;
 		}
+
+		if (walker(rte->securityQuals, context))
+			return true;
 	}
 	return false;
 }
@@ -2755,6 +2758,7 @@ range_table_mutator(List *rtable,
 				MUTATE(newrte->values_lists, rte->values_lists, List *);
 				break;
 		}
+		MUTATE(newrte->securityQuals, rte->securityQuals, List *);
 		newrt = lappend(newrt, newrte);
 	}
 	return newrt;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 568c3b8..a0e3286 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2409,6 +2409,7 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 	WRITE_OID_FIELD(checkAsUser);
 	WRITE_BITMAPSET_FIELD(selectedCols);
 	WRITE_BITMAPSET_FIELD(modifiedCols);
+	WRITE_NODE_FIELD(securityQuals);
 }
 
 static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 216d75e..ef1eae9 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1252,6 +1252,7 @@ _readRangeTblEntry(void)
 	READ_OID_FIELD(checkAsUser);
 	READ_BITMAPSET_FIELD(selectedCols);
 	READ_BITMAPSET_FIELD(modifiedCols);
+	READ_NODE_FIELD(securityQuals);
 
 	READ_DONE();
 }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 35bda67..a1498ba 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -916,6 +916,12 @@ inheritance_planner(PlannerInfo *root)
 		subplan = grouping_planner(&subroot, 0.0 /* retrieve all tuples */ );
 
 		/*
+		 * Planning may have modified the query result relation (if there
+		 * were security barrier quals on the result RTE).
+		 */
+		appinfo->child_relid = subroot.parse->resultRelation;
+
+		/*
 		 * If this child rel was excluded by constraint exclusion, exclude it
 		 * from the result plan.
 		 */
@@ -932,9 +938,32 @@ inheritance_planner(PlannerInfo *root)
 		if (final_rtable == NIL)
 			final_rtable = subroot.parse->rtable;
 		else
-			final_rtable = list_concat(final_rtable,
+		{
+			List	   *tmp_rtable = NIL;
+			ListCell   *cell1, *cell2;
+
+			/*
+			 * Planning this new child may have turned some of the original
+			 * RTEs into subqueries (if they had security barrier quals). If
+			 * so, we want to use these in the final rtable.
+			 */
+			forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
+			{
+				RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
+				RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
+
+				if (rte1->rtekind == RTE_RELATION &&
+					rte1->securityQuals != NIL &&
+					rte2->rtekind == RTE_SUBQUERY)
+					tmp_rtable = lappend(tmp_rtable, rte2);
+				else
+					tmp_rtable = lappend(tmp_rtable, rte1);
+			}
+
+			final_rtable = list_concat(tmp_rtable,
 									   list_copy_tail(subroot.parse->rtable,
 												 list_length(final_rtable)));
+		}
 
 		/*
 		 * We need to collect all the RelOptInfos from all child plans into
@@ -1163,6 +1192,12 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
 		tlist = preprocess_targetlist(root, tlist);
 
 		/*
+		 * Expand any rangetable entries that have security barrier quals.
+		 * This may add new security barrier subquery RTEs to the rangetable.
+		 */
+		expand_security_quals(root, tlist);
+
+		/*
 		 * Locate any window functions in the tlist.  (We don't need to look
 		 * anywhere else, since expressions used in ORDER BY will be in there
 		 * too.)  Note that they could all have been eliminated by constant
@@ -2150,9 +2185,11 @@ preprocess_rowmarks(PlannerInfo *root)
 		 * Ignore RowMarkClauses for subqueries; they aren't real tables and
 		 * can't support true locking.  Subqueries that got flattened into the
 		 * main query should be ignored completely.  Any that didn't will get
-		 * ROW_MARK_COPY items in the next loop.
+		 * ROW_MARK_COPY items in the next loop.  Relations with security
+		 * barrier quals will be later expanded into subqueries, so we treat
+		 * them as such here too.
 		 */
-		if (rte->rtekind != RTE_RELATION)
+		if (rte->rtekind != RTE_RELATION || rte->securityQuals != NIL)
 			continue;
 
 		/*
@@ -2208,7 +2245,8 @@ preprocess_rowmarks(PlannerInfo *root)
 		newrc->rowmarkId = ++(root->glob->lastRowMarkId);
 		/* real tables support REFERENCE, anything else needs COPY */
 		if (rte->rtekind == RTE_RELATION &&
-			rte->relkind != RELKIND_FOREIGN_TABLE)
+			rte->relkind != RELKIND_FOREIGN_TABLE &&
+			rte->securityQuals == NIL)
 			newrc->markType = ROW_MARK_REFERENCE;
 		else
 			newrc->markType = ROW_MARK_COPY;
diff --git a/src/backend/optimizer/prep/Makefile b/src/backend/optimizer/prep/Makefile
index 86301bf..5195d9b 100644
--- a/src/backend/optimizer/prep/Makefile
+++ b/src/backend/optimizer/prep/Makefile
@@ -12,6 +12,6 @@ subdir = src/backend/optimizer/prep
 top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
-OBJS = prepjointree.o prepqual.o preptlist.o prepunion.o
+OBJS = prepjointree.o prepqual.o prepsecurity.o preptlist.o prepunion.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
new file mode 100644
index 0000000..01e788c
--- /dev/null
+++ b/src/backend/optimizer/prep/prepsecurity.c
@@ -0,0 +1,467 @@
+/*-------------------------------------------------------------------------
+ *
+ * prepsecurity.c
+ *	  Routines for preprocessing security barrier quals.
+ *
+ * Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/optimizer/prep/prepsecurity.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/sysattr.h"
+#include "catalog/heap.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/prep.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteManip.h"
+#include "utils/rel.h"
+
+
+typedef struct
+{
+	int			rt_index;		/* Index of security barrier RTE */
+	int			sublevels_up;	/* Current nesting depth */
+	Relation	rel;			/* RTE relation at rt_index */
+	List	   *targetlist;		/* Targetlist for new subquery RTE */
+	List	   *colnames;		/* Column names in subquery RTE */
+	List	   *vars_processed;	/* List of Vars already processed */
+} security_barrier_replace_vars_context;
+
+static void expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual);
+
+static void security_barrier_replace_vars(Node *node,
+							  security_barrier_replace_vars_context *context);
+
+static bool security_barrier_replace_vars_walker(Node *node,
+									 security_barrier_replace_vars_context *context);
+
+static void change_rowmarks(PlanRowMark *parent, int old_rt_index,
+							int new_rt_index, List *rowMarks);
+
+
+/*
+ * expand_security_quals -
+ *	  expands any security barrier quals on RTEs in the query rtable, turning
+ *	  them into security barrier subqueries.
+ *
+ * Any given RTE may have multiple security barrier quals in a list, from which
+ * we create a set of nested subqueries to isolate each security barrier from
+ * the others, providing protection against malicious user-defined security
+ * barriers.  The first security barrier qual in the list will be used in the
+ * innermost subquery.
+ */
+void
+expand_security_quals(PlannerInfo *root, List *tlist)
+{
+	Query	       *parse = root->parse;
+	int				rt_index, new_rt_index;
+	PlanRowMark	   *rowmark;
+
+	/*
+	 * Process each RTE in the rtable list.
+	 *
+	 * Note that this is deliberately not a foreach loop, since the rtable may
+	 * be modified each time through the loop.
+	 */
+	rt_index = 0;
+	while (rt_index < list_length(parse->rtable))
+	{
+		RangeTblEntry *rte;
+
+		rt_index++;
+		rte = rt_fetch(rt_index, parse->rtable);
+
+		if (rte->securityQuals == NIL)
+			continue;
+
+		/*
+		 * Ignore any RTEs that aren't used in the query (such RTEs may be
+		 * present for permissions checks).
+		 */
+		if (rt_index != parse->resultRelation &&
+			!rangeTableEntry_used((Node *) parse, rt_index, 0))
+			continue;
+
+		/*
+		 * If this RTE is the target relation or is the subject of any RowMarks
+		 * then we need to make a copy of it before expanding it.  The
+		 * unexpanded copy will become the new target, and the original RTE
+		 * will be expanded to become the source of rows to update/delete.
+		 */
+		rowmark = get_plan_rowmark(root->rowMarks, rt_index);
+		if (rt_index == parse->resultRelation || rowmark != NULL )
+		{
+			RangeTblEntry *newrte = copyObject(rte);
+			parse->rtable = lappend(parse->rtable, newrte);
+			parse->resultRelation = new_rt_index = list_length(parse->rtable);
+
+			/*
+			 * Wipe out any copied security barrier quals on the new target to
+			 * prevent infinite recursion.
+			 */
+			newrte->securityQuals = NIL;
+
+			/*
+			 * There's no need to do permissions checks twice, so wipe out the
+			 * permissions info for the original RTE (we prefer to keep the
+			 * bits set on the result RTE).
+			 */
+			rte->requiredPerms = 0;
+			rte->checkAsUser = InvalidOid;
+			rte->selectedCols = NULL;
+			rte->modifiedCols = NULL;
+
+			/*
+			 * For the most part, Vars referencing the original relation should
+			 * remain as they are, meaning that they pull OLD values from the
+			 * expanded RTE.  But in the RETURNING list and in any WITH CHECK
+			 * OPTION quals, we want such Vars to represent NEW values, so
+			 * change them to reference the new RTE.
+			 */
+			ChangeVarNodes((Node *) parse->returningList, rt_index,
+						   parse->resultRelation, 0);
+
+			ChangeVarNodes((Node *) parse->withCheckOptions, rt_index,
+						   parse->resultRelation, 0);
+
+			/*
+			 * The PlanRowMark for this RTI, if present, must also
+			 * point to the underlying RTE_RELATION we copied,
+			 * not to the subquery RTE.
+			 */
+			if (rowmark)
+				change_rowmarks(rowmark, rt_index, new_rt_index, root->rowMarks);
+		}
+
+		/*
+		 * Process each security barrier qual in turn, starting with the
+		 * innermost one (the first in the list) and working outwards.
+		 *
+		 * We remove each qual from the list before processing it, so that its
+		 * variables aren't modified by expand_security_qual.  Also we don't
+		 * necessarily want the attributes referred to by the qual to be
+		 * exposed by the newly built subquery.
+		 */
+		while (rte->securityQuals != NIL)
+		{
+			Node   *qual = (Node *) linitial(rte->securityQuals);
+			rte->securityQuals = list_delete_first(rte->securityQuals);
+
+			ChangeVarNodes(qual, rt_index, 1, 0);
+			expand_security_qual(parse, tlist, rt_index, qual);
+		}
+	}
+}
+
+/*
+ * When the rangetable entry for a rowmark target is copied and replaced with a
+ * subquery, rowmarks referring to the replaced RTE must be rewritten to point to
+ * its new index.
+ *
+ * Takes the rowmark to adjust ("parent"), the old and new rtis for the RTE we
+ * just copied, and the list of all rowmarks in the plan.
+ */
+static void
+change_rowmarks(PlanRowMark *parent, int old_rt_index,
+				int new_rt_index, List *rowMarks)
+{
+	ListCell	   *lc;
+	PlanRowMark	   *rowmark;
+
+	/* Adjust rti and (if self) prti to point to the copied RTE. */
+	parent->rti = new_rt_index;
+	if (parent->prti == old_rt_index)
+		parent->prti = new_rt_index;
+	/* If this is a parent RowMark, fix up prti on its children. */
+	if (parent->isParent) {
+		foreach (lc, rowMarks)
+		{
+			rowmark = (PlanRowMark*) lfirst(lc);
+			if (rowmark->prti == old_rt_index)
+				rowmark->prti = new_rt_index;
+		}
+	}
+}
+
+
+
+/*
+ * expand_security_qual -
+ *	  expand the specified security barrier qual on a query RTE, turning the
+ *	  RTE into a security barrier subquery.
+ */
+static void
+expand_security_qual(Query *parse, List *tlist, int rt_index, Node *qual)
+{
+	RangeTblEntry  *rte;
+	Oid				relid;
+	Query		   *subquery;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+	security_barrier_replace_vars_context context;
+	ListCell	   *cell;
+
+	rte = rt_fetch(rt_index, parse->rtable);
+	relid = rte->relid;
+
+	/*
+	 * There should only be 2 possible cases:
+	 *
+	 * 1. A relation RTE, which we turn into a subquery RTE containing all
+	 * referenced columns.
+	 *
+	 * 2. A subquery RTE (either from a prior call to this function or from an
+	 * expanded view).  In this case we build a new subquery on top of it to
+	 * isolate this security barrier qual from any other quals.
+	 */
+	switch (rte->rtekind)
+	{
+		case RTE_RELATION:
+			/*
+			 * Turn the relation RTE into a security barrier subquery RTE,
+			 * moving all permissions checks down into the subquery.
+			 */
+			subquery = makeNode(Query);
+			subquery->commandType = CMD_SELECT;
+			subquery->querySource = QSRC_INSTEAD_RULE;
+
+			subrte = copyObject(rte);
+			subrte->inFromCl = true;
+			subrte->securityQuals = NIL;
+			subquery->rtable = list_make1(subrte);
+
+			subrtr = makeNode(RangeTblRef);
+			subrtr->rtindex = 1;
+			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+			subquery->hasSubLinks = checkExprHasSubLink(qual);
+
+			rte->rtekind = RTE_SUBQUERY;
+			rte->relid = InvalidOid;
+			rte->subquery = subquery;
+			rte->security_barrier = true;
+			rte->inh = false;			/* must not be set for a subquery */
+
+			/* the permissions checks have now been moved down */
+			rte->requiredPerms = 0;
+			rte->checkAsUser = InvalidOid;
+			rte->selectedCols = NULL;
+			rte->modifiedCols = NULL;
+
+			/*
+			 * Replace any variables in the outer query that refer to the
+			 * original relation RTE with references to columns that we will
+			 * expose in the new subquery, building the subquery's targetlist
+			 * as we go.
+			 */
+			context.rt_index = rt_index;
+			context.sublevels_up = 0;
+			context.rel = heap_open(relid, NoLock);
+			context.targetlist = NIL;
+			context.colnames = NIL;
+			context.vars_processed = NIL;
+
+			security_barrier_replace_vars((Node *) parse, &context);
+			security_barrier_replace_vars((Node *) tlist, &context);
+
+			heap_close(context.rel, NoLock);
+
+			/* Now we know what columns the subquery needs to expose */
+			rte->subquery->targetList = context.targetlist;
+			rte->eref = makeAlias(rte->eref->aliasname, context.colnames);
+
+			break;
+
+		case RTE_SUBQUERY:
+			/*
+			 * Build a new subquery that includes all the same columns as the
+			 * original subquery.
+			 */
+			subquery = makeNode(Query);
+			subquery->commandType = CMD_SELECT;
+			subquery->querySource = QSRC_INSTEAD_RULE;
+			subquery->targetList = NIL;
+
+			foreach(cell, rte->subquery->targetList)
+			{
+				TargetEntry	   *tle;
+				Var			   *var;
+
+				tle = (TargetEntry *) lfirst(cell);
+				var = makeVarFromTargetEntry(1, tle);
+
+				tle = makeTargetEntry((Expr *) var,
+									  list_length(subquery->targetList) + 1,
+									  pstrdup(tle->resname),
+									  tle->resjunk);
+				subquery->targetList = lappend(subquery->targetList, tle);
+			}
+
+			subrte = makeNode(RangeTblEntry);
+			subrte->rtekind = RTE_SUBQUERY;
+			subrte->subquery = rte->subquery;
+			subrte->security_barrier = rte->security_barrier;
+			subrte->eref = copyObject(rte->eref);
+			subrte->inFromCl = true;
+			subquery->rtable = list_make1(subrte);
+
+			subrtr = makeNode(RangeTblRef);
+			subrtr->rtindex = 1;
+			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+			subquery->hasSubLinks = checkExprHasSubLink(qual);
+
+			rte->subquery = subquery;
+			rte->security_barrier = true;
+
+			break;
+
+		default:
+			elog(ERROR, "invalid range table entry for security barrier qual");
+	}
+}
+
+
+/*
+ * security_barrier_replace_vars -
+ *	  Apply security barrier variable replacement to an expression tree.
+ *
+ * This also builds/updates a targetlist with entries for each replacement
+ * variable that needs to be exposed by the security barrier subquery RTE.
+ *
+ * NOTE: although this has the form of a walker, we cheat and modify the
+ * nodes in-place.	The given expression tree should have been copied
+ * earlier to ensure that no unwanted side-effects occur!
+ */
+static void
+security_barrier_replace_vars(Node *node,
+							  security_barrier_replace_vars_context *context)
+{
+	/*
+	 * Must be prepared to start with a Query or a bare expression tree; if
+	 * it's a Query, go straight to query_tree_walker to make sure that
+	 * sublevels_up doesn't get incremented prematurely.
+	 */
+	if (node && IsA(node, Query))
+		query_tree_walker((Query *) node,
+						  security_barrier_replace_vars_walker,
+						  (void *) context, 0);
+	else
+		security_barrier_replace_vars_walker(node, context);
+}
+
+static bool
+security_barrier_replace_vars_walker(Node *node,
+									 security_barrier_replace_vars_context *context)
+{
+	if (node == NULL)
+		return false;
+	if (IsA(node, Var))
+	{
+		Var		   *var = (Var *) node;
+
+		/*
+		 * Note that the same Var may be present in different lists, so we
+		 * need to take care not to process it multiple times.
+		 */
+		if (var->varno == context->rt_index &&
+			var->varlevelsup == context->sublevels_up &&
+			!list_member_ptr(context->vars_processed, var))
+		{
+			/*
+			 * Found a matching variable. Make sure that it is in the subquery
+			 * targetlist and map its attno accordingly.
+			 */
+			AttrNumber	attno;
+			ListCell   *l;
+			TargetEntry *tle;
+			char	   *attname;
+			Var		   *newvar;
+
+			/* Search for the base attribute in the subquery targetlist */
+			attno = InvalidAttrNumber;
+			foreach(l, context->targetlist)
+			{
+				tle = (TargetEntry *) lfirst(l);
+				attno++;
+
+				Assert(IsA(tle->expr, Var));
+				if (((Var *) tle->expr)->varattno == var->varattno &&
+					((Var *) tle->expr)->varcollid == var->varcollid)
+				{
+					/* Map the variable onto this subquery targetlist entry */
+					var->varattno = attno;
+					return false;
+				}
+			}
+
+			/* Not in the subquery targetlist, so add it. Get its name. */
+			if (var->varattno < 0)
+			{
+				Form_pg_attribute att_tup;
+
+				att_tup = SystemAttributeDefinition(var->varattno,
+													context->rel->rd_rel->relhasoids);
+				attname = NameStr(att_tup->attname);
+			}
+			else if (var->varattno == InvalidAttrNumber)
+			{
+				attname = "wholerow";
+			}
+			else if (var->varattno <= context->rel->rd_att->natts)
+			{
+				Form_pg_attribute att_tup;
+
+				att_tup = context->rel->rd_att->attrs[var->varattno - 1];
+				attname = NameStr(att_tup->attname);
+			}
+			else
+			{
+				elog(ERROR, "invalid attribute number %d in security_barrier_replace_vars", var->varattno);
+			}
+
+			/* New variable for subquery targetlist */
+			newvar = copyObject(var);
+			newvar->varno = 1;
+
+			attno = list_length(context->targetlist) + 1;
+			tle = makeTargetEntry((Expr *) newvar,
+								  attno,
+								  pstrdup(attname),
+								  false);
+
+			context->targetlist = lappend(context->targetlist, tle);
+
+			context->colnames = lappend(context->colnames,
+										makeString(pstrdup(attname)));
+
+			/* Update the outer query's variable */
+			var->varattno = attno;
+
+			/* Remember this Var so that we don't process it again */
+			context->vars_processed = lappend(context->vars_processed, var);
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		/* Recurse into subselects */
+		bool		result;
+
+		context->sublevels_up++;
+		result = query_tree_walker((Query *) node,
+								   security_barrier_replace_vars_walker,
+								   (void *) context, 0);
+		context->sublevels_up--;
+		return result;
+	}
+	return expression_tree_walker(node, security_barrier_replace_vars_walker,
+								  (void *) context);
+}
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 52dcc72..c7e0199 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -55,6 +55,7 @@ typedef struct
 {
 	PlannerInfo *root;
 	AppendRelInfo *appinfo;
+	int			sublevels_up;
 } adjust_appendrel_attrs_context;
 
 static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1580,8 +1581,9 @@ translate_col_privs(const Bitmapset *parent_privs,
  *	  child rel instead.  We also update rtindexes appearing outside Vars,
  *	  such as resultRelation and jointree relids.
  *
- * Note: this is only applied after conversion of sublinks to subplans,
- * so we don't need to cope with recursion into sub-queries.
+ * Note: this is applied after conversion of sublinks to subplans in the
+ * query jointree, but there may still be sublinks in the security barrier
+ * quals of RTEs, so we do need to cope with recursion into sub-queries.
  *
  * Note: this is not hugely different from what pullup_replace_vars() does;
  * maybe we should try to fold the two routines together.
@@ -1594,9 +1596,12 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
 
 	context.root = root;
 	context.appinfo = appinfo;
+	context.sublevels_up = 0;
 
 	/*
-	 * Must be prepared to start with a Query or a bare expression tree.
+	 * Must be prepared to start with a Query or a bare expression tree; if
+	 * it's a Query, go straight to query_tree_walker to make sure that
+	 * sublevels_up doesn't get incremented prematurely.
 	 */
 	if (node && IsA(node, Query))
 	{
@@ -1635,7 +1640,7 @@ adjust_appendrel_attrs_mutator(Node *node,
 	{
 		Var		   *var = (Var *) copyObject(node);
 
-		if (var->varlevelsup == 0 &&
+		if (var->varlevelsup == context->sublevels_up &&
 			var->varno == appinfo->parent_relid)
 		{
 			var->varno = appinfo->child_relid;
@@ -1652,6 +1657,7 @@ adjust_appendrel_attrs_mutator(Node *node,
 				if (newnode == NULL)
 					elog(ERROR, "attribute %d of relation \"%s\" does not exist",
 						 var->varattno, get_rel_name(appinfo->parent_reloid));
+				((Var *) newnode)->varlevelsup += context->sublevels_up;
 				return newnode;
 			}
 			else if (var->varattno == 0)
@@ -1694,10 +1700,16 @@ adjust_appendrel_attrs_mutator(Node *node,
 					RowExpr    *rowexpr;
 					List	   *fields;
 					RangeTblEntry *rte;
+					ListCell   *lc;
 
 					rte = rt_fetch(appinfo->parent_relid,
 								   context->root->parse->rtable);
 					fields = (List *) copyObject(appinfo->translated_vars);
+					foreach(lc, fields)
+					{
+						Var		   *field = (Var *) lfirst(lc);
+						field->varlevelsup += context->sublevels_up;
+					}
 					rowexpr = makeNode(RowExpr);
 					rowexpr->args = fields;
 					rowexpr->row_typeid = var->vartype;
@@ -1716,7 +1728,8 @@ adjust_appendrel_attrs_mutator(Node *node,
 	{
 		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
 
-		if (cexpr->cvarno == appinfo->parent_relid)
+		if (context->sublevels_up == 0 &&
+			cexpr->cvarno == appinfo->parent_relid)
 			cexpr->cvarno = appinfo->child_relid;
 		return (Node *) cexpr;
 	}
@@ -1724,7 +1737,8 @@ adjust_appendrel_attrs_mutator(Node *node,
 	{
 		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
 
-		if (rtr->rtindex == appinfo->parent_relid)
+		if (context->sublevels_up == 0 &&
+			rtr->rtindex == appinfo->parent_relid)
 			rtr->rtindex = appinfo->child_relid;
 		return (Node *) rtr;
 	}
@@ -1737,7 +1751,8 @@ adjust_appendrel_attrs_mutator(Node *node,
 											  adjust_appendrel_attrs_mutator,
 												 (void *) context);
 		/* now fix JoinExpr's rtindex (probably never happens) */
-		if (j->rtindex == appinfo->parent_relid)
+		if (context->sublevels_up == 0 &&
+			j->rtindex == appinfo->parent_relid)
 			j->rtindex = appinfo->child_relid;
 		return (Node *) j;
 	}
@@ -1750,7 +1765,7 @@ adjust_appendrel_attrs_mutator(Node *node,
 											  adjust_appendrel_attrs_mutator,
 														 (void *) context);
 		/* now fix PlaceHolderVar's relid sets */
-		if (phv->phlevelsup == 0)
+		if (phv->phlevelsup == context->sublevels_up)
 			phv->phrels = adjust_relid_set(phv->phrels,
 										   appinfo->parent_relid,
 										   appinfo->child_relid);
@@ -1822,12 +1837,26 @@ adjust_appendrel_attrs_mutator(Node *node,
 		return (Node *) newinfo;
 	}
 
-	/*
-	 * NOTE: we do not need to recurse into sublinks, because they should
-	 * already have been converted to subplans before we see them.
-	 */
-	Assert(!IsA(node, SubLink));
-	Assert(!IsA(node, Query));
+	if (IsA(node, Query))
+	{
+		/*
+		 * Recurse into sublink subqueries. This should only be possible in
+		 * security barrier quals of top-level RTEs. All other sublinks should
+		 * have already been converted to subplans during expression
+		 * preprocessing, but this doesn't happen for security barrier quals,
+		 * since they are destined to become quals of a subquery RTE, which
+		 * will be recursively planned, and so should not be preprocessed at
+		 * this stage.
+		 */
+		Query	   *newnode;
+
+		context->sublevels_up++;
+		newnode = query_tree_mutator((Query *) node,
+									 adjust_appendrel_attrs_mutator,
+									 (void *) context, 0);
+		context->sublevels_up--;
+		return (Node *) newnode;
+	}
 
 	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
 								   (void *) context);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 0b13645..370b5aa 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1973,8 +1973,7 @@ view_col_is_auto_updatable(RangeTblRef *rtr, TargetEntry *tle)
  * updatable.
  */
 const char *
-view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
-							 bool check_cols)
+view_query_is_auto_updatable(Query *viewquery, bool check_cols)
 {
 	RangeTblRef *rtr;
 	RangeTblEntry *base_rte;
@@ -2048,14 +2047,6 @@ view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
 		return gettext_noop("Views that return set-returning functions 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 the future.
-	 */
-	if (security_barrier)
-		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.
 	 */
@@ -2303,9 +2294,7 @@ relation_is_updatable(Oid reloid,
 	{
 		Query	   *viewquery = get_view_query(rel);
 
-		if (view_query_is_auto_updatable(viewquery,
-										 RelationIsSecurityView(rel),
-										 false) == NULL)
+		if (view_query_is_auto_updatable(viewquery, false) == NULL)
 		{
 			Bitmapset  *updatable_cols;
 			int			auto_events;
@@ -2460,7 +2449,6 @@ rewriteTargetView(Query *parsetree, Relation view)
 
 	auto_update_detail =
 		view_query_is_auto_updatable(viewquery,
-									 RelationIsSecurityView(view),
 									 parsetree->commandType != CMD_DELETE);
 
 	if (auto_update_detail)
@@ -2664,6 +2652,14 @@ rewriteTargetView(Query *parsetree, Relation view)
 												   view_targetlist);
 
 	/*
+	 * Move any security barrier quals from the view RTE onto the new target
+	 * RTE.  Any such quals should now apply to the new target RTE and will not
+	 * reference the original view RTE in the rewritten query.
+	 */
+	new_rte->securityQuals = view_rte->securityQuals;
+	view_rte->securityQuals = NIL;
+
+	/*
 	 * 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.
@@ -2743,6 +2739,10 @@ rewriteTargetView(Query *parsetree, Relation view)
 	 * only adjust their varnos to reference the new target (just the same as
 	 * we did with the view targetlist).
 	 *
+	 * Note that there is special-case handling for the quals of a security
+	 * barrier view, since they need to be kept separate from any user-supplied
+	 * quals, so these quals are kept on the new target RTE.
+	 *
 	 * For INSERT, the view's quals can be ignored in the main query.
 	 */
 	if (parsetree->commandType != CMD_INSERT &&
@@ -2751,7 +2751,25 @@ rewriteTargetView(Query *parsetree, Relation view)
 		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
 
 		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
-		AddQual(parsetree, (Node *) viewqual);
+
+		if (RelationIsSecurityView(view))
+		{
+			/*
+			 * Note: the parsetree has been mutated, so the new_rte pointer is
+			 * stale and needs to be re-computed.
+			 */
+			new_rte = rt_fetch(new_rt_index, parsetree->rtable);
+			new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals);
+
+			/*
+			 * Make sure that the query is marked correctly if the added qual
+			 * has sublinks.
+			 */
+			if (!parsetree->hasSubLinks)
+				parsetree->hasSubLinks = checkExprHasSubLink(viewqual);
+		}
+		else
+			AddQual(parsetree, (Node *) viewqual);
 	}
 
 	/*
@@ -2813,9 +2831,8 @@ rewriteTargetView(Query *parsetree, Relation view)
 				 * Make sure that the query is marked correctly if the added
 				 * qual has sublinks.  We can skip this check if the query is
 				 * already marked, or if the command is an UPDATE, in which
-				 * case the same qual will have already been added to the
-				 * query's WHERE clause, and AddQual will have already done
-				 * this check.
+				 * case the same qual will have already been added, and this
+				 * check will already have been done.
 				 */
 				if (!parsetree->hasSubLinks &&
 					parsetree->commandType != CMD_UPDATE)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 846c31a..a197a86 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -801,6 +801,7 @@ typedef struct RangeTblEntry
 	Oid			checkAsUser;	/* if valid, check access as this role */
 	Bitmapset  *selectedCols;	/* columns needing SELECT permission */
 	Bitmapset  *modifiedCols;	/* columns needing INSERT/UPDATE permission */
+	List	   *securityQuals;	/* any security barrier quals to apply */
 } RangeTblEntry;
 
 /*
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
index 0f5a7d3..f5fc7e8 100644
--- a/src/include/optimizer/prep.h
+++ b/src/include/optimizer/prep.h
@@ -36,6 +36,11 @@ extern Node *negate_clause(Node *node);
 extern Expr *canonicalize_qual(Expr *qual);
 
 /*
+ * prototypes for prepsecurity.c
+ */
+extern void expand_security_quals(PlannerInfo *root, List *tlist);
+
+/*
  * prototypes for preptlist.c
  */
 extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index e4027bd..8da0af5 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -23,7 +23,6 @@ extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
 extern Node *build_column_default(Relation rel, int attrno);
 extern Query *get_view_query(Relation view);
 extern const char *view_query_is_auto_updatable(Query *viewquery,
-										 bool security_barrier,
 										 bool check_cols);
 extern int	relation_is_updatable(Oid reloid,
 						  bool include_triggers,
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index 91d1639..f6db582 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -252,7 +252,7 @@ CREATE VIEW mysecview4 WITH (security_barrier)
        AS SELECT * FROM tbl1 WHERE a <> 0;
 CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
        AS SELECT * FROM tbl1 WHERE a > 100;
-ERROR:  security_barrier requires a Boolean value
+ERROR:  invalid value for boolean option "security_barrier": 100
 CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
        AS SELECT * FROM tbl1 WHERE a < 100;
 ERROR:  unrecognized parameter "invalid_option"
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 99c9165..9a34819 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -22,12 +22,10 @@ CREATE VIEW rw_view14 AS SELECT ctid, a, b FROM base_tbl; -- System columns may
 CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
 CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
 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 VIEW ro_view18 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
-CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
+CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
+CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
 SELECT table_name, is_insertable_into
   FROM information_schema.tables
  WHERE table_name LIKE E'r_\\_view%'
@@ -44,7 +42,6 @@ SELECT table_name, is_insertable_into
  ro_view19  | NO
  ro_view2   | NO
  ro_view20  | NO
- ro_view21  | NO
  ro_view3   | NO
  ro_view4   | NO
  ro_view5   | NO
@@ -55,7 +52,7 @@ SELECT table_name, is_insertable_into
  rw_view14  | YES
  rw_view15  | YES
  rw_view16  | YES
-(21 rows)
+(20 rows)
 
 SELECT table_name, is_updatable, is_insertable_into
   FROM information_schema.views
@@ -73,7 +70,6 @@ SELECT table_name, is_updatable, is_insertable_into
  ro_view19  | NO           | NO
  ro_view2   | NO           | NO
  ro_view20  | NO           | NO
- ro_view21  | NO           | NO
  ro_view3   | NO           | NO
  ro_view4   | NO           | NO
  ro_view5   | NO           | NO
@@ -84,7 +80,7 @@ SELECT table_name, is_updatable, is_insertable_into
  rw_view14  | YES          | YES
  rw_view15  | YES          | YES
  rw_view16  | YES          | YES
-(21 rows)
+(20 rows)
 
 SELECT table_name, column_name, is_updatable
   FROM information_schema.columns
@@ -103,23 +99,21 @@ SELECT table_name, column_name, is_updatable
  ro_view17  | a             | NO
  ro_view17  | b             | NO
  ro_view18  | a             | NO
- ro_view18  | b             | NO
- ro_view19  | a             | NO
+ ro_view19  | sequence_name | NO
+ ro_view19  | last_value    | NO
+ ro_view19  | start_value   | NO
+ ro_view19  | increment_by  | NO
+ ro_view19  | max_value     | NO
+ ro_view19  | min_value     | NO
+ ro_view19  | cache_value   | NO
+ ro_view19  | log_cnt       | NO
+ ro_view19  | is_cycled     | NO
+ ro_view19  | is_called     | 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_view21  | a             | NO
- ro_view21  | b             | NO
- ro_view21  | g             | NO
+ ro_view20  | a             | NO
+ ro_view20  | b             | NO
+ ro_view20  | g             | NO
  ro_view3   | ?column?      | NO
  ro_view4   | count         | NO
  ro_view5   | a             | NO
@@ -140,7 +134,7 @@ SELECT table_name, column_name, is_updatable
  rw_view16  | a             | YES
  rw_view16  | b             | YES
  rw_view16  | aa            | YES
-(48 rows)
+(46 rows)
 
 -- Read-only views
 DELETE FROM ro_view1;
@@ -268,24 +262,20 @@ 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 enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
-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 enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
-DELETE FROM ro_view19;
-ERROR:  cannot delete from view "ro_view19"
+DELETE FROM ro_view18;
+ERROR:  cannot delete from view "ro_view18"
 DETAIL:  Views that do not select from a single table or view are not automatically updatable.
 HINT:  To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
-UPDATE ro_view20 SET max_value=1000;
-ERROR:  cannot update view "ro_view20"
+UPDATE ro_view19 SET max_value=1000;
+ERROR:  cannot update view "ro_view19"
 DETAIL:  Views that do not select from a single table or view are not automatically updatable.
 HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
-UPDATE ro_view21 SET b=upper(b);
-ERROR:  cannot update view "ro_view21"
+UPDATE ro_view20 SET b=upper(b);
+ERROR:  cannot update view "ro_view20"
 DETAIL:  Views that return set-returning functions are not automatically updatable.
 HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
 DROP TABLE base_tbl CASCADE;
-NOTICE:  drop cascades to 17 other objects
+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
@@ -299,13 +289,12 @@ drop cascades to view ro_view11
 drop cascades to view ro_view13
 drop cascades to view rw_view15
 drop cascades to view rw_view16
-drop cascades to view ro_view18
-drop cascades to view ro_view21
+drop cascades to view ro_view20
 drop cascades to view ro_view4
 drop cascades to view rw_view14
-DROP VIEW ro_view10, ro_view12, ro_view19;
+DROP VIEW ro_view10, ro_view12, ro_view18;
 DROP SEQUENCE seq CASCADE;
-NOTICE:  drop cascades to view ro_view20
+NOTICE:  drop cascades to view ro_view19
 -- 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);
@@ -1740,3 +1729,259 @@ 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
+-- security barrier view
+CREATE TABLE base_tbl (person text, visibility text);
+INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                            ('Dick', 'private'),
+                            ('Harry', 'public');
+CREATE VIEW rw_view1 AS
+  SELECT person FROM base_tbl WHERE visibility = 'public';
+CREATE FUNCTION snoop(val text)
+RETURNS boolean AS
+$$
+BEGIN
+  RAISE NOTICE 'snooped value: %', val;
+  RETURN true;
+END;
+$$
+LANGUAGE plpgsql COST 0.000001;
+SELECT * FROM rw_view1 WHERE snoop(person);
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Dick
+NOTICE:  snooped value: Harry
+ person 
+--------
+ Tom
+ Harry
+(2 rows)
+
+UPDATE rw_view1 SET person=person WHERE snoop(person);
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Dick
+NOTICE:  snooped value: Harry
+DELETE FROM rw_view1 WHERE NOT snoop(person);
+NOTICE:  snooped value: Dick
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Harry
+ALTER VIEW rw_view1 SET (security_barrier = true);
+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   | person      | YES
+(1 row)
+
+SELECT * FROM rw_view1 WHERE snoop(person);
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Harry
+ person 
+--------
+ Tom
+ Harry
+(2 rows)
+
+UPDATE rw_view1 SET person=person WHERE snoop(person);
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Harry
+DELETE FROM rw_view1 WHERE NOT snoop(person);
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Harry
+EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+                  QUERY PLAN                   
+-----------------------------------------------
+ Subquery Scan on rw_view1
+   Filter: snoop(rw_view1.person)
+   ->  Seq Scan on base_tbl
+         Filter: (visibility = 'public'::text)
+(4 rows)
+
+EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Update on base_tbl base_tbl_1
+   ->  Subquery Scan on base_tbl
+         Filter: snoop(base_tbl.person)
+         ->  Seq Scan on base_tbl base_tbl_2
+               Filter: (visibility = 'public'::text)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Delete on base_tbl base_tbl_1
+   ->  Subquery Scan on base_tbl
+         Filter: (NOT snoop(base_tbl.person))
+         ->  Seq Scan on base_tbl base_tbl_2
+               Filter: (visibility = 'public'::text)
+(5 rows)
+
+-- security barrier view on top of security barrier view
+CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+  SELECT * FROM rw_view1 WHERE snoop(person);
+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   | person      | YES
+(1 row)
+
+SELECT * FROM rw_view2 WHERE snoop(person);
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Harry
+NOTICE:  snooped value: Harry
+ person 
+--------
+ Tom
+ Harry
+(2 rows)
+
+UPDATE rw_view2 SET person=person WHERE snoop(person);
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Harry
+NOTICE:  snooped value: Harry
+DELETE FROM rw_view2 WHERE NOT snoop(person);
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Harry
+NOTICE:  snooped value: Harry
+EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Subquery Scan on rw_view2
+   Filter: snoop(rw_view2.person)
+   ->  Subquery Scan on rw_view1
+         Filter: snoop(rw_view1.person)
+         ->  Seq Scan on base_tbl
+               Filter: (visibility = 'public'::text)
+(6 rows)
+
+EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Update on base_tbl base_tbl_1
+   ->  Subquery Scan on base_tbl
+         Filter: snoop(base_tbl.person)
+         ->  Subquery Scan on base_tbl_2
+               Filter: snoop(base_tbl_2.person)
+               ->  Seq Scan on base_tbl base_tbl_3
+                     Filter: (visibility = 'public'::text)
+(7 rows)
+
+EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Delete on base_tbl base_tbl_1
+   ->  Subquery Scan on base_tbl
+         Filter: (NOT snoop(base_tbl.person))
+         ->  Subquery Scan on base_tbl_2
+               Filter: snoop(base_tbl_2.person)
+               ->  Seq Scan on base_tbl base_tbl_3
+                     Filter: (visibility = 'public'::text)
+(7 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
+-- security barrier view on top of table with rules
+CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean);
+INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true);
+CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl
+  WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id)
+  DO INSTEAD
+    UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id;
+CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl
+  DO INSTEAD
+    UPDATE base_tbl SET deleted = true WHERE id = old.id;
+CREATE VIEW rw_view1 WITH (security_barrier=true) AS
+  SELECT id, data FROM base_tbl WHERE NOT deleted;
+SELECT * FROM rw_view1;
+ id | data  
+----+-------
+  1 | Row 1
+(1 row)
+
+EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Update on base_tbl base_tbl_2
+   ->  Nested Loop
+         ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_1
+               Index Cond: (id = 1)
+         ->  Subquery Scan on base_tbl
+               Filter: snoop(base_tbl.data)
+               ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_3
+                     Index Cond: (id = 1)
+                     Filter: (NOT deleted)
+(9 rows)
+
+DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+NOTICE:  snooped value: Row 1
+EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Insert on base_tbl
+   InitPlan 1 (returns $0)
+     ->  Index Only Scan using base_tbl_pkey on base_tbl t
+           Index Cond: (id = 2)
+   ->  Result
+         One-Time Filter: ($0 IS NOT TRUE)
+ 
+ Update on base_tbl
+   InitPlan 1 (returns $0)
+     ->  Index Only Scan using base_tbl_pkey on base_tbl t
+           Index Cond: (id = 2)
+   ->  Result
+         One-Time Filter: $0
+         ->  Index Scan using base_tbl_pkey on base_tbl
+               Index Cond: (id = 2)
+(15 rows)
+
+INSERT INTO rw_view1 VALUES (2, 'New row 2');
+SELECT * FROM base_tbl;
+ id |   data    | deleted 
+----+-----------+---------
+  1 | Row 1     | t
+  2 | New row 2 | f
+(2 rows)
+
+DROP TABLE base_tbl CASCADE;
+NOTICE:  drop cascades to view rw_view1
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
index a77cf19..08f7504 100644
--- a/src/test/regress/sql/updatable_views.sql
+++ b/src/test/regress/sql/updatable_views.sql
@@ -25,12 +25,10 @@ CREATE VIEW rw_view14 AS SELECT ctid, a, b FROM base_tbl; -- System columns may
 CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
 CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
 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 VIEW ro_view18 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
-CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
+CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
+CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
 
 SELECT table_name, is_insertable_into
   FROM information_schema.tables
@@ -87,13 +85,12 @@ SELECT * FROM base_tbl;
 DELETE FROM rw_view16 WHERE a=-3; -- should be OK
 -- Read-only views
 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;
-UPDATE ro_view21 SET b=upper(b);
+DELETE FROM ro_view18;
+UPDATE ro_view19 SET max_value=1000;
+UPDATE ro_view20 SET b=upper(b);
 
 DROP TABLE base_tbl CASCADE;
-DROP VIEW ro_view10, ro_view12, ro_view19;
+DROP VIEW ro_view10, ro_view12, ro_view18;
 DROP SEQUENCE seq CASCADE;
 
 -- simple updatable view
@@ -828,3 +825,107 @@ CREATE VIEW rw_view2 AS
   SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION;
 INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check)
 DROP TABLE base_tbl CASCADE;
+
+-- security barrier view
+
+CREATE TABLE base_tbl (person text, visibility text);
+INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                            ('Dick', 'private'),
+                            ('Harry', 'public');
+
+CREATE VIEW rw_view1 AS
+  SELECT person FROM base_tbl WHERE visibility = 'public';
+
+CREATE FUNCTION snoop(val text)
+RETURNS boolean AS
+$$
+BEGIN
+  RAISE NOTICE 'snooped value: %', val;
+  RETURN true;
+END;
+$$
+LANGUAGE plpgsql COST 0.000001;
+
+SELECT * FROM rw_view1 WHERE snoop(person);
+UPDATE rw_view1 SET person=person WHERE snoop(person);
+DELETE FROM rw_view1 WHERE NOT snoop(person);
+
+ALTER VIEW rw_view1 SET (security_barrier = true);
+
+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;
+
+SELECT * FROM rw_view1 WHERE snoop(person);
+UPDATE rw_view1 SET person=person WHERE snoop(person);
+DELETE FROM rw_view1 WHERE NOT snoop(person);
+
+EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+
+-- security barrier view on top of security barrier view
+
+CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+  SELECT * FROM rw_view1 WHERE snoop(person);
+
+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;
+
+SELECT * FROM rw_view2 WHERE snoop(person);
+UPDATE rw_view2 SET person=person WHERE snoop(person);
+DELETE FROM rw_view2 WHERE NOT snoop(person);
+
+EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+
+DROP TABLE base_tbl CASCADE;
+
+-- security barrier view on top of table with rules
+
+CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean);
+INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true);
+
+CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl
+  WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id)
+  DO INSTEAD
+    UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id;
+
+CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl
+  DO INSTEAD
+    UPDATE base_tbl SET deleted = true WHERE id = old.id;
+
+CREATE VIEW rw_view1 WITH (security_barrier=true) AS
+  SELECT id, data FROM base_tbl WHERE NOT deleted;
+
+SELECT * FROM rw_view1;
+
+EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+
+EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
+INSERT INTO rw_view1 VALUES (2, 'New row 2');
+
+SELECT * FROM base_tbl;
+
+DROP TABLE base_tbl CASCADE;
-- 
1.8.3.1

#50Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#49)
Re: WIP patch (v2) for updatable security barrier views

On 30 January 2014 05:36, Craig Ringer <craig@2ndquadrant.com> wrote:

On 01/29/2014 08:29 PM, Dean Rasheed wrote:

On 29 January 2014 11:34, Craig Ringer <craig@2ndquadrant.com> wrote:

On 01/23/2014 06:06 PM, Dean Rasheed wrote:

On 21 January 2014 09:18, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Yes, please review the patch from 09-Jan
(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg18ToTggP8zOBq6QnQHQ@mail.gmail.com).

After further testing I found a bug --- it involves having a security
barrier view on top of a base relation that has a rule that rewrites
the query to have a different result relation, and possibly also a
different command type, so that the securityQuals are no longer on the
result relation, which is a code path not previously tested and the
rowmark handling was wrong. That's probably a pretty obscure case in
the context of security barrier views, but that code path would be
used much more commonly if RLS were built on top of this. Fortunately
the fix is trivial --- updated patch attached.

This is the most recent patch I see, and the one I've been working on
top of.

Are there any known tests that this patch fails?

None that I've been able to come up with.

I've found an issue. I'm not sure if it can be triggered from SQL, but
it affects in-code users who add their own securityQuals.

expand_security_quals fails to update any rowMarks that may point to a
relation being expanded. If the relation isn't the target relation, this
causes a rowmark to refer to a RTE with no relid, and thus failure in
the executor.

Relative patch against updatable s.b. views attached (for easier
reading), along with a new revision of updatable s.b. views that
incorporates the patch.

This isn't triggered by FOR SHARE / FOR UPDATE rowmark on a security
barrier view because the flattening to securityQuals and re-expansion in
the optimizer is only done for _target_ security barrier views. For
target views, different rowmark handling applies, so they don't trigger
it either.

This is triggered by adding securityQuals to a non-target relation
during row-security processing, though.

Hmm, looks like this is a pre-existing bug.

The first thing I tried was to reproduce it using SQL, so I used a
RULE to turn a DELETE into a SELECT FOR UPDATE. This is pretty dumb,
but it shows the problem:

CREATE TABLE foo (a int);
CREATE RULE foo_del_rule AS ON DELETE TO foo
DO INSTEAD SELECT * FROM foo FOR UPDATE;
DELETE FROM foo;

ERROR: no relation entry for relid 1

So I think this should be fixed independently of the updatable s.b. view code.

Regards,
Dean

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

#51Simon Riggs
simon@2ndQuadrant.com
In reply to: Dean Rasheed (#50)
Re: WIP patch (v2) for updatable security barrier views

On 30 January 2014 11:55, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Hmm, looks like this is a pre-existing bug.

The first thing I tried was to reproduce it using SQL, so I used a
RULE to turn a DELETE into a SELECT FOR UPDATE. This is pretty dumb,
but it shows the problem:

CREATE TABLE foo (a int);
CREATE RULE foo_del_rule AS ON DELETE TO foo
DO INSTEAD SELECT * FROM foo FOR UPDATE;
DELETE FROM foo;

ERROR: no relation entry for relid 1

So I think this should be fixed independently of the updatable s.b. view code.

Looks to me there isn't much use case for turning DML into a SELECT -
where would we expect the output to go to if the caller wasn't
prepared to handle the result rows?

IMHO we should simply prohibit such cases rather than attempt to fix
the fact they don't work. We know for certain nobody relies on this
behaviour because its broken already.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#52Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#49)
1 attachment(s)
Re: WIP patch (v2) for updatable security barrier views

On 30 January 2014 05:36, Craig Ringer <craig@2ndquadrant.com> wrote:

On 01/29/2014 08:29 PM, Dean Rasheed wrote:

On 29 January 2014 11:34, Craig Ringer <craig@2ndquadrant.com> wrote:

On 01/23/2014 06:06 PM, Dean Rasheed wrote:

On 21 January 2014 09:18, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:

Yes, please review the patch from 09-Jan
(/messages/by-id/CAEZATCUiKxOg=vOOvjA2S6G-sixzzxg18ToTggP8zOBq6QnQHQ@mail.gmail.com).

After further testing I found a bug --- it involves having a security
barrier view on top of a base relation that has a rule that rewrites
the query to have a different result relation, and possibly also a
different command type, so that the securityQuals are no longer on the
result relation, which is a code path not previously tested and the
rowmark handling was wrong. That's probably a pretty obscure case in
the context of security barrier views, but that code path would be
used much more commonly if RLS were built on top of this. Fortunately
the fix is trivial --- updated patch attached.

This is the most recent patch I see, and the one I've been working on
top of.

Are there any known tests that this patch fails?

None that I've been able to come up with.

I've found an issue. I'm not sure if it can be triggered from SQL, but
it affects in-code users who add their own securityQuals.

It's difficult to fix this kind of issue without a reproducible test
case, but...

expand_security_quals fails to update any rowMarks that may point to a
relation being expanded. If the relation isn't the target relation, this
causes a rowmark to refer to a RTE with no relid, and thus failure in
the executor.

Relative patch against updatable s.b. views attached (for easier
reading), along with a new revision of updatable s.b. views that
incorporates the patch.

I don't like this fix --- you appear to be adding another RTE to the
rangetable (one not in the FROM list) and applying the rowmarks to it,
which seems wrong because you're not locking the right set of rows.
This is reflected in the change to the regression test output where,
in one of the tests, the ctids for the table to update are no longer
coming from the same table. I think a better approach is to push down
the rowmark into the subquery so that any locking required applies to
the pushed down RTE --- see the attached patch.

This is an alternative (and more general) fix to the problem I found
upthread (/messages/by-id/CAEZATCVAqJV5WTjLmyObP21n+CzhbEx2AOzH4e6qmTcueVDjdQ@mail.gmail.com)
with rules on the base table, but hopefully it will solve the issue
you're seeing too.

It doesn't change any of the existing regression test output, but
neither does it add any new tests, since I can't see a way to provoke
your issue in isolation using SQL without first running into the
pre-existing bug with rules that turn DML into SELECT ... FOR UPDATE.

Anyway, please test if this works with your RLS code.

Regards,
Dean

Attachments:

updatable-sb-views.patchtext/x-diff; charset=US-ASCII; name=updatable-sb-views.patchDownload
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
new file mode 100644
index e0fbe1e..888410f
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
*************** CREATE VIEW vista AS SELECT text 'Hello
*** 323,334 ****
         or set-returning functions.
        </para>
       </listitem>
- 
-      <listitem>
-       <para>
-        The view must not have the <literal>security_barrier</> property.
-       </para>
-      </listitem>
      </itemizedlist>
     </para>
  
--- 323,328 ----
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index 08b037e..b48c8d3
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** ATExecSetRelOptions(Relation rel, List *
*** 8829,8835 ****
  		List	   *view_options = untransformRelOptions(newOptions);
  		ListCell   *cell;
  		bool		check_option = false;
- 		bool		security_barrier = false;
  
  		foreach(cell, view_options)
  		{
--- 8829,8834 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8837,8844 ****
  
  			if (pg_strcasecmp(defel->defname, "check_option") == 0)
  				check_option = true;
- 			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 				security_barrier = defGetBoolean(defel);
  		}
  
  		/*
--- 8836,8841 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8848,8855 ****
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query,
! 											 security_barrier, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
--- 8845,8851 ----
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
new file mode 100644
index 1735762..bc08566
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
*************** DefineView(ViewStmt *stmt, const char *q
*** 396,402 ****
  	RangeVar   *view;
  	ListCell   *cell;
  	bool		check_option;
- 	bool		security_barrier;
  
  	/*
  	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
--- 396,401 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 451,457 ****
  	 * specified.
  	 */
  	check_option = false;
- 	security_barrier = false;
  
  	foreach(cell, stmt->options)
  	{
--- 450,455 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 459,466 ****
  
  		if (pg_strcasecmp(defel->defname, "check_option") == 0)
  			check_option = true;
- 		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 			security_barrier = defGetBoolean(defel);
  	}
  
  	/*
--- 457,462 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 470,476 ****
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, security_barrier, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
--- 466,472 ----
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index f90cb67..dae6c66
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 1998,2003 ****
--- 1998,2004 ----
  	COPY_SCALAR_FIELD(checkAsUser);
  	COPY_BITMAPSET_FIELD(selectedCols);
  	COPY_BITMAPSET_FIELD(modifiedCols);
+ 	COPY_NODE_FIELD(securityQuals);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 9438e78..f8f5562
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2293,2298 ****
--- 2293,2299 ----
  	COMPARE_SCALAR_FIELD(checkAsUser);
  	COMPARE_BITMAPSET_FIELD(selectedCols);
  	COMPARE_BITMAPSET_FIELD(modifiedCols);
+ 	COMPARE_NODE_FIELD(securityQuals);
  
  	return true;
  }
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 123f2a6..1e48a7f
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** range_table_walker(List *rtable,
*** 2020,2025 ****
--- 2020,2028 ----
  					return true;
  				break;
  		}
+ 
+ 		if (walker(rte->securityQuals, context))
+ 			return true;
  	}
  	return false;
  }
*************** range_table_mutator(List *rtable,
*** 2755,2760 ****
--- 2758,2764 ----
  				MUTATE(newrte->values_lists, rte->values_lists, List *);
  				break;
  		}
+ 		MUTATE(newrte->securityQuals, rte->securityQuals, List *);
  		newrt = lappend(newrt, newrte);
  	}
  	return newrt;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 568c3b8..a0e3286
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outRangeTblEntry(StringInfo str, const
*** 2409,2414 ****
--- 2409,2415 ----
  	WRITE_OID_FIELD(checkAsUser);
  	WRITE_BITMAPSET_FIELD(selectedCols);
  	WRITE_BITMAPSET_FIELD(modifiedCols);
+ 	WRITE_NODE_FIELD(securityQuals);
  }
  
  static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index 216d75e..ef1eae9
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readRangeTblEntry(void)
*** 1252,1257 ****
--- 1252,1258 ----
  	READ_OID_FIELD(checkAsUser);
  	READ_BITMAPSET_FIELD(selectedCols);
  	READ_BITMAPSET_FIELD(modifiedCols);
+ 	READ_NODE_FIELD(securityQuals);
  
  	READ_DONE();
  }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 35bda67..1beb6c8
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 916,921 ****
--- 916,927 ----
  		subplan = grouping_planner(&subroot, 0.0 /* retrieve all tuples */ );
  
  		/*
+ 		 * Planning may have modified the query result relation (if there
+ 		 * were security barrier quals on the result RTE).
+ 		 */
+ 		appinfo->child_relid = subroot.parse->resultRelation;
+ 
+ 		/*
  		 * If this child rel was excluded by constraint exclusion, exclude it
  		 * from the result plan.
  		 */
*************** inheritance_planner(PlannerInfo *root)
*** 932,940 ****
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 			final_rtable = list_concat(final_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
--- 938,969 ----
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 		{
! 			List	   *tmp_rtable = NIL;
! 			ListCell   *cell1, *cell2;
! 
! 			/*
! 			 * Planning this new child may have turned some of the original
! 			 * RTEs into subqueries (if they had security barrier quals). If
! 			 * so, we want to use these in the final rtable.
! 			 */
! 			forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
! 			{
! 				RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
! 				RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
! 
! 				if (rte1->rtekind == RTE_RELATION &&
! 					rte1->securityQuals != NIL &&
! 					rte2->rtekind == RTE_SUBQUERY)
! 					tmp_rtable = lappend(tmp_rtable, rte2);
! 				else
! 					tmp_rtable = lappend(tmp_rtable, rte1);
! 			}
! 
! 			final_rtable = list_concat(tmp_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
+ 		}
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
*************** grouping_planner(PlannerInfo *root, doub
*** 1163,1168 ****
--- 1192,1203 ----
  		tlist = preprocess_targetlist(root, tlist);
  
  		/*
+ 		 * Expand any rangetable entries that have security barrier quals.
+ 		 * This may add new security barrier subquery RTEs to the rangetable.
+ 		 */
+ 		expand_security_quals(root, tlist);
+ 
+ 		/*
  		 * Locate any window functions in the tlist.  (We don't need to look
  		 * anywhere else, since expressions used in ORDER BY will be in there
  		 * too.)  Note that they could all have been eliminated by constant
diff --git a/src/backend/optimizer/prep/Makefile b/src/backend/optimizer/prep/Makefile
new file mode 100644
index 86301bf..5195d9b
*** a/src/backend/optimizer/prep/Makefile
--- b/src/backend/optimizer/prep/Makefile
*************** subdir = src/backend/optimizer/prep
*** 12,17 ****
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
--- 12,17 ----
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o prepsecurity.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
new file mode 100644
index ...91a6e5d
*** a/src/backend/optimizer/prep/prepsecurity.c
--- b/src/backend/optimizer/prep/prepsecurity.c
***************
*** 0 ****
--- 1,457 ----
+ /*-------------------------------------------------------------------------
+  *
+  * prepsecurity.c
+  *	  Routines for preprocessing security barrier quals.
+  *
+  * Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
+  * Portions Copyright (c) 1994, Regents of the University of California
+  *
+  *
+  * IDENTIFICATION
+  *	  src/backend/optimizer/prep/prepsecurity.c
+  *
+  *-------------------------------------------------------------------------
+  */
+ #include "postgres.h"
+ 
+ #include "access/heapam.h"
+ #include "access/sysattr.h"
+ #include "catalog/heap.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/prep.h"
+ #include "parser/analyze.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteManip.h"
+ #include "utils/rel.h"
+ 
+ 
+ typedef struct
+ {
+ 	int			rt_index;		/* Index of security barrier RTE */
+ 	int			sublevels_up;	/* Current nesting depth */
+ 	Relation	rel;			/* RTE relation at rt_index */
+ 	List	   *targetlist;		/* Targetlist for new subquery RTE */
+ 	List	   *colnames;		/* Column names in subquery RTE */
+ 	List	   *vars_processed;	/* List of Vars already processed */
+ } security_barrier_replace_vars_context;
+ 
+ static void expand_security_qual(PlannerInfo *root, List *tlist, int rt_index,
+ 					 RangeTblEntry *rte, Node *qual);
+ 
+ static void security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context);
+ 
+ static bool security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context);
+ 
+ 
+ /*
+  * expand_security_quals -
+  *	  expands any security barrier quals on RTEs in the query rtable, turning
+  *	  them into security barrier subqueries.
+  *
+  * Any given RTE may have multiple security barrier quals in a list, from which
+  * we create a set of nested subqueries to isolate each security barrier from
+  * the others, providing protection against malicious user-defined security
+  * barriers.  The first security barrier qual in the list will be used in the
+  * innermost subquery.
+  */
+ void
+ expand_security_quals(PlannerInfo *root, List *tlist)
+ {
+ 	Query	   *parse = root->parse;
+ 	int			rt_index;
+ 	ListCell   *cell;
+ 
+ 	/*
+ 	 * Process each RTE in the rtable list.
+ 	 *
+ 	 * We only ever modify entries in place and append to the rtable, so it is
+ 	 * safe to use a foreach loop here.
+ 	 */
+ 	rt_index = 0;
+ 	foreach(cell, parse->rtable)
+ 	{
+ 		RangeTblEntry *rte = (RangeTblEntry *) lfirst(cell);
+ 
+ 		rt_index++;
+ 
+ 		if (rte->securityQuals == NIL)
+ 			continue;
+ 
+ 		/*
+ 		 * Ignore any RTEs that aren't used in the query (such RTEs may be
+ 		 * present for permissions checks).
+ 		 */
+ 		if (rt_index != parse->resultRelation &&
+ 			!rangeTableEntry_used((Node *) parse, rt_index, 0))
+ 			continue;
+ 
+ 		/*
+ 		 * If this RTE is the target then we need to make a copy of it before
+ 		 * expanding it.  The unexpanded copy will become the new target, and
+ 		 * the original RTE will be expanded to become the source of rows to
+ 		 * update/delete.
+ 		 */
+ 		if (rt_index == parse->resultRelation)
+ 		{
+ 			RangeTblEntry *newrte = copyObject(rte);
+ 			parse->rtable = lappend(parse->rtable, newrte);
+ 			parse->resultRelation = list_length(parse->rtable);
+ 
+ 			/*
+ 			 * Wipe out any copied security barrier quals on the new target to
+ 			 * prevent infinite recursion.
+ 			 */
+ 			newrte->securityQuals = NIL;
+ 
+ 			/*
+ 			 * There's no need to do permissions checks twice, so wipe out the
+ 			 * permissions info for the original RTE (we prefer to keep the
+ 			 * bits set on the result RTE).
+ 			 */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * For the most part, Vars referencing the original relation should
+ 			 * remain as they are, meaning that they pull OLD values from the
+ 			 * expanded RTE.  But in the RETURNING list and in any WITH CHECK
+ 			 * OPTION quals, we want such Vars to represent NEW values, so
+ 			 * change them to reference the new RTE.
+ 			 */
+ 			ChangeVarNodes((Node *) parse->returningList, rt_index,
+ 						   parse->resultRelation, 0);
+ 
+ 			ChangeVarNodes((Node *) parse->withCheckOptions, rt_index,
+ 						   parse->resultRelation, 0);
+ 		}
+ 
+ 		/*
+ 		 * Process each security barrier qual in turn, starting with the
+ 		 * innermost one (the first in the list) and working outwards.
+ 		 *
+ 		 * We remove each qual from the list before processing it, so that its
+ 		 * variables aren't modified by expand_security_qual.  Also we don't
+ 		 * necessarily want the attributes referred to by the qual to be
+ 		 * exposed by the newly built subquery.
+ 		 */
+ 		while (rte->securityQuals != NIL)
+ 		{
+ 			Node   *qual = (Node *) linitial(rte->securityQuals);
+ 			rte->securityQuals = list_delete_first(rte->securityQuals);
+ 
+ 			ChangeVarNodes(qual, rt_index, 1, 0);
+ 			expand_security_qual(root, tlist, rt_index, rte, qual);
+ 		}
+ 	}
+ }
+ 
+ 
+ /*
+  * expand_security_qual -
+  *	  expand the specified security barrier qual on a query RTE, turning the
+  *	  RTE into a security barrier subquery.
+  */
+ static void
+ expand_security_qual(PlannerInfo *root, List *tlist, int rt_index,
+ 					 RangeTblEntry *rte, Node *qual)
+ {
+ 	Query		   *parse = root->parse;
+ 	Oid				relid = rte->relid;
+ 	Query		   *subquery;
+ 	RangeTblEntry  *subrte;
+ 	RangeTblRef	   *subrtr;
+ 	PlanRowMark	   *rc;
+ 	security_barrier_replace_vars_context context;
+ 	ListCell	   *cell;
+ 
+ 	/*
+ 	 * There should only be 2 possible cases:
+ 	 *
+ 	 * 1. A relation RTE, which we turn into a subquery RTE containing all
+ 	 * referenced columns.
+ 	 *
+ 	 * 2. A subquery RTE (either from a prior call to this function or from an
+ 	 * expanded view).  In this case we build a new subquery on top of it to
+ 	 * isolate this security barrier qual from any other quals.
+ 	 */
+ 	switch (rte->rtekind)
+ 	{
+ 		case RTE_RELATION:
+ 			/*
+ 			 * Turn the relation RTE into a security barrier subquery RTE,
+ 			 * moving all permissions checks down into the subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 
+ 			subrte = copyObject(rte);
+ 			subrte->inFromCl = true;
+ 			subrte->securityQuals = NIL;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->rtekind = RTE_SUBQUERY;
+ 			rte->relid = InvalidOid;
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 			rte->inh = false;			/* must not be set for a subquery */
+ 
+ 			/* the permissions checks have now been moved down */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * Now deal with any PlanRowMark on this RTE by requesting a lock
+ 			 * of the same strength on the RTE copied down to the subquery.
+ 			 */
+ 			rc = get_plan_rowmark(root->rowMarks, rt_index);
+ 			if (rc != NULL)
+ 			{
+ 				switch (rc->markType)
+ 				{
+ 					case ROW_MARK_EXCLUSIVE:
+ 						applyLockingClause(subquery, 1, LCS_FORUPDATE,
+ 										   rc->noWait, false);
+ 						break;
+ 					case ROW_MARK_NOKEYEXCLUSIVE:
+ 						applyLockingClause(subquery, 1, LCS_FORNOKEYUPDATE,
+ 										   rc->noWait, false);
+ 						break;
+ 					case ROW_MARK_SHARE:
+ 						applyLockingClause(subquery, 1, LCS_FORSHARE,
+ 										   rc->noWait, false);
+ 						break;
+ 					case ROW_MARK_KEYSHARE:
+ 						applyLockingClause(subquery, 1, LCS_FORKEYSHARE,
+ 										   rc->noWait, false);
+ 						break;
+ 					case ROW_MARK_REFERENCE:
+ 					case ROW_MARK_COPY:
+ 						/* No locking needed */
+ 						break;
+ 				}
+ 				root->rowMarks = list_delete(root->rowMarks, rc);
+ 			}
+ 
+ 			/*
+ 			 * Replace any variables in the outer query that refer to the
+ 			 * original relation RTE with references to columns that we will
+ 			 * expose in the new subquery, building the subquery's targetlist
+ 			 * as we go.
+ 			 */
+ 			context.rt_index = rt_index;
+ 			context.sublevels_up = 0;
+ 			context.rel = heap_open(relid, NoLock);
+ 			context.targetlist = NIL;
+ 			context.colnames = NIL;
+ 			context.vars_processed = NIL;
+ 
+ 			security_barrier_replace_vars((Node *) parse, &context);
+ 			security_barrier_replace_vars((Node *) tlist, &context);
+ 
+ 			heap_close(context.rel, NoLock);
+ 
+ 			/* Now we know what columns the subquery needs to expose */
+ 			rte->subquery->targetList = context.targetlist;
+ 			rte->eref = makeAlias(rte->eref->aliasname, context.colnames);
+ 
+ 			break;
+ 
+ 		case RTE_SUBQUERY:
+ 			/*
+ 			 * Build a new subquery that includes all the same columns as the
+ 			 * original subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 			subquery->targetList = NIL;
+ 
+ 			foreach(cell, rte->subquery->targetList)
+ 			{
+ 				TargetEntry	   *tle;
+ 				Var			   *var;
+ 
+ 				tle = (TargetEntry *) lfirst(cell);
+ 				var = makeVarFromTargetEntry(1, tle);
+ 
+ 				tle = makeTargetEntry((Expr *) var,
+ 									  list_length(subquery->targetList) + 1,
+ 									  pstrdup(tle->resname),
+ 									  tle->resjunk);
+ 				subquery->targetList = lappend(subquery->targetList, tle);
+ 			}
+ 
+ 			subrte = makeNode(RangeTblEntry);
+ 			subrte->rtekind = RTE_SUBQUERY;
+ 			subrte->subquery = rte->subquery;
+ 			subrte->security_barrier = rte->security_barrier;
+ 			subrte->eref = copyObject(rte->eref);
+ 			subrte->inFromCl = true;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 
+ 			break;
+ 
+ 		default:
+ 			elog(ERROR, "invalid range table entry for security barrier qual");
+ 	}
+ }
+ 
+ 
+ /*
+  * security_barrier_replace_vars -
+  *	  Apply security barrier variable replacement to an expression tree.
+  *
+  * This also builds/updates a targetlist with entries for each replacement
+  * variable that needs to be exposed by the security barrier subquery RTE.
+  *
+  * NOTE: although this has the form of a walker, we cheat and modify the
+  * nodes in-place.	The given expression tree should have been copied
+  * earlier to ensure that no unwanted side-effects occur!
+  */
+ static void
+ security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context)
+ {
+ 	/*
+ 	 * Must be prepared to start with a Query or a bare expression tree; if
+ 	 * it's a Query, go straight to query_tree_walker to make sure that
+ 	 * sublevels_up doesn't get incremented prematurely.
+ 	 */
+ 	if (node && IsA(node, Query))
+ 		query_tree_walker((Query *) node,
+ 						  security_barrier_replace_vars_walker,
+ 						  (void *) context, 0);
+ 	else
+ 		security_barrier_replace_vars_walker(node, context);
+ }
+ 
+ static bool
+ security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context)
+ {
+ 	if (node == NULL)
+ 		return false;
+ 	if (IsA(node, Var))
+ 	{
+ 		Var		   *var = (Var *) node;
+ 
+ 		/*
+ 		 * Note that the same Var may be present in different lists, so we
+ 		 * need to take care not to process it multiple times.
+ 		 */
+ 		if (var->varno == context->rt_index &&
+ 			var->varlevelsup == context->sublevels_up &&
+ 			!list_member_ptr(context->vars_processed, var))
+ 		{
+ 			/*
+ 			 * Found a matching variable. Make sure that it is in the subquery
+ 			 * targetlist and map its attno accordingly.
+ 			 */
+ 			AttrNumber	attno;
+ 			ListCell   *l;
+ 			TargetEntry *tle;
+ 			char	   *attname;
+ 			Var		   *newvar;
+ 
+ 			/* Search for the base attribute in the subquery targetlist */
+ 			attno = InvalidAttrNumber;
+ 			foreach(l, context->targetlist)
+ 			{
+ 				tle = (TargetEntry *) lfirst(l);
+ 				attno++;
+ 
+ 				Assert(IsA(tle->expr, Var));
+ 				if (((Var *) tle->expr)->varattno == var->varattno &&
+ 					((Var *) tle->expr)->varcollid == var->varcollid)
+ 				{
+ 					/* Map the variable onto this subquery targetlist entry */
+ 					var->varattno = attno;
+ 					return false;
+ 				}
+ 			}
+ 
+ 			/* Not in the subquery targetlist, so add it. Get its name. */
+ 			if (var->varattno < 0)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = SystemAttributeDefinition(var->varattno,
+ 													context->rel->rd_rel->relhasoids);
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else if (var->varattno == InvalidAttrNumber)
+ 			{
+ 				attname = "wholerow";
+ 			}
+ 			else if (var->varattno <= context->rel->rd_att->natts)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = context->rel->rd_att->attrs[var->varattno - 1];
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else
+ 			{
+ 				elog(ERROR, "invalid attribute number %d in security_barrier_replace_vars", var->varattno);
+ 			}
+ 
+ 			/* New variable for subquery targetlist */
+ 			newvar = copyObject(var);
+ 			newvar->varno = 1;
+ 
+ 			attno = list_length(context->targetlist) + 1;
+ 			tle = makeTargetEntry((Expr *) newvar,
+ 								  attno,
+ 								  pstrdup(attname),
+ 								  false);
+ 
+ 			context->targetlist = lappend(context->targetlist, tle);
+ 
+ 			context->colnames = lappend(context->colnames,
+ 										makeString(pstrdup(attname)));
+ 
+ 			/* Update the outer query's variable */
+ 			var->varattno = attno;
+ 
+ 			/* Remember this Var so that we don't process it again */
+ 			context->vars_processed = lappend(context->vars_processed, var);
+ 		}
+ 		return false;
+ 	}
+ 
+ 	if (IsA(node, Query))
+ 	{
+ 		/* Recurse into subselects */
+ 		bool		result;
+ 
+ 		context->sublevels_up++;
+ 		result = query_tree_walker((Query *) node,
+ 								   security_barrier_replace_vars_walker,
+ 								   (void *) context, 0);
+ 		context->sublevels_up--;
+ 		return result;
+ 	}
+ 	return expression_tree_walker(node, security_barrier_replace_vars_walker,
+ 								  (void *) context);
+ }
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
new file mode 100644
index 52dcc72..c7e0199
*** a/src/backend/optimizer/prep/prepunion.c
--- b/src/backend/optimizer/prep/prepunion.c
*************** typedef struct
*** 55,60 ****
--- 55,61 ----
  {
  	PlannerInfo *root;
  	AppendRelInfo *appinfo;
+ 	int			sublevels_up;
  } adjust_appendrel_attrs_context;
  
  static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
*************** translate_col_privs(const Bitmapset *par
*** 1580,1587 ****
   *	  child rel instead.  We also update rtindexes appearing outside Vars,
   *	  such as resultRelation and jointree relids.
   *
!  * Note: this is only applied after conversion of sublinks to subplans,
!  * so we don't need to cope with recursion into sub-queries.
   *
   * Note: this is not hugely different from what pullup_replace_vars() does;
   * maybe we should try to fold the two routines together.
--- 1581,1589 ----
   *	  child rel instead.  We also update rtindexes appearing outside Vars,
   *	  such as resultRelation and jointree relids.
   *
!  * Note: this is applied after conversion of sublinks to subplans in the
!  * query jointree, but there may still be sublinks in the security barrier
!  * quals of RTEs, so we do need to cope with recursion into sub-queries.
   *
   * Note: this is not hugely different from what pullup_replace_vars() does;
   * maybe we should try to fold the two routines together.
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1594,1602 ****
  
  	context.root = root;
  	context.appinfo = appinfo;
  
  	/*
! 	 * Must be prepared to start with a Query or a bare expression tree.
  	 */
  	if (node && IsA(node, Query))
  	{
--- 1596,1607 ----
  
  	context.root = root;
  	context.appinfo = appinfo;
+ 	context.sublevels_up = 0;
  
  	/*
! 	 * Must be prepared to start with a Query or a bare expression tree; if
! 	 * it's a Query, go straight to query_tree_walker to make sure that
! 	 * sublevels_up doesn't get incremented prematurely.
  	 */
  	if (node && IsA(node, Query))
  	{
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1635,1641 ****
  	{
  		Var		   *var = (Var *) copyObject(node);
  
! 		if (var->varlevelsup == 0 &&
  			var->varno == appinfo->parent_relid)
  		{
  			var->varno = appinfo->child_relid;
--- 1640,1646 ----
  	{
  		Var		   *var = (Var *) copyObject(node);
  
! 		if (var->varlevelsup == context->sublevels_up &&
  			var->varno == appinfo->parent_relid)
  		{
  			var->varno = appinfo->child_relid;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1652,1657 ****
--- 1657,1663 ----
  				if (newnode == NULL)
  					elog(ERROR, "attribute %d of relation \"%s\" does not exist",
  						 var->varattno, get_rel_name(appinfo->parent_reloid));
+ 				((Var *) newnode)->varlevelsup += context->sublevels_up;
  				return newnode;
  			}
  			else if (var->varattno == 0)
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1694,1703 ****
--- 1700,1715 ----
  					RowExpr    *rowexpr;
  					List	   *fields;
  					RangeTblEntry *rte;
+ 					ListCell   *lc;
  
  					rte = rt_fetch(appinfo->parent_relid,
  								   context->root->parse->rtable);
  					fields = (List *) copyObject(appinfo->translated_vars);
+ 					foreach(lc, fields)
+ 					{
+ 						Var		   *field = (Var *) lfirst(lc);
+ 						field->varlevelsup += context->sublevels_up;
+ 					}
  					rowexpr = makeNode(RowExpr);
  					rowexpr->args = fields;
  					rowexpr->row_typeid = var->vartype;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1716,1722 ****
  	{
  		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
  
! 		if (cexpr->cvarno == appinfo->parent_relid)
  			cexpr->cvarno = appinfo->child_relid;
  		return (Node *) cexpr;
  	}
--- 1728,1735 ----
  	{
  		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
  
! 		if (context->sublevels_up == 0 &&
! 			cexpr->cvarno == appinfo->parent_relid)
  			cexpr->cvarno = appinfo->child_relid;
  		return (Node *) cexpr;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1724,1730 ****
  	{
  		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
  
! 		if (rtr->rtindex == appinfo->parent_relid)
  			rtr->rtindex = appinfo->child_relid;
  		return (Node *) rtr;
  	}
--- 1737,1744 ----
  	{
  		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
  
! 		if (context->sublevels_up == 0 &&
! 			rtr->rtindex == appinfo->parent_relid)
  			rtr->rtindex = appinfo->child_relid;
  		return (Node *) rtr;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1737,1743 ****
  											  adjust_appendrel_attrs_mutator,
  												 (void *) context);
  		/* now fix JoinExpr's rtindex (probably never happens) */
! 		if (j->rtindex == appinfo->parent_relid)
  			j->rtindex = appinfo->child_relid;
  		return (Node *) j;
  	}
--- 1751,1758 ----
  											  adjust_appendrel_attrs_mutator,
  												 (void *) context);
  		/* now fix JoinExpr's rtindex (probably never happens) */
! 		if (context->sublevels_up == 0 &&
! 			j->rtindex == appinfo->parent_relid)
  			j->rtindex = appinfo->child_relid;
  		return (Node *) j;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1750,1756 ****
  											  adjust_appendrel_attrs_mutator,
  														 (void *) context);
  		/* now fix PlaceHolderVar's relid sets */
! 		if (phv->phlevelsup == 0)
  			phv->phrels = adjust_relid_set(phv->phrels,
  										   appinfo->parent_relid,
  										   appinfo->child_relid);
--- 1765,1771 ----
  											  adjust_appendrel_attrs_mutator,
  														 (void *) context);
  		/* now fix PlaceHolderVar's relid sets */
! 		if (phv->phlevelsup == context->sublevels_up)
  			phv->phrels = adjust_relid_set(phv->phrels,
  										   appinfo->parent_relid,
  										   appinfo->child_relid);
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1822,1833 ****
  		return (Node *) newinfo;
  	}
  
! 	/*
! 	 * NOTE: we do not need to recurse into sublinks, because they should
! 	 * already have been converted to subplans before we see them.
! 	 */
! 	Assert(!IsA(node, SubLink));
! 	Assert(!IsA(node, Query));
  
  	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
  								   (void *) context);
--- 1837,1862 ----
  		return (Node *) newinfo;
  	}
  
! 	if (IsA(node, Query))
! 	{
! 		/*
! 		 * Recurse into sublink subqueries. This should only be possible in
! 		 * security barrier quals of top-level RTEs. All other sublinks should
! 		 * have already been converted to subplans during expression
! 		 * preprocessing, but this doesn't happen for security barrier quals,
! 		 * since they are destined to become quals of a subquery RTE, which
! 		 * will be recursively planned, and so should not be preprocessed at
! 		 * this stage.
! 		 */
! 		Query	   *newnode;
! 
! 		context->sublevels_up++;
! 		newnode = query_tree_mutator((Query *) node,
! 									 adjust_appendrel_attrs_mutator,
! 									 (void *) context, 0);
! 		context->sublevels_up--;
! 		return (Node *) newnode;
! 	}
  
  	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
  								   (void *) context);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 0b13645..370b5aa
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** view_col_is_auto_updatable(RangeTblRef *
*** 1973,1980 ****
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
! 							 bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
--- 1973,1979 ----
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
*************** view_query_is_auto_updatable(Query *view
*** 2048,2061 ****
  		return gettext_noop("Views that return set-returning functions 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 the future.
- 	 */
- 	if (security_barrier)
- 		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.
  	 */
--- 2047,2052 ----
*************** relation_is_updatable(Oid reloid,
*** 2303,2311 ****
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery,
! 										 RelationIsSecurityView(rel),
! 										 false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
--- 2294,2300 ----
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery, false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
*************** rewriteTargetView(Query *parsetree, Rela
*** 2460,2466 ****
  
  	auto_update_detail =
  		view_query_is_auto_updatable(viewquery,
- 									 RelationIsSecurityView(view),
  									 parsetree->commandType != CMD_DELETE);
  
  	if (auto_update_detail)
--- 2449,2454 ----
*************** rewriteTargetView(Query *parsetree, Rela
*** 2664,2669 ****
--- 2652,2665 ----
  												   view_targetlist);
  
  	/*
+ 	 * Move any security barrier quals from the view RTE onto the new target
+ 	 * RTE.  Any such quals should now apply to the new target RTE and will not
+ 	 * reference the original view RTE in the rewritten query.
+ 	 */
+ 	new_rte->securityQuals = view_rte->securityQuals;
+ 	view_rte->securityQuals = NIL;
+ 
+ 	/*
  	 * 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.
*************** rewriteTargetView(Query *parsetree, Rela
*** 2743,2748 ****
--- 2739,2748 ----
  	 * only adjust their varnos to reference the new target (just the same as
  	 * we did with the view targetlist).
  	 *
+ 	 * Note that there is special-case handling for the quals of a security
+ 	 * barrier view, since they need to be kept separate from any user-supplied
+ 	 * quals, so these quals are kept on the new target RTE.
+ 	 *
  	 * For INSERT, the view's quals can be ignored in the main query.
  	 */
  	if (parsetree->commandType != CMD_INSERT &&
*************** rewriteTargetView(Query *parsetree, Rela
*** 2751,2757 ****
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 		AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
--- 2751,2775 ----
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 
! 		if (RelationIsSecurityView(view))
! 		{
! 			/*
! 			 * Note: the parsetree has been mutated, so the new_rte pointer is
! 			 * stale and needs to be re-computed.
! 			 */
! 			new_rte = rt_fetch(new_rt_index, parsetree->rtable);
! 			new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals);
! 
! 			/*
! 			 * Make sure that the query is marked correctly if the added qual
! 			 * has sublinks.
! 			 */
! 			if (!parsetree->hasSubLinks)
! 				parsetree->hasSubLinks = checkExprHasSubLink(viewqual);
! 		}
! 		else
! 			AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
*************** rewriteTargetView(Query *parsetree, Rela
*** 2813,2821 ****
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added to the
! 				 * query's WHERE clause, and AddQual will have already done
! 				 * this check.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
--- 2831,2838 ----
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added, and this
! 				 * check will already have been done.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index ad58b39..1f9083c
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblEntry
*** 801,806 ****
--- 801,807 ----
  	Oid			checkAsUser;	/* if valid, check access as this role */
  	Bitmapset  *selectedCols;	/* columns needing SELECT permission */
  	Bitmapset  *modifiedCols;	/* columns needing INSERT/UPDATE permission */
+ 	List	   *securityQuals;	/* any security barrier quals to apply */
  } RangeTblEntry;
  
  /*
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
new file mode 100644
index 0f5a7d3..f5fc7e8
*** a/src/include/optimizer/prep.h
--- b/src/include/optimizer/prep.h
*************** extern Node *negate_clause(Node *node);
*** 36,41 ****
--- 36,46 ----
  extern Expr *canonicalize_qual(Expr *qual);
  
  /*
+  * prototypes for prepsecurity.c
+  */
+ extern void expand_security_quals(PlannerInfo *root, List *tlist);
+ 
+ /*
   * prototypes for preptlist.c
   */
  extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
new file mode 100644
index e4027bd..8da0af5
*** a/src/include/rewrite/rewriteHandler.h
--- b/src/include/rewrite/rewriteHandler.h
*************** extern void AcquireRewriteLocks(Query *p
*** 23,29 ****
  extern Node *build_column_default(Relation rel, int attrno);
  extern Query *get_view_query(Relation view);
  extern const char *view_query_is_auto_updatable(Query *viewquery,
- 										 bool security_barrier,
  										 bool check_cols);
  extern int	relation_is_updatable(Oid reloid,
  						  bool include_triggers,
--- 23,28 ----
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
new file mode 100644
index 91d1639..f6db582
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
*************** CREATE VIEW mysecview4 WITH (security_ba
*** 252,258 ****
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  security_barrier requires a Boolean value
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
--- 252,258 ----
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  invalid value for boolean option "security_barrier": 100
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 99c9165..261b0c5
*** a/src/test/regress/expected/updatable_views.out
--- b/src/test/regress/expected/updatable_views.out
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 22,33 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
--- 22,31 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
*************** SELECT table_name, is_insertable_into
*** 44,50 ****
   ro_view19  | NO
   ro_view2   | NO
   ro_view20  | NO
-  ro_view21  | NO
   ro_view3   | NO
   ro_view4   | NO
   ro_view5   | NO
--- 42,47 ----
*************** SELECT table_name, is_insertable_into
*** 55,61 ****
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (21 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
--- 52,58 ----
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (20 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
*************** SELECT table_name, is_updatable, is_inse
*** 73,79 ****
   ro_view19  | NO           | NO
   ro_view2   | NO           | NO
   ro_view20  | NO           | NO
-  ro_view21  | NO           | NO
   ro_view3   | NO           | NO
   ro_view4   | NO           | NO
   ro_view5   | NO           | NO
--- 70,75 ----
*************** SELECT table_name, is_updatable, is_inse
*** 84,90 ****
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (21 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
--- 80,86 ----
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (20 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
*************** SELECT table_name, column_name, is_updat
*** 103,125 ****
   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_view21  | a             | NO
!  ro_view21  | b             | NO
!  ro_view21  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
--- 99,119 ----
   ro_view17  | a             | NO
   ro_view17  | b             | NO
   ro_view18  | a             | NO
!  ro_view19  | sequence_name | NO
!  ro_view19  | last_value    | NO
!  ro_view19  | start_value   | NO
!  ro_view19  | increment_by  | NO
!  ro_view19  | max_value     | NO
!  ro_view19  | min_value     | NO
!  ro_view19  | cache_value   | NO
!  ro_view19  | log_cnt       | NO
!  ro_view19  | is_cycled     | NO
!  ro_view19  | is_called     | NO
   ro_view2   | a             | NO
   ro_view2   | b             | NO
!  ro_view20  | a             | NO
!  ro_view20  | b             | NO
!  ro_view20  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
*************** SELECT table_name, column_name, is_updat
*** 140,146 ****
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (48 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
--- 134,140 ----
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (46 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
*************** INSERT INTO ro_view17 VALUES (3, 'ROW 3'
*** 268,291 ****
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! 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 enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view21 SET b=upper(b);
! ERROR:  cannot update view "ro_view21"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  DROP TABLE base_tbl CASCADE;
! NOTICE:  drop cascades to 17 other objects
  DETAIL:  drop cascades to view ro_view1
  drop cascades to view ro_view17
  drop cascades to view ro_view2
--- 262,281 ----
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! DELETE FROM ro_view18;
! ERROR:  cannot delete from view "ro_view18"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! UPDATE ro_view19 SET max_value=1000;
! ERROR:  cannot update view "ro_view19"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view20 SET b=upper(b);
! ERROR:  cannot update view "ro_view20"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  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_view11
*** 299,311 ****
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view18
! drop cascades to view ro_view21
  drop cascades to view ro_view4
  drop cascades to view rw_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);
--- 289,300 ----
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view20
  drop cascades to view ro_view4
  drop cascades to view rw_view14
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
! NOTICE:  drop cascades to view ro_view19
  -- 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);
*************** DROP TABLE base_tbl CASCADE;
*** 1740,1742 ****
--- 1729,1987 ----
  NOTICE:  drop cascades to 2 other objects
  DETAIL:  drop cascades to view rw_view1
  drop cascades to view rw_view2
+ -- security barrier view
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+                   QUERY PLAN                   
+ -----------------------------------------------
+  Subquery Scan on rw_view1
+    Filter: snoop(rw_view1.person)
+    ->  Seq Scan on base_tbl
+          Filter: (visibility = 'public'::text)
+ (4 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ -- security barrier view on top of security barrier view
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Subquery Scan on rw_view2
+    Filter: snoop(rw_view2.person)
+    ->  Subquery Scan on rw_view1
+          Filter: snoop(rw_view1.person)
+          ->  Seq Scan on base_tbl
+                Filter: (visibility = 'public'::text)
+ (6 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 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
+ -- security barrier view on top of table with rules
+ CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean);
+ INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true);
+ CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl
+   WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id)
+   DO INSTEAD
+     UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id;
+ CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl
+   DO INSTEAD
+     UPDATE base_tbl SET deleted = true WHERE id = old.id;
+ CREATE VIEW rw_view1 WITH (security_barrier=true) AS
+   SELECT id, data FROM base_tbl WHERE NOT deleted;
+ SELECT * FROM rw_view1;
+  id | data  
+ ----+-------
+   1 | Row 1
+ (1 row)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+                                QUERY PLAN                                
+ -------------------------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Nested Loop
+          ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_1
+                Index Cond: (id = 1)
+          ->  Subquery Scan on base_tbl
+                Filter: snoop(base_tbl.data)
+                ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_2
+                      Index Cond: (id = 1)
+                      Filter: (NOT deleted)
+ (9 rows)
+ 
+ DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ NOTICE:  snooped value: Row 1
+ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Insert on base_tbl
+    InitPlan 1 (returns $0)
+      ->  Index Only Scan using base_tbl_pkey on base_tbl t
+            Index Cond: (id = 2)
+    ->  Result
+          One-Time Filter: ($0 IS NOT TRUE)
+  
+  Update on base_tbl
+    InitPlan 1 (returns $0)
+      ->  Index Only Scan using base_tbl_pkey on base_tbl t
+            Index Cond: (id = 2)
+    ->  Result
+          One-Time Filter: $0
+          ->  Index Scan using base_tbl_pkey on base_tbl
+                Index Cond: (id = 2)
+ (15 rows)
+ 
+ INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ SELECT * FROM base_tbl;
+  id |   data    | deleted 
+ ----+-----------+---------
+   1 | Row 1     | t
+   2 | New row 2 | f
+ (2 rows)
+ 
+ DROP TABLE base_tbl CASCADE;
+ NOTICE:  drop cascades to view rw_view1
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index a77cf19..08f7504
*** a/src/test/regress/sql/updatable_views.sql
--- b/src/test/regress/sql/updatable_views.sql
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 25,36 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
--- 25,34 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
*************** SELECT * FROM base_tbl;
*** 87,99 ****
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  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;
! UPDATE ro_view21 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view19;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
--- 85,96 ----
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  INSERT INTO ro_view17 VALUES (3, 'ROW 3');
! DELETE FROM ro_view18;
! UPDATE ro_view19 SET max_value=1000;
! UPDATE ro_view20 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
*************** CREATE VIEW rw_view2 AS
*** 828,830 ****
--- 825,931 ----
    SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION;
  INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check)
  DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view
+ 
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ 
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ 
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ -- security barrier view on top of security barrier view
+ 
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view on top of table with rules
+ 
+ CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean);
+ INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true);
+ 
+ CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl
+   WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id)
+   DO INSTEAD
+     UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id;
+ 
+ CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl
+   DO INSTEAD
+     UPDATE base_tbl SET deleted = true WHERE id = old.id;
+ 
+ CREATE VIEW rw_view1 WITH (security_barrier=true) AS
+   SELECT id, data FROM base_tbl WHERE NOT deleted;
+ 
+ SELECT * FROM rw_view1;
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ 
+ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ 
+ SELECT * FROM base_tbl;
+ 
+ DROP TABLE base_tbl CASCADE;
#53Craig Ringer
craig@2ndquadrant.com
In reply to: Dean Rasheed (#52)
Re: WIP patch (v2) for updatable security barrier views

On 01/31/2014 05:09 PM, Dean Rasheed wrote:

I don't like this fix --- you appear to be adding another RTE to the
rangetable (one not in the FROM list) and applying the rowmarks to it,
which seems wrong because you're not locking the right set of rows.
This is reflected in the change to the regression test output where,
in one of the tests, the ctids for the table to update are no longer
coming from the same table. I think a better approach is to push down
the rowmark into the subquery so that any locking required applies to
the pushed down RTE --- see the attached patch.

Thankyou.

I wasn't able to detect any changes in behaviour caused by the original
change other than the table alias change due to the additional RTE. The
new RTE referred to the same Relation, and there didn't seem to be any
problems caused by that.

Nonetheless, your fix seems a lot cleaner, especially in the target-view
case.

I've updated the commitfest app to show this patch.

Anyway, please test if this works with your RLS code.

It does. Thankyou.

I'm still working on the other issues I outlined yesterday, but that's
for the other thread.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#54Craig Ringer
craig@2ndquadrant.com
In reply to: Craig Ringer (#1)
Re: WIP patch (v2) for updatable security barrier views

I've found an issue with updatable security barrier views. Locking is
being pushed down into the subquery. Locking is thus applied before
user-supplied quals are, so we potentially lock too many rows.

I'm looking into the code now, but in the mean time, here's a demo of
the problem:

regress=> CREATE TABLE t1(x integer, y integer);
CREATE TABLE
regress=> INSERT INTO t1(x,y) VALUES (1,1), (2,2), (3,3), (4,4);
INSERT 0 4
regress=> CREATE VIEW v1 WITH (security_barrier) AS SELECT x, y FROM t1
WHERE x % 2 = 0;
CREATE VIEW
regress=> EXPLAIN SELECT * FROM v1 FOR UPDATE;
QUERY PLAN
-----------------------------------------------------------------------
LockRows (cost=0.00..42.43 rows=11 width=40)
-> Subquery Scan on v1 (cost=0.00..42.32 rows=11 width=40)
-> LockRows (cost=0.00..42.21 rows=11 width=14)
-> Seq Scan on t1 (cost=0.00..42.10 rows=11 width=14)
Filter: ((x % 2) = 0)
Planning time: 0.140 ms
(6 rows)

or, preventing pushdown with a wrapper function to demonstrate the problem:

regress=> CREATE FUNCTION is_one(integer) RETURNS boolean AS $$ DECLARE
result integer; BEGIN SELECT $1 = 1

regress=> EXPLAIN SELECT * FROM v1 WHERE is_one(x) FOR UPDATE;
QUERY PLAN
-----------------------------------------------------------------------
LockRows (cost=0.00..45.11 rows=4 width=40)
-> Subquery Scan on v1 (cost=0.00..45.07 rows=4 width=40)
Filter: is_one(v1.x)
-> LockRows (cost=0.00..42.21 rows=11 width=14)
-> Seq Scan on t1 (cost=0.00..42.10 rows=11 width=14)
Filter: ((x % 2) = 0)
Planning time: 0.147 ms
(7 rows)

OK, so it looks like the code:

/*
* Now deal with any PlanRowMark on this RTE by requesting a
lock
* of the same strength on the RTE copied down to the subquery.
*/
rc = get_plan_rowmark(root->rowMarks, rt_index);
if (rc != NULL)
{
switch (rc->markType)
{
/* .... */
}
root->rowMarks = list_delete(root->rowMarks, rc);
}

isn't actually appropriate. We should _not_ be pushing locking down into
the subquery.

Instead, we should be retargeting the rowmark so it points to the new
subquery RTE, marking rows _after_filtering. We want a plan like:

regress=> EXPLAIN SELECT * FROM v1 WHERE is_one(x) FOR UPDATE;
QUERY PLAN
-----------------------------------------------------------------------
LockRows (cost=0.00..45.11 rows=4 width=40)
-> Subquery Scan on v1 (cost=0.00..45.07 rows=4 width=40)
Filter: is_one(v1.x)
-> Seq Scan on t1 (cost=0.00..42.10 rows=11 width=14)
Filter: ((x % 2) = 0)
Planning time: 0.147 ms
(7 rows)

I'm not too sure what the best way to do that is. Time permitting I'll
see if I can work out the RowMark code and set something up.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

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

#55Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Craig Ringer (#54)
Re: WIP patch (v2) for updatable security barrier views

On 10 March 2014 03:36, Craig Ringer <craig@2ndquadrant.com> wrote:

I've found an issue with updatable security barrier views. Locking is
being pushed down into the subquery. Locking is thus applied before
user-supplied quals are, so we potentially lock too many rows.

I'm looking into the code now, but in the mean time, here's a demo of
the problem:

regress=> CREATE TABLE t1(x integer, y integer);
CREATE TABLE
regress=> INSERT INTO t1(x,y) VALUES (1,1), (2,2), (3,3), (4,4);
INSERT 0 4
regress=> CREATE VIEW v1 WITH (security_barrier) AS SELECT x, y FROM t1
WHERE x % 2 = 0;
CREATE VIEW
regress=> EXPLAIN SELECT * FROM v1 FOR UPDATE;
QUERY PLAN
-----------------------------------------------------------------------
LockRows (cost=0.00..42.43 rows=11 width=40)
-> Subquery Scan on v1 (cost=0.00..42.32 rows=11 width=40)
-> LockRows (cost=0.00..42.21 rows=11 width=14)
-> Seq Scan on t1 (cost=0.00..42.10 rows=11 width=14)
Filter: ((x % 2) = 0)
Planning time: 0.140 ms
(6 rows)

That has nothing to do with *updatable* security barrier views,
because that's not an update. In fact you get that exact same plan on
HEAD, without the updatable security barrier views patch.

or, preventing pushdown with a wrapper function to demonstrate the problem:

regress=> CREATE FUNCTION is_one(integer) RETURNS boolean AS $$ DECLARE
result integer; BEGIN SELECT $1 = 1

regress=> EXPLAIN SELECT * FROM v1 WHERE is_one(x) FOR UPDATE;
QUERY PLAN
-----------------------------------------------------------------------
LockRows (cost=0.00..45.11 rows=4 width=40)
-> Subquery Scan on v1 (cost=0.00..45.07 rows=4 width=40)
Filter: is_one(v1.x)
-> LockRows (cost=0.00..42.21 rows=11 width=14)
-> Seq Scan on t1 (cost=0.00..42.10 rows=11 width=14)
Filter: ((x % 2) = 0)
Planning time: 0.147 ms
(7 rows)

OK, so it looks like the code:

/*
* Now deal with any PlanRowMark on this RTE by requesting a
lock
* of the same strength on the RTE copied down to the subquery.
*/
rc = get_plan_rowmark(root->rowMarks, rt_index);
if (rc != NULL)
{
switch (rc->markType)
{
/* .... */
}
root->rowMarks = list_delete(root->rowMarks, rc);
}

isn't actually appropriate. We should _not_ be pushing locking down into
the subquery.

That code isn't being invoked in this test case because you're just
selecting from the view, so it's the normal view expansion code, not
the new securityQuals expansion code.

Instead, we should be retargeting the rowmark so it points to the new
subquery RTE, marking rows _after_filtering. We want a plan like:

regress=> EXPLAIN SELECT * FROM v1 WHERE is_one(x) FOR UPDATE;
QUERY PLAN
-----------------------------------------------------------------------
LockRows (cost=0.00..45.11 rows=4 width=40)
-> Subquery Scan on v1 (cost=0.00..45.07 rows=4 width=40)
Filter: is_one(v1.x)
-> Seq Scan on t1 (cost=0.00..42.10 rows=11 width=14)
Filter: ((x % 2) = 0)
Planning time: 0.147 ms
(7 rows)

I'm not too sure what the best way to do that is. Time permitting I'll
see if I can work out the RowMark code and set something up.

Agreed, that seems like it would be a saner plan, but the place to
look to fix it isn't in the updatable security barriers view patch.
Perhaps the updatable security barriers view patch suffers from the
same problem, but first I'd like to know what that problem is in HEAD.

Regards,
Dean

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

#56Tom Lane
tgl@sss.pgh.pa.us
In reply to: Dean Rasheed (#55)
Re: WIP patch (v2) for updatable security barrier views

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

On 10 March 2014 03:36, Craig Ringer <craig@2ndquadrant.com> wrote:

I've found an issue with updatable security barrier views. Locking is
being pushed down into the subquery. Locking is thus applied before
user-supplied quals are, so we potentially lock too many rows.

That has nothing to do with *updatable* security barrier views,
because that's not an update. In fact you get that exact same plan on
HEAD, without the updatable security barrier views patch.

I got around to looking at this. The proximate reason for the two
LockRows nodes is:

(1) The parser and rewriter intentionally insert RowMarkClauses into
both the outer query and the subquery when a FOR UPDATE/SHARE applies
to a subquery-in-FROM. The comment in transformLockingClause explains
why:

* FOR UPDATE/SHARE of subquery is propagated to all of
* subquery's rels, too. We could do this later (based on
* the marking of the subquery RTE) but it is convenient
* to have local knowledge in each query level about which
* rels need to be opened with RowShareLock.

that is, if we didn't push down the RowMarkClause, processing of the
subquery would be at risk of opening relations with too weak a lock.
In the example case, this pushdown is actually done by the rewriter's
markQueryForLocking function, but it's just emulating what the parser
would have done if the view query had been written in-line.

(2) The planner doesn't consider this, and generates a LockRows plan node
for each level of the query, since both of them have rowMarks.

However, I'm not sure this is a bug, or at least it's not the same bug you
think it is. The lower LockRows node is doing a ROW_MARK_EXCLUSIVE mark
on the physical table rows, while the upper one is doing a ROW_MARK_COPY
on the virtual rows emitted by the subquery. *These are not the same
thing*. A ROW_MARK_COPY isn't a lock of any sort; it's just there to
allow EvalPlanQual to see the row that was emitted from the subquery,
in case a recheck has to be done during a Read Committed UPDATE/DELETE.
Since this is a pure SELECT, the upper "locking" action is useless, and
arguably the planner ought to be smart enough not to emit it. But it's
just wasting some cycles, it's not changing any semantics.

So if you wanted user-supplied quals to limit which rows get locked
physically, they would need to be applied before the lower LockRows node.

To my mind, it's not immediately apparent that that is a reasonable
expectation. The entire *point* of a security_barrier view is that
unsafe quals don't get pushed into it, so why would you expect that
those quals get applied early enough to limit locking?

regards, tom lane

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

#57Stephen Frost
sfrost@snowman.net
In reply to: Tom Lane (#56)
Re: WIP patch (v2) for updatable security barrier views

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

Dean Rasheed <dean.a.rasheed@gmail.com> writes:

On 10 March 2014 03:36, Craig Ringer <craig@2ndquadrant.com> wrote:

I've found an issue with updatable security barrier views. Locking is
being pushed down into the subquery. Locking is thus applied before
user-supplied quals are, so we potentially lock too many rows.

That has nothing to do with *updatable* security barrier views,
because that's not an update. In fact you get that exact same plan on
HEAD, without the updatable security barrier views patch.

I got around to looking at this.

Thanks!

So if you wanted user-supplied quals to limit which rows get locked
physically, they would need to be applied before the lower LockRows node.

Right.

To my mind, it's not immediately apparent that that is a reasonable
expectation. The entire *point* of a security_barrier view is that
unsafe quals don't get pushed into it, so why would you expect that
those quals get applied early enough to limit locking?

Right, we don't want unsafe quals to get pushed down below the
security_barrier view. The point here is that those quals should not be
able to lock rows which they don't even see.

My understanding of the issue was that the unsafe quals were being able
to see and lock rows that they shouldn't know exist. If that's not the
case then I don't think there's a bug here..? Craig/Dean, can you
provide a complete test case which shows the issue?

Thanks,

Stephen

#58Tom Lane
tgl@sss.pgh.pa.us
In reply to: Stephen Frost (#57)
Re: WIP patch (v2) for updatable security barrier views

Stephen Frost <sfrost@snowman.net> writes:

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

So if you wanted user-supplied quals to limit which rows get locked
physically, they would need to be applied before the lower LockRows node.

Right.

To my mind, it's not immediately apparent that that is a reasonable
expectation. The entire *point* of a security_barrier view is that
unsafe quals don't get pushed into it, so why would you expect that
those quals get applied early enough to limit locking?

Right, we don't want unsafe quals to get pushed down below the
security_barrier view. The point here is that those quals should not be
able to lock rows which they don't even see.

That sounds a bit confused. The quals aren't getting to see rows they
shouldn't be allowed to see. The issue rather is whether rows that the
user quals would exclude might get locked anyway because the locking
happens before those quals are checked.

[ thinks for awhile... ] I guess the thing that is surprising is that
ordinarily, if you say SELECT ... WHERE ... FOR UPDATE, only rows passing
the WHERE clause get locked. If there's a security view involved, that
gets reversed (at least for unsafe clauses). So from that standpoint
it does seem pretty bogus. I'm not sure how much we can do about it
given the current implementation technique for security views. A physical
row lock requires access to the source relation and the ctid column,
neither of which are visible at all outside an un-flattened subquery.

Anyway, this is definitely a pre-existing issue with security_barrier
views (or really with any unflattenable subquery), and so I'm not sure
if it has much to do with the patch at hand.

regards, tom lane

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

#59Stephen Frost
sfrost@snowman.net
In reply to: Tom Lane (#58)
Re: WIP patch (v2) for updatable security barrier views

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

That sounds a bit confused.

It was- I clearly hadn't followed the thread entirely.

The quals aren't getting to see rows they
shouldn't be allowed to see. The issue rather is whether rows that the
user quals would exclude might get locked anyway because the locking
happens before those quals are checked.

This, imv, moves this from a 'major' bug to more of a 'performance' or
'obnoxious' side-effect issue that we should address *eventually*, but
not something to hold up the RLS implementation. We often have more
locking than is strictly required and then reduce it later (see also:
ALTER TABLE lock reduction thread). It will be an unfortunate "gotcha"
for a release or two, but hopefully we'll be able to address it...

[ thinks for awhile... ] I guess the thing that is surprising is that
ordinarily, if you say SELECT ... WHERE ... FOR UPDATE, only rows passing
the WHERE clause get locked. If there's a security view involved, that
gets reversed (at least for unsafe clauses). So from that standpoint
it does seem pretty bogus. I'm not sure how much we can do about it
given the current implementation technique for security views. A physical
row lock requires access to the source relation and the ctid column,
neither of which are visible at all outside an un-flattened subquery.

Interesting. I'm trying to reason out why we don't have it already in
similar situations; we can't *always* flatten a subquery... Updatable
security_barrier views and RLS may make this a more promenint issue but
hopefully that'll just encourage work on fixing it (perhaps take the
higher level lock and then figure out a way to drop it when the rows
don't match the later quals..?).

Anyway, this is definitely a pre-existing issue with security_barrier
views (or really with any unflattenable subquery), and so I'm not sure
if it has much to do with the patch at hand.

Agreed.

Thanks!

Stephen

#60Tom Lane
tgl@sss.pgh.pa.us
In reply to: Stephen Frost (#59)
Re: WIP patch (v2) for updatable security barrier views

Stephen Frost <sfrost@snowman.net> writes:

Interesting. I'm trying to reason out why we don't have it already in
similar situations; we can't *always* flatten a subquery...

I think we do, just nobody's particularly noticed (which further reduces
the urgency of finding a solution, I suppose).

regards, tom lane

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

#61Gregory Smith
gregsmithpgsql@gmail.com
In reply to: Stephen Frost (#59)
Re: WIP patch (v2) for updatable security barrier views

On 4/9/14 9:56 PM, Stephen Frost wrote:

As for docs and testing, those are things we would certainly be better
off with and may mean that this isn't able to make it into 9.4, which is
fair, but I wouldn't toss it out solely due to that.

We have a git repo with multiple worked out code examples, ones that
have been in active testing for months now. It's private just to reduce
mistakes as things are cleared for public consumption, but I (and Mark
and Jeff here) can pull anything we like from it to submit to hackers.
There's also a test case set from Yeb Havinga he used for his review.

I expected to turn at least one of those into a Postgres regression
test. The whole thing squealed to a stop when the regression tests
Craig was working on were raising multiple serious questions. I didn't
see much sense in bundling more boring, passing tests when the ones we
already had seemed off--and no one was sure why.

Now that Tom has given some guidance on the row locking weirdness, maybe
it's time for me to start updating those into make check form. The
documentation has been in a similar holding pattern. I have lots of
resources to help document what does and doesn't work here to the
quality expected in the manual. I just need a little more confidence
about which feature set is commit worthy. The documentation that makes
sense is very different if only updatable security barrier views is
committed.

--
Greg Smith greg.smith@crunchydatasolutions.com
Chief PostgreSQL Evangelist - http://crunchydatasolutions.com/

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

#62Stephen Frost
sfrost@snowman.net
In reply to: Dean Rasheed (#52)
Re: WIP patch (v2) for updatable security barrier views

Dean, Craig, all,

* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:

This is reflected in the change to the regression test output where,
in one of the tests, the ctids for the table to update are no longer
coming from the same table. I think a better approach is to push down
the rowmark into the subquery so that any locking required applies to
the pushed down RTE --- see the attached patch.

I'm working through this patch and came across a few places where I
wanted to ask questions (as much for my own edification as questioning
the actual implementation). Also, feel free to point out if I'm
bringing up something which has already been discussed. I've been
trying to follow the discussion but it's been a while and my memory may
have faded.

While in the planner, we need to address the case of a child RTE which
has been transformed from a relation to a subquery. That all makes
perfect sense, but I'm wondering if it'd be better to change this
conditional:

! if (rte1->rtekind == RTE_RELATION &&
! rte1->securityQuals != NIL &&
! rte2->rtekind == RTE_SUBQUERY)

which essentially says "if a relation was changed to a subquery *and*
it has security quals then we need to update the entry" into one like
this:

! if (rte1->rtekind == RTE_RELATION &&
! rte2->rtekind == RTE_SUBQUERY)
! {
! Assert(rte1->securityQuals != NIL);
! ...

which changes it to "if a relation was changed to a subquery, it had
better be because it's got securityQuals that we're dealing with". My
general thinking here is that we'd be better off with the Assert()
firing during some later development which changes things in this area
than skipping the change because there aren't any securityQuals and then
expecting everything to be fine with the subquery actually being a
relation..

I could see flipping that around too, to check if there are
securityQuals and then Assert() on the change from relation to subquery-
after all, if there are securityQuals then it *must* be a subquery,
right?

A similar case exists in prepunion.c where we're checking if we should
recurse while in adjust_appendrel_attrs_mutator()- the check exists as

! if (IsA(node, Query))

(... which used to be an Assert(!IsA(node, Query)) ...)

but the comment is then quite clear that we should only be doing this in
the security-barrier case; perhaps we should Assert() there to that
effect? It'd certainly make me feel a bit better about removing the two
Asserts() which were there; presumably we had to also remove the
Assert(!IsA(node, SubLink)) ?

Also, it seems like there should be a check_stack_depth() call here now?

That covers more-or-less everything outside of prepsecurity.c itself.
I'm planning to review that tomorrow night. In general, I'm pretty
happy with the shape of this. The wiki and earlier discussions were
quite useful. My one complaint about this is that it feels like a few
more comments and more documentation updates would be warrented; and in
particular we need to make note of the locking "gotcha" in the docs.
That's not a "solution", of course, but since we know about it we should
probably make sure users are aware.

Thanks,

Stephen

#63Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Stephen Frost (#62)
Re: WIP patch (v2) for updatable security barrier views

On 11 April 2014 04:04, Stephen Frost <sfrost@snowman.net> wrote:

Dean, Craig, all,

* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:

This is reflected in the change to the regression test output where,
in one of the tests, the ctids for the table to update are no longer
coming from the same table. I think a better approach is to push down
the rowmark into the subquery so that any locking required applies to
the pushed down RTE --- see the attached patch.

I'm working through this patch and came across a few places where I
wanted to ask questions (as much for my own edification as questioning
the actual implementation). Also, feel free to point out if I'm
bringing up something which has already been discussed. I've been
trying to follow the discussion but it's been a while and my memory may
have faded.

Thanks for looking at this.

While in the planner, we need to address the case of a child RTE which
has been transformed from a relation to a subquery. That all makes
perfect sense, but I'm wondering if it'd be better to change this
conditional:

! if (rte1->rtekind == RTE_RELATION &&
! rte1->securityQuals != NIL &&
! rte2->rtekind == RTE_SUBQUERY)

which essentially says "if a relation was changed to a subquery *and*
it has security quals then we need to update the entry" into one like
this:

! if (rte1->rtekind == RTE_RELATION &&
! rte2->rtekind == RTE_SUBQUERY)
! {
! Assert(rte1->securityQuals != NIL);
! ...

which changes it to "if a relation was changed to a subquery, it had
better be because it's got securityQuals that we're dealing with". My
general thinking here is that we'd be better off with the Assert()
firing during some later development which changes things in this area
than skipping the change because there aren't any securityQuals and then
expecting everything to be fine with the subquery actually being a
relation..

Yes, I think that makes sense, and it seems like a sensible check.

I could see flipping that around too, to check if there are
securityQuals and then Assert() on the change from relation to subquery-
after all, if there are securityQuals then it *must* be a subquery,
right?

No, because a relation with securityQuals is only changed to a
subquery if it is actually used in the main query (see the check in
expand_security_quals). During the normal course of events when
working with an inheritance hierarchy, there are RTEs for other
children that aren't referred to in the main query for the current
inheritance child, and those RTEs are not expanded into subqueries.

A similar case exists in prepunion.c where we're checking if we should
recurse while in adjust_appendrel_attrs_mutator()- the check exists as

! if (IsA(node, Query))

(... which used to be an Assert(!IsA(node, Query)) ...)

but the comment is then quite clear that we should only be doing this in
the security-barrier case; perhaps we should Assert() there to that
effect? It'd certainly make me feel a bit better about removing the two
Asserts() which were there; presumably we had to also remove the
Assert(!IsA(node, SubLink)) ?

When I originally wrote those comments, I thought that this change
would only apply to securityQuals. Later, following a seemingly
unrelated bug involving UPDATEs containing LATERAL references to the
target relation (which is now disallowed), I thought that this change
might help with that case too, if such UPDATEs were to be allowed
again in the future
(/messages/by-id/CAEZATCXpOsF5wZ1XXWQur7G5M52=MwzUaqYE9b0RgqhXvw34Pw@mail.gmail.com).

For now though, the comments are correct, and it can only happen with
securityQuals. Adding an Assert() to that effect might be a bit fiddly
though, because the information required isn't available on the local
Node being processed, so I think it would have to add and maintain an
additional flag on the mutator context. I'm not sure that it's worth
it.

Also, it seems like there should be a check_stack_depth() call here now?

Hm, perhaps. I don't see any such checks in the rewriter though, and I
wouldn't expect the call depth here to get any deeper than it had
previously done there.

That covers more-or-less everything outside of prepsecurity.c itself.
I'm planning to review that tomorrow night. In general, I'm pretty
happy with the shape of this. The wiki and earlier discussions were
quite useful. My one complaint about this is that it feels like a few
more comments and more documentation updates would be warrented; and in
particular we need to make note of the locking "gotcha" in the docs.
That's not a "solution", of course, but since we know about it we should
probably make sure users are aware.

Am I right in thinking that the "locking gotcha" only happens if you
create a security_barrier view conaining a "SELECT ... FOR UPDATE"? If
so, that seems like rather a niche case - not that that means we
shouldn't warn people about it.

Thanks again for looking at this.

Regards,
Dean

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

#64Stephen Frost
sfrost@snowman.net
In reply to: Dean Rasheed (#63)
Re: WIP patch (v2) for updatable security barrier views

Dean,

* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:

On 11 April 2014 04:04, Stephen Frost <sfrost@snowman.net> wrote:

which changes it to "if a relation was changed to a subquery, it had
better be because it's got securityQuals that we're dealing with". My
general thinking here is that we'd be better off with the Assert()
firing during some later development which changes things in this area
than skipping the change because there aren't any securityQuals and then
expecting everything to be fine with the subquery actually being a
relation..

Yes, I think that makes sense, and it seems like a sensible check.

Ok, I'll change that.

I could see flipping that around too, to check if there are
securityQuals and then Assert() on the change from relation to subquery-
after all, if there are securityQuals then it *must* be a subquery,
right?

No, because a relation with securityQuals is only changed to a
subquery if it is actually used in the main query (see the check in
expand_security_quals). During the normal course of events when
working with an inheritance hierarchy, there are RTEs for other
children that aren't referred to in the main query for the current
inheritance child, and those RTEs are not expanded into subqueries.

Ah, yes, makes sense.

For now though, the comments are correct, and it can only happen with
securityQuals. Adding an Assert() to that effect might be a bit fiddly
though, because the information required isn't available on the local
Node being processed, so I think it would have to add and maintain an
additional flag on the mutator context. I'm not sure that it's worth
it.

Yeah, I was afraid that might be the case. No problem.

Also, it seems like there should be a check_stack_depth() call here now?

Hm, perhaps. I don't see any such checks in the rewriter though, and I
wouldn't expect the call depth here to get any deeper than it had
previously done there.

Hmm, ok. I'll think on that one.

That covers more-or-less everything outside of prepsecurity.c itself.
I'm planning to review that tomorrow night. In general, I'm pretty
happy with the shape of this. The wiki and earlier discussions were
quite useful. My one complaint about this is that it feels like a few
more comments and more documentation updates would be warrented; and in
particular we need to make note of the locking "gotcha" in the docs.
That's not a "solution", of course, but since we know about it we should
probably make sure users are aware.

Am I right in thinking that the "locking gotcha" only happens if you
create a security_barrier view conaining a "SELECT ... FOR UPDATE"? If
so, that seems like rather a niche case - not that that means we
shouldn't warn people about it.

Hmm, the 'gotcha' I was referring to was the issue discussed upthread
around rows getting locked to be updated which didn't pass all the quals
(they passed the security_barrier view's, but not the user-supplied
ones), which could happen during a normal 'update' against a
security_barrier view, right? I didn't think that would require the
view definition to be 'FOR UPDATE'; if that's required then it would
seem like we're actually doing what the user expects based on their view
definition..

Thanks,

Stephen

#65Craig Ringer
craig@2ndquadrant.com
In reply to: Stephen Frost (#64)
Re: WIP patch (v2) for updatable security barrier views

(Sorry if this breaks the thread history; on mobile)

Am I right in thinking that the "locking gotcha" only happens if you
create a security_barrier view conaining a "SELECT ... FOR UPDATE"? If
so, that seems like rather a niche case - not that that means we
shouldn't warn people about it.

Hmm, the 'gotcha' I was referring to was the issue discussed upthread
around rows getting locked to be updated which didn't pass all the quals
(they passed the security_barrier view's, but not the user-supplied
ones), which could happen during a normal 'update' against a
security_barrier view, right?  I didn't think that would require the
view definition to be 'FOR UPDATE';

It doesn't require the view to be defined FOR UPDATE.

I'll try to write an isolstiontester case to donstrate this on the weekend.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#66Stephen Frost
sfrost@snowman.net
In reply to: Craig Ringer (#65)
Re: WIP patch (v2) for updatable security barrier views

* Craig Ringer (craig@2ndquadrant.com) wrote:

Hmm, the 'gotcha' I was referring to was the issue discussed upthread
around rows getting locked to be updated which didn't pass all the quals
(they passed the security_barrier view's, but not the user-supplied
ones), which could happen during a normal 'update' against a
security_barrier view, right?  I didn't think that would require the
view definition to be 'FOR UPDATE';

It doesn't require the view to be defined FOR UPDATE.

Ok, great, glad I got that correct. :)

I'll try to write an isolstiontester case to donstrate this on the weekend.

Great, thanks. I'll take a stab at writing up the 'gotcha' note tonight
or tomorrow.

Thanks again,

Stephen

#67Tom Lane
tgl@sss.pgh.pa.us
In reply to: Stephen Frost (#64)
Re: WIP patch (v2) for updatable security barrier views

Stephen Frost <sfrost@snowman.net> writes:

* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:

Am I right in thinking that the "locking gotcha" only happens if you
create a security_barrier view conaining a "SELECT ... FOR UPDATE"? If
so, that seems like rather a niche case - not that that means we
shouldn't warn people about it.

Hmm, the 'gotcha' I was referring to was the issue discussed upthread
around rows getting locked to be updated which didn't pass all the quals
(they passed the security_barrier view's, but not the user-supplied
ones), which could happen during a normal 'update' against a
security_barrier view, right? I didn't think that would require the
view definition to be 'FOR UPDATE'; if that's required then it would
seem like we're actually doing what the user expects based on their view
definition..

Yeah, the point of the "gotcha" is that a FOR UPDATE specified *outside* a
security-barrier view would act as though it had appeared *inside* the
view, since it effectively gets pushed down even though outer quals don't.

regards, tom lane

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

#68Stephen Frost
sfrost@snowman.net
In reply to: Dean Rasheed (#63)
1 attachment(s)
Re: WIP patch (v2) for updatable security barrier views

Dean, Craig, all,

* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:

On 11 April 2014 04:04, Stephen Frost <sfrost@snowman.net> wrote:

which changes it to "if a relation was changed to a subquery, it had
better be because it's got securityQuals that we're dealing with". My
general thinking here is that we'd be better off with the Assert()
firing during some later development which changes things in this area
than skipping the change because there aren't any securityQuals and then
expecting everything to be fine with the subquery actually being a
relation..

Yes, I think that makes sense, and it seems like a sensible check.

I've made this change and updated the comments a bit.

For now though, the comments are correct, and it can only happen with
securityQuals. Adding an Assert() to that effect might be a bit fiddly
though, because the information required isn't available on the local
Node being processed, so I think it would have to add and maintain an
additional flag on the mutator context. I'm not sure that it's worth
it.

Added a comment to that effect.

Also, it seems like there should be a check_stack_depth() call here now?

Hm, perhaps. I don't see any such checks in the rewriter though, and I
wouldn't expect the call depth here to get any deeper than it had
previously done there.

I didn't do this. I tend to agree with you, though it'd be interesting
to see if it's possible to break things with enough levels... I suspect
that if we have a problem there though that it's in existing releases.

Am I right in thinking that the "locking gotcha" only happens if you
create a security_barrier view conaining a "SELECT ... FOR UPDATE"? If
so, that seems like rather a niche case - not that that means we
shouldn't warn people about it.

I've added both C-level comments and a new paragraph to the 'updatable
views' area of the documentation which attempt to address the issue
here- please review and comment.

I also went over prepsecurity.c and the regression tests and didn't
really find much to complain about there. I should be able to look at
the RLS patch tomorrow.

Thanks,

Stephen

Attachments:

updatable-sb-views_sf.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
new file mode 100644
index 57bfae3..3224d9f 100644
*** a/doc/src/sgml/ref/create_view.sgml
--- b/doc/src/sgml/ref/create_view.sgml
*************** CREATE VIEW vista AS SELECT text 'Hello
*** 323,334 ****
         or set-returning functions.
        </para>
       </listitem>
- 
-      <listitem>
-       <para>
-        The view must not have the <literal>security_barrier</> property.
-       </para>
-      </listitem>
      </itemizedlist>
     </para>
  
--- 323,328 ----
*************** CREATE VIEW vista AS SELECT text 'Hello
*** 362,367 ****
--- 356,373 ----
     </para>
  
     <para>
+     If an automatically updatable view is marked with the
+     <literal>security_barrier</> property then all the view's <literal>WHERE</>
+     conditions (and any conditions using operators which are marked as LEAKPROOF)
+     will always be evaluated before any conditions that a user of the view has
+     added.   See <xref linkend="rules-privileges"> for full details.  Note that,
+     due to this, rows which are not ultimately returned (because they do not
+     pass the user's <literal>WHERE</> conditions) may still end up being locked.
+     <command>EXPLAIN</command> can be used to see which conditions are
+     applied before the LockRows and which are not, for a given query.
+    </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
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
new file mode 100644
index 8f9e5e5..f5ae98f 100644
*** a/src/backend/commands/tablecmds.c
--- b/src/backend/commands/tablecmds.c
*************** ATExecSetRelOptions(Relation rel, List *
*** 8910,8916 ****
  		List	   *view_options = untransformRelOptions(newOptions);
  		ListCell   *cell;
  		bool		check_option = false;
- 		bool		security_barrier = false;
  
  		foreach(cell, view_options)
  		{
--- 8910,8915 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8918,8925 ****
  
  			if (pg_strcasecmp(defel->defname, "check_option") == 0)
  				check_option = true;
- 			if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 				security_barrier = defGetBoolean(defel);
  		}
  
  		/*
--- 8917,8922 ----
*************** ATExecSetRelOptions(Relation rel, List *
*** 8929,8936 ****
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query,
! 											 security_barrier, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
--- 8926,8932 ----
  		if (check_option)
  		{
  			const char *view_updatable_error =
! 				view_query_is_auto_updatable(view_query, true);
  
  			if (view_updatable_error)
  				ereport(ERROR,
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
new file mode 100644
index 1735762..bc08566 100644
*** a/src/backend/commands/view.c
--- b/src/backend/commands/view.c
*************** DefineView(ViewStmt *stmt, const char *q
*** 396,402 ****
  	RangeVar   *view;
  	ListCell   *cell;
  	bool		check_option;
- 	bool		security_barrier;
  
  	/*
  	 * Run parse analysis to convert the raw parse tree to a Query.  Note this
--- 396,401 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 451,457 ****
  	 * specified.
  	 */
  	check_option = false;
- 	security_barrier = false;
  
  	foreach(cell, stmt->options)
  	{
--- 450,455 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 459,466 ****
  
  		if (pg_strcasecmp(defel->defname, "check_option") == 0)
  			check_option = true;
- 		if (pg_strcasecmp(defel->defname, "security_barrier") == 0)
- 			security_barrier = defGetBoolean(defel);
  	}
  
  	/*
--- 457,462 ----
*************** DefineView(ViewStmt *stmt, const char *q
*** 470,476 ****
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, security_barrier, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
--- 466,472 ----
  	if (check_option)
  	{
  		const char *view_updatable_error =
! 			view_query_is_auto_updatable(viewParse, true);
  
  		if (view_updatable_error)
  			ereport(ERROR,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index c89d808..98ad910 100644
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyRangeTblEntry(const RangeTblEntry *
*** 1998,2003 ****
--- 1998,2004 ----
  	COPY_SCALAR_FIELD(checkAsUser);
  	COPY_BITMAPSET_FIELD(selectedCols);
  	COPY_BITMAPSET_FIELD(modifiedCols);
+ 	COPY_NODE_FIELD(securityQuals);
  
  	return newnode;
  }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 9793cf5..9901d23 100644
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblEntry(const RangeTblEntry
*** 2296,2301 ****
--- 2296,2302 ----
  	COMPARE_SCALAR_FIELD(checkAsUser);
  	COMPARE_BITMAPSET_FIELD(selectedCols);
  	COMPARE_BITMAPSET_FIELD(modifiedCols);
+ 	COMPARE_NODE_FIELD(securityQuals);
  
  	return true;
  }
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index 123f2a6..1e48a7f 100644
*** a/src/backend/nodes/nodeFuncs.c
--- b/src/backend/nodes/nodeFuncs.c
*************** range_table_walker(List *rtable,
*** 2020,2025 ****
--- 2020,2028 ----
  					return true;
  				break;
  		}
+ 
+ 		if (walker(rte->securityQuals, context))
+ 			return true;
  	}
  	return false;
  }
*************** range_table_mutator(List *rtable,
*** 2755,2760 ****
--- 2758,2764 ----
  				MUTATE(newrte->values_lists, rte->values_lists, List *);
  				break;
  		}
+ 		MUTATE(newrte->securityQuals, rte->securityQuals, List *);
  		newrt = lappend(newrt, newrte);
  	}
  	return newrt;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index bfb4b9f..10e8139 100644
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outRangeTblEntry(StringInfo str, const
*** 2409,2414 ****
--- 2409,2415 ----
  	WRITE_OID_FIELD(checkAsUser);
  	WRITE_BITMAPSET_FIELD(selectedCols);
  	WRITE_BITMAPSET_FIELD(modifiedCols);
+ 	WRITE_NODE_FIELD(securityQuals);
  }
  
  static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index 216d75e..ef1eae9 100644
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readRangeTblEntry(void)
*** 1252,1257 ****
--- 1252,1258 ----
  	READ_OID_FIELD(checkAsUser);
  	READ_BITMAPSET_FIELD(selectedCols);
  	READ_BITMAPSET_FIELD(modifiedCols);
+ 	READ_NODE_FIELD(securityQuals);
  
  	READ_DONE();
  }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 35bda67..0508d16 100644
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 916,921 ****
--- 916,927 ----
  		subplan = grouping_planner(&subroot, 0.0 /* retrieve all tuples */ );
  
  		/*
+ 		 * Planning may have modified the query result relation (if there
+ 		 * were security barrier quals on the result RTE).
+ 		 */
+ 		appinfo->child_relid = subroot.parse->resultRelation;
+ 
+ 		/*
  		 * If this child rel was excluded by constraint exclusion, exclude it
  		 * from the result plan.
  		 */
*************** inheritance_planner(PlannerInfo *root)
*** 932,940 ****
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 			final_rtable = list_concat(final_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
--- 938,977 ----
  		if (final_rtable == NIL)
  			final_rtable = subroot.parse->rtable;
  		else
! 		{
! 			List	   *tmp_rtable = NIL;
! 			ListCell   *cell1, *cell2;
! 
! 			/*
! 			 * Check to see if any of the original RTEs were turned into
! 			 * subqueries during planning.  Currently, this should only ever
! 			 * happen due to securityQuals being involved which push a
! 			 * relation down under a subquery, to ensure that the security
! 			 * barrier quals are evaluated first.
! 			 *
! 			 * When this happens, we want to use the new subqueries in the
! 			 * final rtable.
! 			 */
! 			forboth(cell1, final_rtable, cell2, subroot.parse->rtable)
! 			{
! 				RangeTblEntry *rte1 = (RangeTblEntry *) lfirst(cell1);
! 				RangeTblEntry *rte2 = (RangeTblEntry *) lfirst(cell2);
! 
! 				if (rte1->rtekind == RTE_RELATION &&
! 					rte2->rtekind == RTE_SUBQUERY)
! 				{
! 					/* Should only be when there are securityQuals today */
! 					Assert(rte1->securityQuals != NIL);
! 					tmp_rtable = lappend(tmp_rtable, rte2);
! 				}
! 				else
! 					tmp_rtable = lappend(tmp_rtable, rte1);
! 			}
! 
! 			final_rtable = list_concat(tmp_rtable,
  									   list_copy_tail(subroot.parse->rtable,
  												 list_length(final_rtable)));
+ 		}
  
  		/*
  		 * We need to collect all the RelOptInfos from all child plans into
*************** grouping_planner(PlannerInfo *root, doub
*** 1163,1168 ****
--- 1200,1211 ----
  		tlist = preprocess_targetlist(root, tlist);
  
  		/*
+ 		 * Expand any rangetable entries that have security barrier quals.
+ 		 * This may add new security barrier subquery RTEs to the rangetable.
+ 		 */
+ 		expand_security_quals(root, tlist);
+ 
+ 		/*
  		 * Locate any window functions in the tlist.  (We don't need to look
  		 * anywhere else, since expressions used in ORDER BY will be in there
  		 * too.)  Note that they could all have been eliminated by constant
diff --git a/src/backend/optimizer/prep/Makefile b/src/backend/optimizer/prep/Makefile
new file mode 100644
index 86301bf..5195d9b 100644
*** a/src/backend/optimizer/prep/Makefile
--- b/src/backend/optimizer/prep/Makefile
*************** subdir = src/backend/optimizer/prep
*** 12,17 ****
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
--- 12,17 ----
  top_builddir = ../../../..
  include $(top_builddir)/src/Makefile.global
  
! OBJS = prepjointree.o prepqual.o prepsecurity.o preptlist.o prepunion.o
  
  include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c
new file mode 100644
index ...7daaa33 .
*** a/src/backend/optimizer/prep/prepsecurity.c
--- b/src/backend/optimizer/prep/prepsecurity.c
***************
*** 0 ****
--- 1,466 ----
+ /*-------------------------------------------------------------------------
+  *
+  * prepsecurity.c
+  *	  Routines for preprocessing security barrier quals.
+  *
+  * Portions Copyright (c) 1996-2013, PostgreSQL Global Development Group
+  * Portions Copyright (c) 1994, Regents of the University of California
+  *
+  *
+  * IDENTIFICATION
+  *	  src/backend/optimizer/prep/prepsecurity.c
+  *
+  *-------------------------------------------------------------------------
+  */
+ #include "postgres.h"
+ 
+ #include "access/heapam.h"
+ #include "access/sysattr.h"
+ #include "catalog/heap.h"
+ #include "nodes/makefuncs.h"
+ #include "nodes/nodeFuncs.h"
+ #include "optimizer/prep.h"
+ #include "parser/analyze.h"
+ #include "parser/parsetree.h"
+ #include "rewrite/rewriteManip.h"
+ #include "utils/rel.h"
+ 
+ 
+ typedef struct
+ {
+ 	int			rt_index;		/* Index of security barrier RTE */
+ 	int			sublevels_up;	/* Current nesting depth */
+ 	Relation	rel;			/* RTE relation at rt_index */
+ 	List	   *targetlist;		/* Targetlist for new subquery RTE */
+ 	List	   *colnames;		/* Column names in subquery RTE */
+ 	List	   *vars_processed;	/* List of Vars already processed */
+ } security_barrier_replace_vars_context;
+ 
+ static void expand_security_qual(PlannerInfo *root, List *tlist, int rt_index,
+ 					 RangeTblEntry *rte, Node *qual);
+ 
+ static void security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context);
+ 
+ static bool security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context);
+ 
+ 
+ /*
+  * expand_security_quals -
+  *	  expands any security barrier quals on RTEs in the query rtable, turning
+  *	  them into security barrier subqueries.
+  *
+  * Any given RTE may have multiple security barrier quals in a list, from which
+  * we create a set of nested subqueries to isolate each security barrier from
+  * the others, providing protection against malicious user-defined security
+  * barriers.  The first security barrier qual in the list will be used in the
+  * innermost subquery.
+  */
+ void
+ expand_security_quals(PlannerInfo *root, List *tlist)
+ {
+ 	Query	   *parse = root->parse;
+ 	int			rt_index;
+ 	ListCell   *cell;
+ 
+ 	/*
+ 	 * Process each RTE in the rtable list.
+ 	 *
+ 	 * We only ever modify entries in place and append to the rtable, so it is
+ 	 * safe to use a foreach loop here.
+ 	 */
+ 	rt_index = 0;
+ 	foreach(cell, parse->rtable)
+ 	{
+ 		RangeTblEntry *rte = (RangeTblEntry *) lfirst(cell);
+ 
+ 		rt_index++;
+ 
+ 		if (rte->securityQuals == NIL)
+ 			continue;
+ 
+ 		/*
+ 		 * Ignore any RTEs that aren't used in the query (such RTEs may be
+ 		 * present for permissions checks).
+ 		 */
+ 		if (rt_index != parse->resultRelation &&
+ 			!rangeTableEntry_used((Node *) parse, rt_index, 0))
+ 			continue;
+ 
+ 		/*
+ 		 * If this RTE is the target then we need to make a copy of it before
+ 		 * expanding it.  The unexpanded copy will become the new target, and
+ 		 * the original RTE will be expanded to become the source of rows to
+ 		 * update/delete.
+ 		 */
+ 		if (rt_index == parse->resultRelation)
+ 		{
+ 			RangeTblEntry *newrte = copyObject(rte);
+ 			parse->rtable = lappend(parse->rtable, newrte);
+ 			parse->resultRelation = list_length(parse->rtable);
+ 
+ 			/*
+ 			 * Wipe out any copied security barrier quals on the new target to
+ 			 * prevent infinite recursion.
+ 			 */
+ 			newrte->securityQuals = NIL;
+ 
+ 			/*
+ 			 * There's no need to do permissions checks twice, so wipe out the
+ 			 * permissions info for the original RTE (we prefer to keep the
+ 			 * bits set on the result RTE).
+ 			 */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * For the most part, Vars referencing the original relation should
+ 			 * remain as they are, meaning that they pull OLD values from the
+ 			 * expanded RTE.  But in the RETURNING list and in any WITH CHECK
+ 			 * OPTION quals, we want such Vars to represent NEW values, so
+ 			 * change them to reference the new RTE.
+ 			 */
+ 			ChangeVarNodes((Node *) parse->returningList, rt_index,
+ 						   parse->resultRelation, 0);
+ 
+ 			ChangeVarNodes((Node *) parse->withCheckOptions, rt_index,
+ 						   parse->resultRelation, 0);
+ 		}
+ 
+ 		/*
+ 		 * Process each security barrier qual in turn, starting with the
+ 		 * innermost one (the first in the list) and working outwards.
+ 		 *
+ 		 * We remove each qual from the list before processing it, so that its
+ 		 * variables aren't modified by expand_security_qual.  Also we don't
+ 		 * necessarily want the attributes referred to by the qual to be
+ 		 * exposed by the newly built subquery.
+ 		 */
+ 		while (rte->securityQuals != NIL)
+ 		{
+ 			Node   *qual = (Node *) linitial(rte->securityQuals);
+ 			rte->securityQuals = list_delete_first(rte->securityQuals);
+ 
+ 			ChangeVarNodes(qual, rt_index, 1, 0);
+ 			expand_security_qual(root, tlist, rt_index, rte, qual);
+ 		}
+ 	}
+ }
+ 
+ 
+ /*
+  * expand_security_qual -
+  *	  expand the specified security barrier qual on a query RTE, turning the
+  *	  RTE into a security barrier subquery.
+  */
+ static void
+ expand_security_qual(PlannerInfo *root, List *tlist, int rt_index,
+ 					 RangeTblEntry *rte, Node *qual)
+ {
+ 	Query		   *parse = root->parse;
+ 	Oid				relid = rte->relid;
+ 	Query		   *subquery;
+ 	RangeTblEntry  *subrte;
+ 	RangeTblRef	   *subrtr;
+ 	PlanRowMark	   *rc;
+ 	security_barrier_replace_vars_context context;
+ 	ListCell	   *cell;
+ 
+ 	/*
+ 	 * There should only be 2 possible cases:
+ 	 *
+ 	 * 1. A relation RTE, which we turn into a subquery RTE containing all
+ 	 * referenced columns.
+ 	 *
+ 	 * 2. A subquery RTE (either from a prior call to this function or from an
+ 	 * expanded view).  In this case we build a new subquery on top of it to
+ 	 * isolate this security barrier qual from any other quals.
+ 	 */
+ 	switch (rte->rtekind)
+ 	{
+ 		case RTE_RELATION:
+ 			/*
+ 			 * Turn the relation RTE into a security barrier subquery RTE,
+ 			 * moving all permissions checks down into the subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 
+ 			subrte = copyObject(rte);
+ 			subrte->inFromCl = true;
+ 			subrte->securityQuals = NIL;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->rtekind = RTE_SUBQUERY;
+ 			rte->relid = InvalidOid;
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 			rte->inh = false;			/* must not be set for a subquery */
+ 
+ 			/* the permissions checks have now been moved down */
+ 			rte->requiredPerms = 0;
+ 			rte->checkAsUser = InvalidOid;
+ 			rte->selectedCols = NULL;
+ 			rte->modifiedCols = NULL;
+ 
+ 			/*
+ 			 * Now deal with any PlanRowMark on this RTE by requesting a lock
+ 			 * of the same strength on the RTE copied down to the subquery.
+ 			 *
+ 			 * Note that we can't push the user-defined quals down since they
+ 			 * may included untrusted functions and that means that we will
+ 			 * end up locking all rows which pass the securityQuals, even if
+ 			 * those rows don't pass the user-defined quals.  This is currently
+ 			 * documented behavior, but it'd be nice to come up with a better
+ 			 * solution some day.
+ 			 */
+ 			rc = get_plan_rowmark(root->rowMarks, rt_index);
+ 			if (rc != NULL)
+ 			{
+ 				switch (rc->markType)
+ 				{
+ 					case ROW_MARK_EXCLUSIVE:
+ 						applyLockingClause(subquery, 1, LCS_FORUPDATE,
+ 										   rc->noWait, false);
+ 						break;
+ 					case ROW_MARK_NOKEYEXCLUSIVE:
+ 						applyLockingClause(subquery, 1, LCS_FORNOKEYUPDATE,
+ 										   rc->noWait, false);
+ 						break;
+ 					case ROW_MARK_SHARE:
+ 						applyLockingClause(subquery, 1, LCS_FORSHARE,
+ 										   rc->noWait, false);
+ 						break;
+ 					case ROW_MARK_KEYSHARE:
+ 						applyLockingClause(subquery, 1, LCS_FORKEYSHARE,
+ 										   rc->noWait, false);
+ 						break;
+ 					case ROW_MARK_REFERENCE:
+ 					case ROW_MARK_COPY:
+ 						/* No locking needed */
+ 						break;
+ 				}
+ 				root->rowMarks = list_delete(root->rowMarks, rc);
+ 			}
+ 
+ 			/*
+ 			 * Replace any variables in the outer query that refer to the
+ 			 * original relation RTE with references to columns that we will
+ 			 * expose in the new subquery, building the subquery's targetlist
+ 			 * as we go.
+ 			 */
+ 			context.rt_index = rt_index;
+ 			context.sublevels_up = 0;
+ 			context.rel = heap_open(relid, NoLock);
+ 			context.targetlist = NIL;
+ 			context.colnames = NIL;
+ 			context.vars_processed = NIL;
+ 
+ 			security_barrier_replace_vars((Node *) parse, &context);
+ 			security_barrier_replace_vars((Node *) tlist, &context);
+ 
+ 			heap_close(context.rel, NoLock);
+ 
+ 			/* Now we know what columns the subquery needs to expose */
+ 			rte->subquery->targetList = context.targetlist;
+ 			rte->eref = makeAlias(rte->eref->aliasname, context.colnames);
+ 
+ 			break;
+ 
+ 		case RTE_SUBQUERY:
+ 			/*
+ 			 * Build a new subquery that includes all the same columns as the
+ 			 * original subquery.
+ 			 */
+ 			subquery = makeNode(Query);
+ 			subquery->commandType = CMD_SELECT;
+ 			subquery->querySource = QSRC_INSTEAD_RULE;
+ 			subquery->targetList = NIL;
+ 
+ 			foreach(cell, rte->subquery->targetList)
+ 			{
+ 				TargetEntry	   *tle;
+ 				Var			   *var;
+ 
+ 				tle = (TargetEntry *) lfirst(cell);
+ 				var = makeVarFromTargetEntry(1, tle);
+ 
+ 				tle = makeTargetEntry((Expr *) var,
+ 									  list_length(subquery->targetList) + 1,
+ 									  pstrdup(tle->resname),
+ 									  tle->resjunk);
+ 				subquery->targetList = lappend(subquery->targetList, tle);
+ 			}
+ 
+ 			subrte = makeNode(RangeTblEntry);
+ 			subrte->rtekind = RTE_SUBQUERY;
+ 			subrte->subquery = rte->subquery;
+ 			subrte->security_barrier = rte->security_barrier;
+ 			subrte->eref = copyObject(rte->eref);
+ 			subrte->inFromCl = true;
+ 			subquery->rtable = list_make1(subrte);
+ 
+ 			subrtr = makeNode(RangeTblRef);
+ 			subrtr->rtindex = 1;
+ 			subquery->jointree = makeFromExpr(list_make1(subrtr), qual);
+ 			subquery->hasSubLinks = checkExprHasSubLink(qual);
+ 
+ 			rte->subquery = subquery;
+ 			rte->security_barrier = true;
+ 
+ 			break;
+ 
+ 		default:
+ 			elog(ERROR, "invalid range table entry for security barrier qual");
+ 	}
+ }
+ 
+ 
+ /*
+  * security_barrier_replace_vars -
+  *	  Apply security barrier variable replacement to an expression tree.
+  *
+  * This also builds/updates a targetlist with entries for each replacement
+  * variable that needs to be exposed by the security barrier subquery RTE.
+  *
+  * NOTE: although this has the form of a walker, we cheat and modify the
+  * nodes in-place.	The given expression tree should have been copied
+  * earlier to ensure that no unwanted side-effects occur!
+  */
+ static void
+ security_barrier_replace_vars(Node *node,
+ 							  security_barrier_replace_vars_context *context)
+ {
+ 	/*
+ 	 * Must be prepared to start with a Query or a bare expression tree; if
+ 	 * it's a Query, go straight to query_tree_walker to make sure that
+ 	 * sublevels_up doesn't get incremented prematurely.
+ 	 */
+ 	if (node && IsA(node, Query))
+ 		query_tree_walker((Query *) node,
+ 						  security_barrier_replace_vars_walker,
+ 						  (void *) context, 0);
+ 	else
+ 		security_barrier_replace_vars_walker(node, context);
+ }
+ 
+ static bool
+ security_barrier_replace_vars_walker(Node *node,
+ 									 security_barrier_replace_vars_context *context)
+ {
+ 	if (node == NULL)
+ 		return false;
+ 
+ 	if (IsA(node, Var))
+ 	{
+ 		Var		   *var = (Var *) node;
+ 
+ 		/*
+ 		 * Note that the same Var may be present in different lists, so we
+ 		 * need to take care not to process it multiple times.
+ 		 */
+ 		if (var->varno == context->rt_index &&
+ 			var->varlevelsup == context->sublevels_up &&
+ 			!list_member_ptr(context->vars_processed, var))
+ 		{
+ 			/*
+ 			 * Found a matching variable. Make sure that it is in the subquery
+ 			 * targetlist and map its attno accordingly.
+ 			 */
+ 			AttrNumber	attno;
+ 			ListCell   *l;
+ 			TargetEntry *tle;
+ 			char	   *attname;
+ 			Var		   *newvar;
+ 
+ 			/* Search for the base attribute in the subquery targetlist */
+ 			attno = InvalidAttrNumber;
+ 			foreach(l, context->targetlist)
+ 			{
+ 				tle = (TargetEntry *) lfirst(l);
+ 				attno++;
+ 
+ 				Assert(IsA(tle->expr, Var));
+ 				if (((Var *) tle->expr)->varattno == var->varattno &&
+ 					((Var *) tle->expr)->varcollid == var->varcollid)
+ 				{
+ 					/* Map the variable onto this subquery targetlist entry */
+ 					var->varattno = attno;
+ 					return false;
+ 				}
+ 			}
+ 
+ 			/* Not in the subquery targetlist, so add it. Get its name. */
+ 			if (var->varattno < 0)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = SystemAttributeDefinition(var->varattno,
+ 													context->rel->rd_rel->relhasoids);
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else if (var->varattno == InvalidAttrNumber)
+ 			{
+ 				attname = "wholerow";
+ 			}
+ 			else if (var->varattno <= context->rel->rd_att->natts)
+ 			{
+ 				Form_pg_attribute att_tup;
+ 
+ 				att_tup = context->rel->rd_att->attrs[var->varattno - 1];
+ 				attname = NameStr(att_tup->attname);
+ 			}
+ 			else
+ 			{
+ 				elog(ERROR, "invalid attribute number %d in security_barrier_replace_vars", var->varattno);
+ 			}
+ 
+ 			/* New variable for subquery targetlist */
+ 			newvar = copyObject(var);
+ 			newvar->varno = 1;
+ 
+ 			attno = list_length(context->targetlist) + 1;
+ 			tle = makeTargetEntry((Expr *) newvar,
+ 								  attno,
+ 								  pstrdup(attname),
+ 								  false);
+ 
+ 			context->targetlist = lappend(context->targetlist, tle);
+ 
+ 			context->colnames = lappend(context->colnames,
+ 										makeString(pstrdup(attname)));
+ 
+ 			/* Update the outer query's variable */
+ 			var->varattno = attno;
+ 
+ 			/* Remember this Var so that we don't process it again */
+ 			context->vars_processed = lappend(context->vars_processed, var);
+ 		}
+ 		return false;
+ 	}
+ 
+ 	if (IsA(node, Query))
+ 	{
+ 		/* Recurse into subselects */
+ 		bool		result;
+ 
+ 		context->sublevels_up++;
+ 		result = query_tree_walker((Query *) node,
+ 								   security_barrier_replace_vars_walker,
+ 								   (void *) context, 0);
+ 		context->sublevels_up--;
+ 		return result;
+ 	}
+ 
+ 	return expression_tree_walker(node, security_barrier_replace_vars_walker,
+ 								  (void *) context);
+ }
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
new file mode 100644
index 52dcc72..cdf541d 100644
*** a/src/backend/optimizer/prep/prepunion.c
--- b/src/backend/optimizer/prep/prepunion.c
*************** typedef struct
*** 55,60 ****
--- 55,61 ----
  {
  	PlannerInfo *root;
  	AppendRelInfo *appinfo;
+ 	int			sublevels_up;
  } adjust_appendrel_attrs_context;
  
  static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
*************** translate_col_privs(const Bitmapset *par
*** 1580,1587 ****
   *	  child rel instead.  We also update rtindexes appearing outside Vars,
   *	  such as resultRelation and jointree relids.
   *
!  * Note: this is only applied after conversion of sublinks to subplans,
!  * so we don't need to cope with recursion into sub-queries.
   *
   * Note: this is not hugely different from what pullup_replace_vars() does;
   * maybe we should try to fold the two routines together.
--- 1581,1589 ----
   *	  child rel instead.  We also update rtindexes appearing outside Vars,
   *	  such as resultRelation and jointree relids.
   *
!  * Note: this is applied after conversion of sublinks to subplans in the
!  * query jointree, but there may still be sublinks in the security barrier
!  * quals of RTEs, so we do need to cope with recursion into sub-queries.
   *
   * Note: this is not hugely different from what pullup_replace_vars() does;
   * maybe we should try to fold the two routines together.
*************** adjust_appendrel_attrs(PlannerInfo *root
*** 1594,1602 ****
  
  	context.root = root;
  	context.appinfo = appinfo;
  
  	/*
! 	 * Must be prepared to start with a Query or a bare expression tree.
  	 */
  	if (node && IsA(node, Query))
  	{
--- 1596,1607 ----
  
  	context.root = root;
  	context.appinfo = appinfo;
+ 	context.sublevels_up = 0;
  
  	/*
! 	 * Must be prepared to start with a Query or a bare expression tree; if
! 	 * it's a Query, go straight to query_tree_walker to make sure that
! 	 * sublevels_up doesn't get incremented prematurely.
  	 */
  	if (node && IsA(node, Query))
  	{
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1635,1641 ****
  	{
  		Var		   *var = (Var *) copyObject(node);
  
! 		if (var->varlevelsup == 0 &&
  			var->varno == appinfo->parent_relid)
  		{
  			var->varno = appinfo->child_relid;
--- 1640,1646 ----
  	{
  		Var		   *var = (Var *) copyObject(node);
  
! 		if (var->varlevelsup == context->sublevels_up &&
  			var->varno == appinfo->parent_relid)
  		{
  			var->varno = appinfo->child_relid;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1652,1657 ****
--- 1657,1663 ----
  				if (newnode == NULL)
  					elog(ERROR, "attribute %d of relation \"%s\" does not exist",
  						 var->varattno, get_rel_name(appinfo->parent_reloid));
+ 				((Var *) newnode)->varlevelsup += context->sublevels_up;
  				return newnode;
  			}
  			else if (var->varattno == 0)
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1694,1703 ****
--- 1700,1715 ----
  					RowExpr    *rowexpr;
  					List	   *fields;
  					RangeTblEntry *rte;
+ 					ListCell   *lc;
  
  					rte = rt_fetch(appinfo->parent_relid,
  								   context->root->parse->rtable);
  					fields = (List *) copyObject(appinfo->translated_vars);
+ 					foreach(lc, fields)
+ 					{
+ 						Var		   *field = (Var *) lfirst(lc);
+ 						field->varlevelsup += context->sublevels_up;
+ 					}
  					rowexpr = makeNode(RowExpr);
  					rowexpr->args = fields;
  					rowexpr->row_typeid = var->vartype;
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1716,1722 ****
  	{
  		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
  
! 		if (cexpr->cvarno == appinfo->parent_relid)
  			cexpr->cvarno = appinfo->child_relid;
  		return (Node *) cexpr;
  	}
--- 1728,1735 ----
  	{
  		CurrentOfExpr *cexpr = (CurrentOfExpr *) copyObject(node);
  
! 		if (context->sublevels_up == 0 &&
! 			cexpr->cvarno == appinfo->parent_relid)
  			cexpr->cvarno = appinfo->child_relid;
  		return (Node *) cexpr;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1724,1730 ****
  	{
  		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
  
! 		if (rtr->rtindex == appinfo->parent_relid)
  			rtr->rtindex = appinfo->child_relid;
  		return (Node *) rtr;
  	}
--- 1737,1744 ----
  	{
  		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
  
! 		if (context->sublevels_up == 0 &&
! 			rtr->rtindex == appinfo->parent_relid)
  			rtr->rtindex = appinfo->child_relid;
  		return (Node *) rtr;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1737,1743 ****
  											  adjust_appendrel_attrs_mutator,
  												 (void *) context);
  		/* now fix JoinExpr's rtindex (probably never happens) */
! 		if (j->rtindex == appinfo->parent_relid)
  			j->rtindex = appinfo->child_relid;
  		return (Node *) j;
  	}
--- 1751,1758 ----
  											  adjust_appendrel_attrs_mutator,
  												 (void *) context);
  		/* now fix JoinExpr's rtindex (probably never happens) */
! 		if (context->sublevels_up == 0 &&
! 			j->rtindex == appinfo->parent_relid)
  			j->rtindex = appinfo->child_relid;
  		return (Node *) j;
  	}
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1750,1756 ****
  											  adjust_appendrel_attrs_mutator,
  														 (void *) context);
  		/* now fix PlaceHolderVar's relid sets */
! 		if (phv->phlevelsup == 0)
  			phv->phrels = adjust_relid_set(phv->phrels,
  										   appinfo->parent_relid,
  										   appinfo->child_relid);
--- 1765,1771 ----
  											  adjust_appendrel_attrs_mutator,
  														 (void *) context);
  		/* now fix PlaceHolderVar's relid sets */
! 		if (phv->phlevelsup == context->sublevels_up)
  			phv->phrels = adjust_relid_set(phv->phrels,
  										   appinfo->parent_relid,
  										   appinfo->child_relid);
*************** adjust_appendrel_attrs_mutator(Node *nod
*** 1822,1833 ****
  		return (Node *) newinfo;
  	}
  
! 	/*
! 	 * NOTE: we do not need to recurse into sublinks, because they should
! 	 * already have been converted to subplans before we see them.
! 	 */
! 	Assert(!IsA(node, SubLink));
! 	Assert(!IsA(node, Query));
  
  	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
  								   (void *) context);
--- 1837,1865 ----
  		return (Node *) newinfo;
  	}
  
! 	if (IsA(node, Query))
! 	{
! 		/*
! 		 * Recurse into sublink subqueries. This should only be possible in
! 		 * security barrier quals of top-level RTEs. All other sublinks should
! 		 * have already been converted to subplans during expression
! 		 * preprocessing, but this doesn't happen for security barrier quals,
! 		 * since they are destined to become quals of a subquery RTE, which
! 		 * will be recursively planned, and so should not be preprocessed at
! 		 * this stage.
! 		 *
! 		 * We don't explicitly Assert() for securityQuals here simply because
! 		 * it's not trivial to do so.
! 		 */
! 		Query	   *newnode;
! 
! 		context->sublevels_up++;
! 		newnode = query_tree_mutator((Query *) node,
! 									 adjust_appendrel_attrs_mutator,
! 									 (void *) context, 0);
! 		context->sublevels_up--;
! 		return (Node *) newnode;
! 	}
  
  	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
  								   (void *) context);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 5dbcce3..caed8ca 100644
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** view_col_is_auto_updatable(RangeTblRef *
*** 2023,2030 ****
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool security_barrier,
! 							 bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
--- 2023,2029 ----
   * updatable.
   */
  const char *
! view_query_is_auto_updatable(Query *viewquery, bool check_cols)
  {
  	RangeTblRef *rtr;
  	RangeTblEntry *base_rte;
*************** view_query_is_auto_updatable(Query *view
*** 2098,2111 ****
  		return gettext_noop("Views that return set-returning functions 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 the future.
- 	 */
- 	if (security_barrier)
- 		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.
  	 */
--- 2097,2102 ----
*************** relation_is_updatable(Oid reloid,
*** 2353,2361 ****
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery,
! 										 RelationIsSecurityView(rel),
! 										 false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
--- 2344,2350 ----
  	{
  		Query	   *viewquery = get_view_query(rel);
  
! 		if (view_query_is_auto_updatable(viewquery, false) == NULL)
  		{
  			Bitmapset  *updatable_cols;
  			int			auto_events;
*************** rewriteTargetView(Query *parsetree, Rela
*** 2510,2516 ****
  
  	auto_update_detail =
  		view_query_is_auto_updatable(viewquery,
- 									 RelationIsSecurityView(view),
  									 parsetree->commandType != CMD_DELETE);
  
  	if (auto_update_detail)
--- 2499,2504 ----
*************** rewriteTargetView(Query *parsetree, Rela
*** 2714,2719 ****
--- 2702,2715 ----
  												   view_targetlist);
  
  	/*
+ 	 * Move any security barrier quals from the view RTE onto the new target
+ 	 * RTE.  Any such quals should now apply to the new target RTE and will not
+ 	 * reference the original view RTE in the rewritten query.
+ 	 */
+ 	new_rte->securityQuals = view_rte->securityQuals;
+ 	view_rte->securityQuals = NIL;
+ 
+ 	/*
  	 * 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.
*************** rewriteTargetView(Query *parsetree, Rela
*** 2793,2798 ****
--- 2789,2798 ----
  	 * only adjust their varnos to reference the new target (just the same as
  	 * we did with the view targetlist).
  	 *
+ 	 * Note that there is special-case handling for the quals of a security
+ 	 * barrier view, since they need to be kept separate from any user-supplied
+ 	 * quals, so these quals are kept on the new target RTE.
+ 	 *
  	 * For INSERT, the view's quals can be ignored in the main query.
  	 */
  	if (parsetree->commandType != CMD_INSERT &&
*************** rewriteTargetView(Query *parsetree, Rela
*** 2801,2807 ****
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 		AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
--- 2801,2825 ----
  		Node	   *viewqual = (Node *) copyObject(viewquery->jointree->quals);
  
  		ChangeVarNodes(viewqual, base_rt_index, new_rt_index, 0);
! 
! 		if (RelationIsSecurityView(view))
! 		{
! 			/*
! 			 * Note: the parsetree has been mutated, so the new_rte pointer is
! 			 * stale and needs to be re-computed.
! 			 */
! 			new_rte = rt_fetch(new_rt_index, parsetree->rtable);
! 			new_rte->securityQuals = lcons(viewqual, new_rte->securityQuals);
! 
! 			/*
! 			 * Make sure that the query is marked correctly if the added qual
! 			 * has sublinks.
! 			 */
! 			if (!parsetree->hasSubLinks)
! 				parsetree->hasSubLinks = checkExprHasSubLink(viewqual);
! 		}
! 		else
! 			AddQual(parsetree, (Node *) viewqual);
  	}
  
  	/*
*************** rewriteTargetView(Query *parsetree, Rela
*** 2863,2871 ****
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added to the
! 				 * query's WHERE clause, and AddQual will have already done
! 				 * this check.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
--- 2881,2888 ----
  				 * Make sure that the query is marked correctly if the added
  				 * qual has sublinks.  We can skip this check if the query is
  				 * already marked, or if the command is an UPDATE, in which
! 				 * case the same qual will have already been added, and this
! 				 * check will already have been done.
  				 */
  				if (!parsetree->hasSubLinks &&
  					parsetree->commandType != CMD_UPDATE)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index b5011af..18d4991 100644
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblEntry
*** 801,806 ****
--- 801,807 ----
  	Oid			checkAsUser;	/* if valid, check access as this role */
  	Bitmapset  *selectedCols;	/* columns needing SELECT permission */
  	Bitmapset  *modifiedCols;	/* columns needing INSERT/UPDATE permission */
+ 	List	   *securityQuals;	/* any security barrier quals to apply */
  } RangeTblEntry;
  
  /*
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
new file mode 100644
index 0f5a7d3..f5fc7e8 100644
*** a/src/include/optimizer/prep.h
--- b/src/include/optimizer/prep.h
*************** extern Node *negate_clause(Node *node);
*** 36,41 ****
--- 36,46 ----
  extern Expr *canonicalize_qual(Expr *qual);
  
  /*
+  * prototypes for prepsecurity.c
+  */
+ extern void expand_security_quals(PlannerInfo *root, List *tlist);
+ 
+ /*
   * prototypes for preptlist.c
   */
  extern List *preprocess_targetlist(PlannerInfo *root, List *tlist);
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
new file mode 100644
index 22551ec..1b51213 100644
*** a/src/include/rewrite/rewriteHandler.h
--- b/src/include/rewrite/rewriteHandler.h
*************** extern void AcquireRewriteLocks(Query *p
*** 25,31 ****
  extern Node *build_column_default(Relation rel, int attrno);
  extern Query *get_view_query(Relation view);
  extern const char *view_query_is_auto_updatable(Query *viewquery,
- 										 bool security_barrier,
  										 bool check_cols);
  extern int	relation_is_updatable(Oid reloid,
  						  bool include_triggers,
--- 25,30 ----
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
new file mode 100644
index 91d1639..f6db582 100644
*** a/src/test/regress/expected/create_view.out
--- b/src/test/regress/expected/create_view.out
*************** CREATE VIEW mysecview4 WITH (security_ba
*** 252,258 ****
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  security_barrier requires a Boolean value
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
--- 252,258 ----
         AS SELECT * FROM tbl1 WHERE a <> 0;
  CREATE VIEW mysecview5 WITH (security_barrier=100)	-- Error
         AS SELECT * FROM tbl1 WHERE a > 100;
! ERROR:  invalid value for boolean option "security_barrier": 100
  CREATE VIEW mysecview6 WITH (invalid_option)		-- Error
         AS SELECT * FROM tbl1 WHERE a < 100;
  ERROR:  unrecognized parameter "invalid_option"
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
new file mode 100644
index 99c9165..261b0c5 100644
*** a/src/test/regress/expected/updatable_views.out
--- b/src/test/regress/expected/updatable_views.out
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 22,33 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
--- 22,31 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
   WHERE table_name LIKE E'r_\\_view%'
*************** SELECT table_name, is_insertable_into
*** 44,50 ****
   ro_view19  | NO
   ro_view2   | NO
   ro_view20  | NO
-  ro_view21  | NO
   ro_view3   | NO
   ro_view4   | NO
   ro_view5   | NO
--- 42,47 ----
*************** SELECT table_name, is_insertable_into
*** 55,61 ****
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (21 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
--- 52,58 ----
   rw_view14  | YES
   rw_view15  | YES
   rw_view16  | YES
! (20 rows)
  
  SELECT table_name, is_updatable, is_insertable_into
    FROM information_schema.views
*************** SELECT table_name, is_updatable, is_inse
*** 73,79 ****
   ro_view19  | NO           | NO
   ro_view2   | NO           | NO
   ro_view20  | NO           | NO
-  ro_view21  | NO           | NO
   ro_view3   | NO           | NO
   ro_view4   | NO           | NO
   ro_view5   | NO           | NO
--- 70,75 ----
*************** SELECT table_name, is_updatable, is_inse
*** 84,90 ****
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (21 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
--- 80,86 ----
   rw_view14  | YES          | YES
   rw_view15  | YES          | YES
   rw_view16  | YES          | YES
! (20 rows)
  
  SELECT table_name, column_name, is_updatable
    FROM information_schema.columns
*************** SELECT table_name, column_name, is_updat
*** 103,125 ****
   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_view21  | a             | NO
!  ro_view21  | b             | NO
!  ro_view21  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
--- 99,119 ----
   ro_view17  | a             | NO
   ro_view17  | b             | NO
   ro_view18  | a             | NO
!  ro_view19  | sequence_name | NO
!  ro_view19  | last_value    | NO
!  ro_view19  | start_value   | NO
!  ro_view19  | increment_by  | NO
!  ro_view19  | max_value     | NO
!  ro_view19  | min_value     | NO
!  ro_view19  | cache_value   | NO
!  ro_view19  | log_cnt       | NO
!  ro_view19  | is_cycled     | NO
!  ro_view19  | is_called     | NO
   ro_view2   | a             | NO
   ro_view2   | b             | NO
!  ro_view20  | a             | NO
!  ro_view20  | b             | NO
!  ro_view20  | g             | NO
   ro_view3   | ?column?      | NO
   ro_view4   | count         | NO
   ro_view5   | a             | NO
*************** SELECT table_name, column_name, is_updat
*** 140,146 ****
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (48 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
--- 134,140 ----
   rw_view16  | a             | YES
   rw_view16  | b             | YES
   rw_view16  | aa            | YES
! (46 rows)
  
  -- Read-only views
  DELETE FROM ro_view1;
*************** INSERT INTO ro_view17 VALUES (3, 'ROW 3'
*** 268,291 ****
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! 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 enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! 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 enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view21 SET b=upper(b);
! ERROR:  cannot update view "ro_view21"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  DROP TABLE base_tbl CASCADE;
! NOTICE:  drop cascades to 17 other objects
  DETAIL:  drop cascades to view ro_view1
  drop cascades to view ro_view17
  drop cascades to view ro_view2
--- 262,281 ----
  ERROR:  cannot insert into view "ro_view1"
  DETAIL:  Views containing DISTINCT are not automatically updatable.
  HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.
! DELETE FROM ro_view18;
! ERROR:  cannot delete from view "ro_view18"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable deleting from the view, provide an INSTEAD OF DELETE trigger or an unconditional ON DELETE DO INSTEAD rule.
! UPDATE ro_view19 SET max_value=1000;
! ERROR:  cannot update view "ro_view19"
  DETAIL:  Views that do not select from a single table or view are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
! UPDATE ro_view20 SET b=upper(b);
! ERROR:  cannot update view "ro_view20"
  DETAIL:  Views that return set-returning functions are not automatically updatable.
  HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
  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_view11
*** 299,311 ****
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view18
! drop cascades to view ro_view21
  drop cascades to view ro_view4
  drop cascades to view rw_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);
--- 289,300 ----
  drop cascades to view ro_view13
  drop cascades to view rw_view15
  drop cascades to view rw_view16
! drop cascades to view ro_view20
  drop cascades to view ro_view4
  drop cascades to view rw_view14
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
! NOTICE:  drop cascades to view ro_view19
  -- 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);
*************** DROP TABLE base_tbl CASCADE;
*** 1740,1742 ****
--- 1729,1987 ----
  NOTICE:  drop cascades to 2 other objects
  DETAIL:  drop cascades to view rw_view1
  drop cascades to view rw_view2
+ -- security barrier view
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Dick
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+                   QUERY PLAN                   
+ -----------------------------------------------
+  Subquery Scan on rw_view1
+    Filter: snoop(rw_view1.person)
+    ->  Seq Scan on base_tbl
+          Filter: (visibility = 'public'::text)
+ (4 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Seq Scan on base_tbl base_tbl_2
+                Filter: (visibility = 'public'::text)
+ (5 rows)
+ 
+ -- security barrier view on top of security barrier view
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 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   | person      | YES
+ (1 row)
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+  person 
+ --------
+  Tom
+  Harry
+ (2 rows)
+ 
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Tom
+ NOTICE:  snooped value: Harry
+ NOTICE:  snooped value: Harry
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+                      QUERY PLAN                      
+ -----------------------------------------------------
+  Subquery Scan on rw_view2
+    Filter: snoop(rw_view2.person)
+    ->  Subquery Scan on rw_view1
+          Filter: snoop(rw_view1.person)
+          ->  Seq Scan on base_tbl
+                Filter: (visibility = 'public'::text)
+ (6 rows)
+ 
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: snoop(base_tbl.person)
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 rows)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Delete on base_tbl base_tbl_1
+    ->  Subquery Scan on base_tbl
+          Filter: (NOT snoop(base_tbl.person))
+          ->  Subquery Scan on base_tbl_2
+                Filter: snoop(base_tbl_2.person)
+                ->  Seq Scan on base_tbl base_tbl_3
+                      Filter: (visibility = 'public'::text)
+ (7 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
+ -- security barrier view on top of table with rules
+ CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean);
+ INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true);
+ CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl
+   WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id)
+   DO INSTEAD
+     UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id;
+ CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl
+   DO INSTEAD
+     UPDATE base_tbl SET deleted = true WHERE id = old.id;
+ CREATE VIEW rw_view1 WITH (security_barrier=true) AS
+   SELECT id, data FROM base_tbl WHERE NOT deleted;
+ SELECT * FROM rw_view1;
+  id | data  
+ ----+-------
+   1 | Row 1
+ (1 row)
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+                                QUERY PLAN                                
+ -------------------------------------------------------------------------
+  Update on base_tbl base_tbl_1
+    ->  Nested Loop
+          ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_1
+                Index Cond: (id = 1)
+          ->  Subquery Scan on base_tbl
+                Filter: snoop(base_tbl.data)
+                ->  Index Scan using base_tbl_pkey on base_tbl base_tbl_2
+                      Index Cond: (id = 1)
+                      Filter: (NOT deleted)
+ (9 rows)
+ 
+ DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ NOTICE:  snooped value: Row 1
+ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
+                         QUERY PLAN                         
+ -----------------------------------------------------------
+  Insert on base_tbl
+    InitPlan 1 (returns $0)
+      ->  Index Only Scan using base_tbl_pkey on base_tbl t
+            Index Cond: (id = 2)
+    ->  Result
+          One-Time Filter: ($0 IS NOT TRUE)
+  
+  Update on base_tbl
+    InitPlan 1 (returns $0)
+      ->  Index Only Scan using base_tbl_pkey on base_tbl t
+            Index Cond: (id = 2)
+    ->  Result
+          One-Time Filter: $0
+          ->  Index Scan using base_tbl_pkey on base_tbl
+                Index Cond: (id = 2)
+ (15 rows)
+ 
+ INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ SELECT * FROM base_tbl;
+  id |   data    | deleted 
+ ----+-----------+---------
+   1 | Row 1     | t
+   2 | New row 2 | f
+ (2 rows)
+ 
+ DROP TABLE base_tbl CASCADE;
+ NOTICE:  drop cascades to view rw_view1
diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql
new file mode 100644
index a77cf19..08f7504 100644
*** a/src/test/regress/sql/updatable_views.sql
--- b/src/test/regress/sql/updatable_views.sql
*************** CREATE VIEW rw_view14 AS SELECT ctid, a,
*** 25,36 ****
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  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
! CREATE VIEW ro_view21 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
--- 25,34 ----
  CREATE VIEW rw_view15 AS SELECT a, upper(b) FROM base_tbl; -- Expression/function may be part of an updatable view
  CREATE VIEW rw_view16 AS SELECT a, b, a AS aa FROM base_tbl; -- Repeated column may be part of an updatable view
  CREATE VIEW ro_view17 AS SELECT * FROM ro_view1; -- Base relation not updatable
! CREATE VIEW ro_view18 AS SELECT * FROM (VALUES(1)) AS tmp(a); -- VALUES in rangetable
  CREATE SEQUENCE seq;
! CREATE VIEW ro_view19 AS SELECT * FROM seq; -- View based on a sequence
! CREATE VIEW ro_view20 AS SELECT a, b, generate_series(1, a) g FROM base_tbl; -- SRF in targetlist not supported
  
  SELECT table_name, is_insertable_into
    FROM information_schema.tables
*************** SELECT * FROM base_tbl;
*** 87,99 ****
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  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;
! UPDATE ro_view21 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view19;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
--- 85,96 ----
  DELETE FROM rw_view16 WHERE a=-3; -- should be OK
  -- Read-only views
  INSERT INTO ro_view17 VALUES (3, 'ROW 3');
! DELETE FROM ro_view18;
! UPDATE ro_view19 SET max_value=1000;
! UPDATE ro_view20 SET b=upper(b);
  
  DROP TABLE base_tbl CASCADE;
! DROP VIEW ro_view10, ro_view12, ro_view18;
  DROP SEQUENCE seq CASCADE;
  
  -- simple updatable view
*************** CREATE VIEW rw_view2 AS
*** 828,830 ****
--- 825,931 ----
    SELECT * FROM rw_view1 WHERE a > b WITH LOCAL CHECK OPTION;
  INSERT INTO rw_view2 VALUES (2,3); -- ok, but not in view (doesn't fail rw_view2's check)
  DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view
+ 
+ CREATE TABLE base_tbl (person text, visibility text);
+ INSERT INTO base_tbl VALUES ('Tom', 'public'),
+                             ('Dick', 'private'),
+                             ('Harry', 'public');
+ 
+ CREATE VIEW rw_view1 AS
+   SELECT person FROM base_tbl WHERE visibility = 'public';
+ 
+ CREATE FUNCTION snoop(val text)
+ RETURNS boolean AS
+ $$
+ BEGIN
+   RAISE NOTICE 'snooped value: %', val;
+   RETURN true;
+ END;
+ $$
+ LANGUAGE plpgsql COST 0.000001;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ ALTER VIEW rw_view1 SET (security_barrier = true);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view1 WHERE snoop(person);
+ UPDATE rw_view1 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view1 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view1 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE NOT snoop(person);
+ 
+ -- security barrier view on top of security barrier view
+ 
+ CREATE VIEW rw_view2 WITH (security_barrier = true) AS
+   SELECT * FROM rw_view1 WHERE snoop(person);
+ 
+ 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;
+ 
+ SELECT * FROM rw_view2 WHERE snoop(person);
+ UPDATE rw_view2 SET person=person WHERE snoop(person);
+ DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ EXPLAIN (costs off) SELECT * FROM rw_view2 WHERE snoop(person);
+ EXPLAIN (costs off) UPDATE rw_view2 SET person=person WHERE snoop(person);
+ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE NOT snoop(person);
+ 
+ DROP TABLE base_tbl CASCADE;
+ 
+ -- security barrier view on top of table with rules
+ 
+ CREATE TABLE base_tbl(id int PRIMARY KEY, data text, deleted boolean);
+ INSERT INTO base_tbl VALUES (1, 'Row 1', false), (2, 'Row 2', true);
+ 
+ CREATE RULE base_tbl_ins_rule AS ON INSERT TO base_tbl
+   WHERE EXISTS (SELECT 1 FROM base_tbl t WHERE t.id = new.id)
+   DO INSTEAD
+     UPDATE base_tbl SET data = new.data, deleted = false WHERE id = new.id;
+ 
+ CREATE RULE base_tbl_del_rule AS ON DELETE TO base_tbl
+   DO INSTEAD
+     UPDATE base_tbl SET deleted = true WHERE id = old.id;
+ 
+ CREATE VIEW rw_view1 WITH (security_barrier=true) AS
+   SELECT id, data FROM base_tbl WHERE NOT deleted;
+ 
+ SELECT * FROM rw_view1;
+ 
+ EXPLAIN (costs off) DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ DELETE FROM rw_view1 WHERE id = 1 AND snoop(data);
+ 
+ EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ INSERT INTO rw_view1 VALUES (2, 'New row 2');
+ 
+ SELECT * FROM base_tbl;
+ 
+ DROP TABLE base_tbl CASCADE;
#69Stephen Frost
sfrost@snowman.net
In reply to: Tom Lane (#67)
Re: WIP patch (v2) for updatable security barrier views

All,

* Tom Lane (tgl@sss.pgh.pa.us) wrote:

Yeah, the point of the "gotcha" is that a FOR UPDATE specified *outside* a
security-barrier view would act as though it had appeared *inside* the
view, since it effectively gets pushed down even though outer quals don't.

Alright, I've committed this with an updated note regarding the locking,
and a few additional regression tests (which appear to have upset some
of the buildfarm- will look at that...).

Please let me know if you see any issues. I'm planning to spend
more-or-less all of tomorrow looking over the RLS patch. As it's rather
large, I'm not sure I'll be able to get through it all, but I'm gonna
give it a go.

Thanks,

Stephen

#70Stephen Frost
sfrost@snowman.net
In reply to: Gregory Smith (#61)
Re: WIP patch (v2) for updatable security barrier views

Greg,

* Gregory Smith (gregsmithpgsql@gmail.com) wrote:

Now that Tom has given some guidance on the row locking weirdness,
maybe it's time for me to start updating those into make check form.

If you have time, that'd certainly be helpful.

The documentation has been in a similar holding pattern. I have
lots of resources to help document what does and doesn't work here
to the quality expected in the manual. I just need a little more
confidence about which feature set is commit worthy. The
documentation that makes sense is very different if only updatable
security barrier views is committed.

Guess I see that a bit differently- the two features, while they might
be able to be used together, should both be documented appropriately and
at the level that each can be used independently. I'm not sure that
documentation about how to build RLS on top of updatable security
barrier views w/o actual RLS support being in PG would be something we'd
want to include in the PG documentation, which I guess is what you're
getting at here.

Thanks,

Stephen

#71Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Stephen Frost (#69)
Re: WIP patch (v2) for updatable security barrier views

On 13 April 2014 05:23, Stephen Frost <sfrost@snowman.net> wrote:

Alright, I've committed this with an updated note regarding the locking,
and a few additional regression tests

That's awesome. Thank you very much.

Regards,
Dean

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