[v9.3] Row-Level Security

Started by Kohei KaiGaiover 13 years ago40 messages
#1Kohei KaiGai
kaigai@kaigai.gr.jp
1 attachment(s)

The attached patch provides bare row-level security feature.

Table's owner can set its own security policy using the following syntax:
ALTER TABLE <table> SET ROW LEVEL SECURITY (<condition>);
ALTER TABLE <table> RESET ROW LEVEL SECURITY;

The condition must be an expression that returns a boolean value
and can reference contents of each rows. (Right now, it does not
support to contain SubLink in the expression; to be improved)

In the previous discussion, we planned to add a syntax option to
clarify the command type to fire the RLS policy, such as FOR UPDATE.
But current my opinion is, it is not necessary. For example, we can
reference the contents of rows being updated / deleted using
RETURNING clause. So, it does not make sense to have different
RLS policy at UPDATE / DELETE from SELECT.

If and when user's query (SELECT, UPDATE or DELETE, not INSERT)
references the relation with RLS policy, only rows that satisfies the
supplied condition are available to access.
It performs as if the configured condition was implicitly added to
the WHERE clause, however, this mechanism tries to replace
references to the table with RLS policy by a simple sub-query
that scans the target table with RLS policy, to ensure the policy
condition is evaluated earlier than any other user given qualifier.

EXPLAIN shows how RLS works.

postgres=# ALTER TABLE sample SET ROW LEVEL SECURITY (z > current_date - 10);
ALTER TABLE

postgres=# EXPLAIN SELECT * FROM sample WHERE f_leak(y);
QUERY PLAN
------------------------------------------------------------------------------------
Subquery Scan on sample (cost=0.00..42.54 rows=215 width=40)
Filter: f_leak(sample.y)
-> Seq Scan on sample (cost=0.00..36.10 rows=644 width=66)
Filter: ((z > (('now'::cstring)::date - 10)) OR
has_superuser_privilege())
(4 rows)

In above example, the security policy does not allow to reference
rows earlier than 10 days. Then, SELECT on the table was
expanded to a sub-query and configured expression was added
inside of the sub-query. Database superuser can bypass any
security checks, so "OR has_superuser_privilege()" was automatically
attached in addition to user configured expression.

On the other hand, I'm not 100% sure about my design to restrict
rows to be updated and deleted. Similarly, it expands the target
relation of UPDATE or DELETE statement into a sub-query with
condition. ExecModifyTable() pulls a tuple from the sub-query,
instead of regular table, so it seems to me working at least, but
I didn't try all the possible cases of course.

postgres=# EXPLAIN UPDATE sample SET y = y || '_updt' WHERE f_leak(y);
QUERY PLAN
------------------------------------------------------------------------------------------
Update on sample (cost=0.00..43.08 rows=215 width=46)
-> Subquery Scan on sample (cost=0.00..43.08 rows=215 width=46)
Filter: f_leak(sample.y)
-> Seq Scan on sample (cost=0.00..36.10 rows=644 width=66)
Filter: ((z > (('now'::cstring)::date - 10)) OR
has_superuser_privilege())
(5 rows)

I have two other ideas to implement writer side RLS.

The first idea modifies WHERE clause to satisfies RLS policy, but Robert
concerned about this idea in the previous discussion, because it takes
twice scans.
UPDATE sample SET y = y || '_updt' WHERE f_leak(y);
shall be modified to:
UPDATE sample SET y = y || '_updt' WHERE ctid = (
SELECT ctid FROM (
SELECT ctid, * FROM sample WHERE <RLS policy>
) AS pseudo_sample WHERE f_leak(y)
);
Although the outer scan is ctid scan, it takes seq-scan at first level.

The second idea tries to duplicate RangeTblEntry of the target relation
to be updated or deleted, then one perform as target relation as is, and
the other performs as data source to produce older version of tuples;
being replaced by a sub-query with RLS condition.
I didn't try the second idea yet. As long as we can patch the code that
assumes the target relation has same rtindex with source relation, it
might be safe approach. However, I'm not sure which is less invasive
approach compared to the current patch.

Of course, here is some limitations, to keep the patch size reasonable
level to review.
- The permission to bypass RLS policy was under discussion.
If and when we should provide a special permission to bypass RLS
policy, the "OR has_superuser_privilege()" shall be replaced by
"OR has_table_privilege(tableoid, 'RLSBYPASS')".
Right now, I allows only superuser to bypass RLS policy.
- This patch focuses on the bare feature only, not any enhancement
at query optimization feature, so RLS policy might prevent index-scan,
right now.
- RLS policy is not applied to the row to be inserted, or newer version
of row to be updated. It can be implemented using before-row trigger.
It might be an idea to inject RLS trigger function automatically, like
FK constraints, but not yet.
- As Florian pointed out, current_user may change during query
execution if DECLARE and FETCH are used.
Although it is not a matter in RLS itself, should be improved later.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

Attachments:

pgsql-v9.3-row-level-security.v1.patchapplication/octet-stream; name=pgsql-v9.3-row-level-security.v1.patchDownload
 doc/src/sgml/ref/alter_table.sgml          |   38 ++
 doc/src/sgml/user-manag.sgml               |  125 +++++++
 src/backend/catalog/Makefile               |    2 +-
 src/backend/catalog/dependency.c           |   22 ++
 src/backend/commands/explain.c             |    8 +-
 src/backend/commands/tablecmds.c           |  185 ++++++++++
 src/backend/optimizer/plan/planner.c       |    7 +
 src/backend/optimizer/prep/preptlist.c     |   54 ++-
 src/backend/optimizer/prep/prepunion.c     |   28 +-
 src/backend/optimizer/util/Makefile        |    2 +-
 src/backend/optimizer/util/rowlvsec.c      |  524 ++++++++++++++++++++++++++++
 src/backend/parser/gram.y                  |   16 +
 src/backend/utils/adt/acl.c                |   34 ++
 src/backend/utils/adt/ri_triggers.c        |   33 +-
 src/backend/utils/cache/syscache.c         |   12 +
 src/include/catalog/dependency.h           |    1 +
 src/include/catalog/indexing.h             |    3 +
 src/include/catalog/pg_proc.h              |    7 +
 src/include/catalog/pg_rowlvsec.h          |   39 +++
 src/include/commands/tablecmds.h           |    2 +
 src/include/nodes/parsenodes.h             |    5 +-
 src/include/optimizer/rowlvsec.h           |   31 ++
 src/include/utils/syscache.h               |    1 +
 src/test/regress/expected/rowlvsec.out     |  438 +++++++++++++++++++++++
 src/test/regress/expected/sanity_check.out |    3 +-
 src/test/regress/parallel_schedule         |    2 +-
 src/test/regress/serial_schedule           |    1 +
 src/test/regress/sql/rowlvsec.sql          |  216 ++++++++++++
 28 files changed, 1821 insertions(+), 18 deletions(-)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 04e3e54..a3c89d1 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     NOT OF
     OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
     SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+    SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)
+    RESET ROW LEVEL SECURITY
 
 <phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
 
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
    </varlistentry>
 
    <varlistentry>
+    <term><literal>SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+    <listitem>
+     <para>
+      This form set row-level security policy of the table.
+      Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+      as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+      clause, although mechanism guarantees to evaluate this condition earlier
+      than any other user given condition.
+      See also <xref linkend="ROW-LEVEL-SECURITY">.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET ROW LEVEL SECURITY</literal></term>
+    <listitem>
+     <para>
+      This form reset row-level security policy of the table, if exists.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
@@ -806,6 +831,19 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="PARAMETER">condition</replaceable></term>
+      <listitem>
+       <para>
+        An expression that returns a value of type boolean. Expect for a case
+        when queries are executed with superuser privilege, only rows for which
+        this expression returns true will be fetched, updated or deleted.
+        This expression can reference columns of the relation being configured,
+        however, unavailable to include sub-query right now.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..e4d5aaf 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,129 @@ DROP ROLE <replaceable>name</replaceable>;
   </para>
  </sect1>
 
+ <sect1 id="row-level-security">
+  <title>Row-level Security</title>
+  <para>
+   <productname>PostgreSQL</productname> v9.3 or later provides
+   row-level security feature, like several commercial database
+   management system. It allows table owner to assign a particular
+   condition that performs as a security policy of the table; only
+   rows that satisfies the condition are available to fetch, update
+   or delete, except for a case when superuser runs queries.
+  </para>
+  <para>
+   Row-level security policy can be set using
+   <literal>SET ROW LEVEL SECURITY</literal> command of
+   <xref linkend="SQL-ALTERTABLE"> statement, as an expression
+   form that returns a value of type boolean. This expression can
+   contain references to columns of the relation, so it enables
+   to construct arbitrary rule to make access control decision
+   based on contents of each rows.
+  </para>
+  <para>
+   For example, the following <literal>customer</literal> table
+   has <literal>uname</literal> field to store user name, and
+   it assume we don't want to expose any properties of other
+   customers.
+   The following command set <literal>current_user = uname</literal>
+   as row-level security policy on the <literal>customer</literal>
+   table.
+<screen>
+postgres=# ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+</screen>
+   <xref linkend="SQL-EXPLAIN"> command shows how row-level
+   security policy works on the supplied query.
+<screen>
+postgres=# EXPLAIN (costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+                                    QUERY PLAN
+-----------------------------------------------------------------------------------
+ Subquery Scan on customer
+   Filter: f_leak(customer.upasswd)
+   ->  Seq Scan on customer
+         Filter: ((("current_user"())::text = uname) OR has_superuser_privilege())
+(4 rows)
+</screen>
+   This query execution plan means the preconfigured row-level
+   security policy is implicitly added, and executed earlier than
+   user given conditions regardless to its cost.
+   On the other hand, these conditions are overwritten if current
+   user has superuser privilege.
+
+   Similary, <xref linkend="SQL-UPDATE"> or <xref linkend="SQL-DELETE">
+   commands are also restricted with this policy, as follows.
+<screen>
+postgres=# EXPLAIN (costs off) UPDATE customer SET upasswd = md5('abcd') WHERE cid = 2;
+                                       QUERY PLAN
+-----------------------------------------------------------------------------------------
+ Update on customer
+   ->  Subquery Scan on customer
+         ->  Index Scan using customer_pkey on customer
+               Index Cond: (cid = 2)
+               Filter: ((("current_user"())::text = uname) OR has_superuser_privilege())
+(5 rows)
+</screen>
+  </para>
+  <para>
+   Please note that row-level security policy of the parent
+   table is not applied to child relations. Scope of row-level
+   security policy is limited to the relation on which it is
+   set.
+<screen>
+postgres=# EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(y);
+                                QUERY PLAN
+--------------------------------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.y)
+               ->  Seq Scan on t1
+                     Filter: (((x % 2) = 0) OR has_superuser_privilege())
+         ->  Seq Scan on t2 t1
+               Filter: f_leak(y)
+         ->  Subquery Scan on t3
+               Filter: f_leak(t3.y)
+               ->  Seq Scan on t3 t1
+                     Filter: (((x % 2) = 1) OR has_superuser_privilege())
+(12 rows)
+</screen>
+   In the above example, <literal>t1</literal> has inherited
+   child table <literal>t2</literal> and <literal>t3</literal>,
+   and row-level security policy is set on only <literal>t1</literal>,
+   and <literal>t3</literal>, not <literal>t2</literal>.
+
+   The row-level security policy of <literal>t1</literal>,
+   <literal>x</literal> must be even-number, is appended only
+   <literal>t1</literal>, neither <literal>t2</literal> nor
+   <literal>t3</literal>. On the contrary, <literal>t3</literal>
+   has different row-level security policy; <literal>x</literal>
+   must be odd-number.
+  </para>
+  <para>
+   Right now, row-level security feature has several limitation,
+   although these shall be improved in the future version.
+
+   Row-level security policy is not applied to rows to be inserted
+   and newer revision of updated rows, thus, it is needed to
+   define before-row-insert or before-row-update trigger to check
+   whether the row's contents satisfies the policy.
+
+   We cannot contain sub-query as a part of row-level security
+   policy, so access control decision has to be done with
+   information originated from the target relation.
+
+   Row-level security policy implicitly added to reference for
+   the target relation may prevent query optimization, because
+   query optimization has to be done query execution of couse,
+   in spite of all the permission check shall be applied on
+   execution stage.
+
+   Although it is not a specific matter in row-level security,
+   <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH">
+   allows to switch current user identifier during execution
+   of the query. Thus, it may cause unpredicated behavior
+   if and when <literal>current_user</literal> is used as
+   a part of row-level security policy.
+  </para>
+ </sect1>
 </chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 62fc9b0..a21c116 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
-	pg_foreign_table.h \
+	pg_foreign_table.h pg_rowlvsec.h \
 	pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
 	toasting.h indexing.h \
     )
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index d4e1f76..73bb603 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -45,6 +45,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlvsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
@@ -60,6 +61,7 @@
 #include "commands/proclang.h"
 #include "commands/schemacmds.h"
 #include "commands/seclabel.h"
+#include "commands/tablecmds.h"
 #include "commands/tablespace.h"
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
@@ -1221,6 +1223,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveExtensionById(object->objectId);
 			break;
 
+		case OCLASS_ROWLVSEC:
+			RemoveRowLvSecById(object->objectId);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized object class: %u",
 				 object->classId);
@@ -2269,6 +2275,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case ExtensionRelationId:
 			return OCLASS_EXTENSION;
+
+		case RowLevelSecurityRelationId:
+			return OCLASS_ROWLVSEC;
 	}
 
 	/* shouldn't get here */
@@ -2903,6 +2912,19 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_ROWLVSEC:
+			{
+				char	   *relname;
+
+				relname = get_rel_name(object->objectId);
+				if (!relname)
+					elog(ERROR, "cache lookup failed for relation %u",
+						 object->objectId);
+				appendStringInfo(&buffer, _("row-level security of %s"),
+								 relname);
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 1e8f618..cdc7d3a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1825,8 +1825,12 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
 		case T_TidScan:
 		case T_ForeignScan:
 		case T_ModifyTable:
-			/* Assert it's on a real relation */
-			Assert(rte->rtekind == RTE_RELATION);
+			/*
+			 * Assert it's on either a real relation, or a sub-query
+			 * originated from a real relation with row-level security
+			 */
+			Assert(rte->rtekind == RTE_RELATION ||
+				   (rte->rtekind == RTE_SUBQUERY && OidIsValid(rte->relid)));
 			objectname = get_rel_name(rte->relid);
 			if (es->verbose)
 				namespace = get_namespace_name(get_rel_namespace(rte->relid));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5c69cfb..6c94ca2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlvsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -384,6 +385,7 @@ static void ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
 static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid);
 static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode);
 static void ATExecDropOf(Relation rel, LOCKMODE lockmode);
+static void ATExecSetRowLvSecurity(Relation relation, Node *clause);
 static void ATExecGenericOptions(Relation rel, List *options);
 
 static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
@@ -2834,6 +2836,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_SetTableSpace:		/* must rewrite heap */
 			case AT_DropNotNull:		/* may change some SQL plans */
 			case AT_SetNotNull:
+			case AT_SetRowLvSecurity:
+			case AT_ResetRowLvSecurity:
 			case AT_GenericOptions:
 			case AT_AlterColumnGenericOptions:
 				cmd_lockmode = AccessExclusiveLock;
@@ -3196,6 +3200,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_DropInherit:	/* NO INHERIT */
 		case AT_AddOf:			/* OF */
 		case AT_DropOf: /* NOT OF */
+		case AT_SetRowLvSecurity:
+		case AT_ResetRowLvSecurity:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* These commands never recurse */
 			/* No command-specific prep needed */
@@ -3471,6 +3477,10 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropOf:
 			ATExecDropOf(rel, lockmode);
 			break;
+		case AT_SetRowLvSecurity:
+		case AT_ResetRowLvSecurity:
+			ATExecSetRowLvSecurity(rel, (Node *) cmd->def);
+			break;
 		case AT_GenericOptions:
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
@@ -7648,6 +7658,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				Assert(defaultexpr);
 				break;
 
+			case OCLASS_ROWLVSEC:
+				/*
+				 * A row-level security policy can depend on a column in case
+				 * when the policy clause references a particular column.
+				 * Due to same reason why TRIGGER ... WHEN does not support
+				 * to change column's type being referenced in clause, row-
+				 * level security policy also does not support it.
+				 */
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used in a row-level security policy"),
+                         errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject),
+								   colName)));
+				break;
+
 			case OCLASS_PROC:
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
@@ -9745,6 +9771,165 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode)
 	heap_close(relationRelation, RowExclusiveLock);
 }
 
+
+/*
+ * ALTER TABLE <name> SET ROW LEVEL SECURITY (...) OR 
+ *                    RESET ROW LEVEL SECURITY
+ */
+static void
+ATExecSetRowLvSecurity(Relation relation, Node *clause)
+{
+	Oid			relationId = RelationGetRelid(relation);
+	Relation	rls_rel;
+
+	rls_rel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	if (clause)
+	{
+		/* SET ROW LEVEL SECURITY (<clause>) */
+		ParseState	   *pstate;
+		RangeTblEntry  *rte;
+		Node		   *qual;
+		HeapTuple		oldtup;
+		HeapTuple		newtup;
+		Datum			values[Natts_pg_rowlvsec];
+		bool			isnull[Natts_pg_rowlvsec];
+		bool			replace[Natts_pg_rowlvsec];
+		ObjectAddress	target;
+		ObjectAddress	myself;
+
+		memset(values,  0, sizeof(values));
+		memset(isnull,  0, sizeof(isnull));
+		memset(replace, 0, sizeof(replace));
+
+		/* Parse the supplied clause */
+		pstate = make_parsestate(NULL);
+
+		rte = addRangeTableEntryForRelation(pstate, relation,
+											NULL, false, false);
+		addRTEtoQuery(pstate, rte, false, true, true);
+
+		qual = transformWhereClause(pstate,
+									copyObject(clause),
+									"ROW LEVEL SECURITY");
+		/*
+		 * XXX - Likely, here is no technical reason why we don't support
+		 * subLinks in the security policy. But, we don't implement the
+		 * routines to support sub-queries in RLS policy.
+		 */
+		if (pstate->p_hasSubLinks)
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot use subquery in row-level security")));
+		/* No aggregate function support */
+		if (pstate->p_hasAggs)
+			ereport(ERROR,
+					(errcode(ERRCODE_GROUPING_ERROR),
+					 errmsg("cannot use aggregate in row-level security")));
+		/* No window function support */
+		if (pstate->p_hasWindowFuncs)
+			ereport(ERROR,
+					(errcode(ERRCODE_WINDOWING_ERROR),
+					 errmsg("cannot use window function in row-level security")));
+
+		/* Do insert or update pg_rowlvsec catalog */
+		oldtup = SearchSysCache1(RLSRELID, ObjectIdGetDatum(relationId));
+		if (HeapTupleIsValid(oldtup))
+		{
+			replace[Anum_pg_rowlvsec_rlsqual - 1] = true;
+			values[Anum_pg_rowlvsec_rlsqual - 1]
+				= CStringGetTextDatum(nodeToString(qual));
+
+			newtup = heap_modify_tuple(oldtup,
+									   RelationGetDescr(rls_rel),
+									   values, isnull, replace);
+			simple_heap_update(rls_rel, &newtup->t_self, newtup);
+
+			deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+									   relationId, false);
+			ReleaseSysCache(oldtup);
+		}
+		else
+		{
+			values[Anum_pg_rowlvsec_rlsrelid - 1]
+				= ObjectIdGetDatum(relationId);
+			values[Anum_pg_rowlvsec_rlsqual - 1]
+				= CStringGetTextDatum(nodeToString(qual));
+
+			newtup = heap_form_tuple(RelationGetDescr(rls_rel),
+									 values, isnull);
+			simple_heap_insert(rls_rel, newtup);
+
+		}
+		CatalogUpdateIndexes(rls_rel, newtup);
+
+		heap_freetuple(newtup);
+
+		/* records dependencies of RLS-policy and relation/columns */
+		target.classId = RelationRelationId;
+		target.objectId = relationId;
+		target.objectSubId = 0;
+
+		myself.classId = RowLevelSecurityRelationId;
+		myself.objectId = relationId;
+		myself.objectSubId = 0;
+
+		recordDependencyOn(&myself, &target, DEPENDENCY_NORMAL);
+
+		recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+							   DEPENDENCY_NORMAL);
+
+		free_parsestate(pstate);
+
+		CacheInvalidateRelcache(relation);
+	}
+	else
+	{
+		if (SearchSysCacheExists1(RLSRELID, ObjectIdGetDatum(relationId)))
+		{
+			ObjectAddress	address;
+
+			address.classId = RowLevelSecurityRelationId;
+			address.objectId = RelationGetRelid(relation);
+			address.objectSubId = 0;
+
+			performDeletion(&address, DROP_RESTRICT, 0);
+
+			CacheInvalidateRelcache(relation);
+		}
+		else
+		{
+			/* nothing to do here */
+			elog(INFO, "relation %s has no row-level security policy, skipped",
+				 RelationGetRelationName(relation));
+		}
+	}
+	heap_close(rls_rel, RowExclusiveLock);
+}
+
+/*
+ * Guts of Row-level security policy deletion.
+ */
+void
+RemoveRowLvSecById(Oid relationId)
+{
+	Relation	rls_rel;
+	HeapTuple	tuple;
+
+	rls_rel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCache1(RLSRELID, ObjectIdGetDatum(relationId));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for row-level security policy %u",
+			 relationId);
+
+	simple_heap_delete(rls_rel, &tuple->t_self);
+
+	ReleaseSysCache(tuple);
+
+	heap_close(rls_rel, RowExclusiveLock);
+}
+
 /*
  * ALTER FOREIGN TABLE <name> OPTIONS (...)
  */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index df76341..f833e7f 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -388,6 +388,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	expand_inherited_tables(root);
 
 	/*
+	 * Apply row-level security policy of tables, if configured.
+	 * It has to be processed after inherited tables are expanded, because
+	 * row-level security policy has per-table basis.
+	 */
+	apply_rowlv_security(root);
+
+	/*
 	 * Set hasHavingQual to remember if HAVING clause is present.  Needed
 	 * because preprocess_expression will reduce a constant-true condition to
 	 * an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index 1af4e7f..8c42617 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -38,6 +38,44 @@
 static List *expand_targetlist(List *tlist, int command_type,
 				  Index result_relation, List *range_table);
 
+/*
+ * lookup_varattno
+ *
+ * This routine returns an attribute number to reference a particular
+ * attribute. In case when the target relation is really relation,
+ * we can reference arbitrary attribute (including system column)
+ * without any translations. However, we have to translate varattno
+ * of Vat that references sub-queries being originated from regular
+ * relations with row-level security policy.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+	RangeTblEntry  *rte = rt_fetch(rt_index, rtables);
+
+	if (rte->rtekind == RTE_SUBQUERY &&
+		rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+	{
+		ListCell   *cell;
+
+		foreach (cell, rte->subquery->targetList)
+		{
+			TargetEntry	*tle = lfirst(cell);
+			Var			*var;
+
+			if (IsA(tle->expr, Const))
+				continue;
+
+			var = (Var *) tle->expr;
+			Assert(IsA(var, Var));
+
+			if (var->varattno == attno)
+				return tle->resno;
+		}
+		elog(ERROR, "invalid attno %d on the pseudo targetList", attno);
+	}
+	return attno;
+}
 
 /*
  * preprocess_targetlist
@@ -62,7 +100,9 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 	{
 		RangeTblEntry *rte = rt_fetch(result_relation, range_table);
 
-		if (rte->subquery != NULL || rte->relid == InvalidOid)
+		if ((rte->subquery != NULL &&
+			 rte->subquery->querySource != QSRC_ROW_LEVEL_SECURITY) ||
+			rte->relid == InvalidOid)
 			elog(ERROR, "subquery cannot be result relation");
 	}
 
@@ -95,7 +135,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 		{
 			/* It's a regular table, so fetch its TID */
 			var = makeVar(rc->rti,
-						  SelfItemPointerAttributeNumber,
+						  lookup_varattno(SelfItemPointerAttributeNumber,
+										  rc->rti, range_table),
 						  TIDOID,
 						  -1,
 						  InvalidOid,
@@ -111,7 +152,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 			if (rc->isParent)
 			{
 				var = makeVar(rc->rti,
-							  TableOidAttributeNumber,
+							  lookup_varattno(TableOidAttributeNumber,
+											  rc->rti, range_table),
 							  OIDOID,
 							  -1,
 							  InvalidOid,
@@ -129,7 +171,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 			/* Not a table, so we need the whole row as a junk var */
 			var = makeWholeRowVar(rt_fetch(rc->rti, range_table),
 								  rc->rti,
-								  0,
+								  lookup_varattno(0, rc->rti, range_table),
 								  false);
 			snprintf(resname, sizeof(resname), "wholerow%u", rc->rowmarkId);
 			tle = makeTargetEntry((Expr *) var,
@@ -298,7 +340,9 @@ expand_targetlist(List *tlist, int command_type,
 					if (!att_tup->attisdropped)
 					{
 						new_expr = (Node *) makeVar(result_relation,
-													attrno,
+													lookup_varattno(attrno,
+														 result_relation,
+														 range_table),
 													atttype,
 													atttypmod,
 													attcollation,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index 6475633..415e841 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -1696,7 +1696,33 @@ adjust_appendrel_attrs_mutator(Node *node,
 					return (Node *) rowexpr;
 				}
 			}
-			/* system attributes don't need any other translation */
+			else
+			{
+				RangeTblEntry  *rte = rt_fetch(appinfo->child_relid,
+											   context->root->parse->rtable);
+				if (rte->rtekind == RTE_SUBQUERY &&
+					rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+				{
+					ListCell   *cell;
+
+					foreach (cell, rte->subquery->targetList)
+					{
+						TargetEntry	*subtle = lfirst(cell);
+						Var			*subvar;
+
+						if (IsA(subtle->expr, Const))
+							continue;
+
+						subvar = (Var *) subtle->expr;
+						if (var->varattno == subvar->varattno)
+						{
+							Assert(var->vartype == subvar->vartype);
+							Assert(var->vartypmod == subvar->vartypmod);
+							var->varattno = subtle->resno;
+						}
+					}
+				}
+			}
 		}
 		return (Node *) var;
 	}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..fe71c0c 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
-       relnode.o restrictinfo.o tlist.o var.o
+       relnode.o restrictinfo.o rowlvsec.o tlist.o var.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlvsec.c b/src/backend/optimizer/util/rowlvsec.c
new file mode 100644
index 0000000..d311bdd
--- /dev/null
+++ b/src/backend/optimizer/util/rowlvsec.c
@@ -0,0 +1,524 @@
+/*
+ * optimizer/util/rowlvsec.c
+ *    Row-level security support routines
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlvsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodes.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/primnodes.h"
+#include "optimizer/rowlvsec.h"
+#include "parser/parsetree.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/* hook to allow extensions to assign their own security policy */
+rowlv_security_hook_type rowlv_security_hook = NULL;
+
+/* current working mode of row-level security policy  */
+static RowLvSecMode rowlv_security_mode = RowLvSecEnabled;
+
+/*
+ * switch_rowlv_security_mode
+ *
+ * It switches the current performing mode of row-level security feature,
+ * and returns older mode. It enables to disable this functionality for
+ * temporary usage; such as foreign-key checks to prohibit update of PKs
+ * being referenced by others.
+ * The caller must ensure the saved older mode shall be restored, and
+ * it correctly works even if an error would be raised. A typical
+ * implementation encloses the code block with PG_TRY(), PG_CATCH() and
+ * PG_END_TRY() to catch errors and restore the saved mode prior to
+ * re-throw the error.
+ */
+RowLvSecMode
+switch_rowlv_security_mode(RowLvSecMode new_mode)
+{
+	RowLvSecMode	old_mode = rowlv_security_mode;
+
+	rowlv_security_mode = new_mode;
+
+	return old_mode;
+}
+
+typedef struct {
+	PlannerInfo	*root;
+	int		varlevelsup;
+} fixup_var_context;
+
+static bool
+fixup_var_references_walker(Node *node, fixup_var_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var			   *var = (Var *)node;
+		RangeTblEntry  *rte;
+
+		/* This variable is not reference the target sub-query */
+		/* XXX - add comment here for inherited columns */
+
+
+		/*
+		 * Does this Var node reference the Query node currently we focused
+		 * on. If not, we simply ignore it.
+		 */
+		if (var->varlevelsup != context->varlevelsup)
+			return false;
+
+		rte = rt_fetch(var->varno, context->root->parse->rtable);
+		if (!rte)
+			elog(ERROR, "invalid varno %d", var->varno);
+
+		if (rte->rtekind == RTE_SUBQUERY &&
+			rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+		{
+			List	   *targetList = rte->subquery->targetList;
+			ListCell   *cell;
+
+			foreach (cell, targetList)
+			{
+				TargetEntry	   *subtle = lfirst(cell);
+				Var			   *subvar;
+
+				if (IsA(subtle->expr, Const))
+					continue;
+
+				subvar = (Var *)subtle->expr;
+				Assert(IsA(subvar, Var));
+
+				if (var->varattno == subvar->varattno)
+				{
+					var->varattno = subtle->resno;
+					return false;
+				}
+			}
+			elog(ERROR, "invalid varattno %d", var->varattno);
+		}
+		return false;
+	}
+	else if (IsA(node, Query))
+	{
+		bool	result;
+
+		context->varlevelsup++;
+		result = query_tree_walker((Query *) node,
+								   fixup_var_references_walker,
+								   (void *) context, 0);
+		context->varlevelsup--;
+
+		return result;
+	}
+	return expression_tree_walker(node,
+								  fixup_var_references_walker,
+								  (void *) context);
+}
+
+/*
+ * fixup_varattnos
+ *
+ * It fixes up varattno of Var node that referenced the relation with RLS
+ * policy but replaced to a sub-query. Here is no guarantee varattno fits
+ * with TargetEntry's resno of the sub-query, so needs to fix up them.
+ */
+static void
+fixup_varattnos(PlannerInfo *root)
+{
+	fixup_var_context	context;
+	ListCell		   *cell;
+
+	/*
+	 * Fixup Var->varattno that references the sub-queries originated from
+	 * regular relations with RLS policy. 
+	 */
+	context.root = root;
+	context.varlevelsup = 0;
+
+	query_tree_walker(root->parse,
+					  fixup_var_references_walker,
+					  (void *) &context, 0);
+
+	/*
+	 * Fixup translated_vars of AppendRelInfo
+	 */
+	foreach (cell, root->append_rel_list)
+	{
+		AppendRelInfo  *appinfo = lfirst(cell);
+		RangeTblEntry  *prte;
+		RangeTblEntry  *crte;
+
+		prte = rt_fetch(appinfo->parent_relid, root->parse->rtable);
+		crte = rt_fetch(appinfo->child_relid,  root->parse->rtable);
+
+		/*
+		 * Parent relation entry (that has rte->inh = true) is never
+		 * expanded, so no need to adject order of translated_vars.
+		 */
+		Assert(!(prte->rtekind == RTE_SUBQUERY &&
+				 prte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY));
+
+		/*
+		 * In case when the child relation entry is a sub-query originated
+		 * from a regular relation with RLS policy, attribute number to
+		 * reference a particular column (especially, system column) was
+		 * changed, thus, its correspondence relationship should be fixed.
+		 */
+		if (crte->rtekind == RTE_SUBQUERY &&
+			crte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+		{
+			ListCell	   *l;
+			AttrNumber		attnum = 1;
+
+			/* Ensure to perform as a sub-query, not an inherited table */
+			appinfo->child_reltype = InvalidOid;
+
+			foreach (l, appinfo->translated_vars)
+			{
+				Var	   *var = lfirst(l);
+
+				if (var != NULL)
+				{
+					ListCell   *ll;
+
+					Assert(IsA(var, Var));
+
+					foreach (ll, crte->subquery->targetList)
+					{
+						TargetEntry	   *subtle = lfirst(ll);
+						Var			   *subvar;
+
+						/* skip references to dropped column */
+						if (IsA(subtle->expr, Const))
+							continue;
+
+						subvar = (Var *)subtle->expr;
+						Assert(IsA(subvar, Var));
+						/*
+						 * The "resno" of TargetEntry that has a reference to
+						 * the required column should be a corrent "varattno"
+						 * of Var node that reference a column from outside of
+						 * the sub-query.
+						 */
+						if (var->varattno == subvar->varattno)
+						{
+							var->varattno = subtle->resno;
+							break;
+						}
+					}
+					if (ll == NULL)
+						elog(ERROR, "invalid varattno %d", var->varattno);
+				}
+				attnum++;
+			}
+		}
+	}
+}
+
+/*
+ * make_pseudo_column
+ *
+ * It is a utility routine to generate a TargetEntry object that references
+ * a particular attribute of underlying relation. Its "expr" field shall
+ * have either Var or Const node. In case when the supplied "attnum" would
+ * be already dropped column, it injects a NULL value for a placeholder,
+ * according to the manner in preprocess_targetlist().
+ */
+static TargetEntry *
+make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+	HeapTuple			atttup;
+	Form_pg_attribute	attform;
+	Expr   *expr;
+	char   *resname;
+
+	Assert(attnum != InvalidAttrNumber);
+
+	atttup = SearchSysCache2(ATTNUM,
+							 ObjectIdGetDatum(subrte->relid),
+							 Int16GetDatum(attnum));
+	if (!HeapTupleIsValid(atttup))
+		elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+			 attnum, subrte->relid);
+	attform = (Form_pg_attribute) GETSTRUCT(atttup);
+
+	if (attform->attisdropped)
+	{
+		/* Insert NULL just for a placeholder for dropped column */
+		expr = (Expr *) makeConst(INT4OID,
+								  -1,
+								  InvalidOid,
+								  sizeof(int32),
+								  (Datum) 0,
+								  true,		/* isnull */
+								  true		/* byval */ );
+	}
+	else
+	{
+		expr = (Expr *) makeVar((Index) 1,
+								attform->attnum,
+								attform->atttypid,
+								attform->atttypmod,
+								InvalidOid,
+								0);
+	}
+	resname = pstrdup(NameStr(attform->attname));
+
+	ReleaseSysCache(atttup);
+
+	return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * pull_rowlv_security_policy
+ *
+ * This routine pulls an expression node from pg_rowlvsec system
+ * catalog, if configured. In addition to the configured qualifier,
+ * it also allows extensions to append its own row-level policy.
+ */
+static Expr *
+pull_rowlv_security_policy(PlannerInfo *root, RangeTblEntry *rte)
+{
+	HeapTuple	tuple;
+	Expr	   *qual_expr = NULL;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+	tuple = SearchSysCache1(RLSRELID, ObjectIdGetDatum(rte->relid));
+	if (HeapTupleIsValid(tuple))
+	{
+		Datum	datum;
+		bool	isnull;
+		char   *sepol_string;
+		Expr   *super_expr;
+		Expr   *sepol_expr;
+
+		datum = SysCacheGetAttr(RLSRELID, tuple,
+								Anum_pg_rowlvsec_rlsqual, &isnull);
+		Assert(!isnull);
+
+		/*
+		 * XXX - Row-level security policy has the following form:
+		 *   (( user-supplied-condition ) OR has_superuser_privilege())
+		 * that allows superusers to bypass all the controls, and make
+		 * sure complete backup.
+		 */
+		sepol_string = TextDatumGetCString(datum);
+		sepol_expr = (Expr *) stringToNode(sepol_string);
+		Assert(exprType((Node *) sepol_expr) == BOOLOID);
+
+		super_expr = (Expr *) makeFuncExpr(F_HAS_SUPERUSER_PRIVILEGE,
+										   BOOLOID, NIL,
+										   InvalidOid,
+										   InvalidOid,
+										   COERCE_DONTCARE);
+		qual_expr = makeBoolExpr(OR_EXPR,
+								 list_make2(sepol_expr,
+											super_expr), -1);
+		pfree(sepol_string);
+
+		ReleaseSysCache(tuple);
+	}
+
+	/*
+	 * This hook allows extensitons to append its own row-level security
+	 * policy function. If qual_expr is not NULL, it should be connected
+	 * with AND operator.
+	 */
+	if (rowlv_security_hook)
+		qual_expr = (*rowlv_security_hook)(root, rte, qual_expr);
+
+	return qual_expr;
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * It replaces a particular RangeTblEntry to the relation with RLS policy
+ * by a simple sub-query that references the relation with WHERE clause
+ * to perform as row-level security policy.
+ */
+static void
+expand_rtentry_with_policy(PlannerInfo *root,
+						   RangeTblEntry *rte,
+						   Expr *qual_expr)
+{
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+	TargetEntry	   *subtle;
+	HeapTuple		reltup;
+	Form_pg_class	relform;
+	AttrNumber		attno;
+	List		   *targetList = NIL;
+	List		   *colNameList = NIL;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+	/*
+	 * Construct a sub-query structure
+	 */
+	subqry = (Query *) makeNode(Query);
+	subqry->commandType = CMD_SELECT;
+	subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	subrte = copyObject(rte);
+	subrtr = makeNode(RangeTblRef);
+	subrtr->rtindex = 1;
+
+	subqry->rtable = list_make1(subrte);
+	subqry->jointree = makeFromExpr(list_make1(subrtr),
+									(Node *) qual_expr);
+
+	/*
+	 * Construct pseudo columns as TargetEntry of sub-query that reference
+	 * a particular attribute of the underlying relation.
+	 */
+	reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(subrte->relid));
+	if (!HeapTupleIsValid(reltup))
+		elog(ERROR, "cache lookup failed for relation %u", subrte->relid);
+	relform = (Form_pg_class) GETSTRUCT(reltup);
+
+	/*
+	 * XXX - the pseudo columns that just references regular attribute of 
+	 * underlying table must has identical resno with attnum of the column,
+	 * because preprocess_targetlist() sort-out target entries according to
+	 * the attribute number to fit physical structure of the target relation
+	 * in case when it has inherited tables.
+	 *
+	 * Likely, it can be optimized to have minimum number of TargetEntry
+	 * except for a case of the target relation of UPDATE. But not yet.
+	 */
+	for (attno = 1; attno <= relform->relnatts; attno++)
+	{
+		subtle = make_pseudo_column(subrte, attno);
+		subtle->resno = list_length(targetList) + 1;
+		Assert(attno == subtle->resno);
+		targetList = lappend(targetList, subtle);
+		colNameList = lappend(colNameList,
+							  makeString(pstrdup(subtle->resname)));
+	}
+
+	/*
+	 * Also, system columns and whole-row-reference are also added.
+	 */
+	for (attno = FirstLowInvalidHeapAttributeNumber + 1; attno < 0; attno++)
+	{
+		if (attno == ObjectIdAttributeNumber && !relform->relhasoids)
+			continue;
+
+		subtle = make_pseudo_column(subrte, attno);
+		subtle->resno = list_length(targetList) + 1;
+
+		targetList = lappend(targetList, subtle);
+        colNameList = lappend(colNameList,
+                              makeString(pstrdup(subtle->resname)));
+	}
+	subqry->targetList = targetList;
+	ReleaseSysCache(reltup);
+
+	/*
+	 * Replace this RengeTblEntry by the sub-query with security-barrier
+	 */
+	rte->rtekind = RTE_SUBQUERY;
+	rte->subquery = subqry;
+	rte->security_barrier = true;
+
+	/* no permission check on subquery itself */
+	rte->requiredPerms = 0;
+	rte->checkAsUser = InvalidOid;
+	rte->selectedCols = NULL;
+	rte->modifiedCols = NULL;
+
+	rte->alias = NULL;
+	rte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+}
+
+/*
+ * apply_rowlv_security
+ *
+ * It tries to apply row-level security policy of the relation.
+ * If and when a particular policy is configured on the referenced
+ * relation, it shall be replaced by a sub-query with security-barrier flag;
+ * that references the relation with row-level security policy.
+ * In the result, all users can see is rows of the relation that satisfies
+ * the condition supplied as security policy.
+ */
+void
+apply_rowlv_security(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	ListCell   *cell;
+	Index		rtindex;
+	bool		needs_fixup = false;
+
+	/* Mode checks */
+	if (rowlv_security_mode == RowLvSecDisabled)
+		return;
+
+	/*
+	 * No need to apply row-level security on sub-query being originated
+	 * from regular relation with RLS policy any more.
+	 */
+	if (parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+		return;
+
+	rtindex = 0;
+	foreach (cell, parse->rtable)
+	{
+		RangeTblEntry  *rte = lfirst(cell);
+		Expr		   *qual_expr;
+
+		rtindex++;
+
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+		/*
+		 * Parent relation of inheritance tree is just a placeholder here,
+		 * so no need to apply row-level security them.
+		 */
+		if (rte->inh)
+			continue;
+
+		/*
+		 * It is unavailable to check rows to be inserted here,
+		 * before-row-insert trigger should be utilized instead.
+		 */
+		if (parse->commandType == CMD_INSERT &&
+			parse->resultRelation == rtindex)
+			continue;
+
+		/*
+		 * In case when row-level security policy was configured on the
+		 * table referenced by this RangeTblEntry, it shall be rewritten
+		 * to sub-query with the policy
+		 */
+		qual_expr = pull_rowlv_security_policy(root, rte);
+		if (qual_expr)
+		{
+			expand_rtentry_with_policy(root, rte, qual_expr);
+			needs_fixup = true;
+		}
+	}
+
+	/*
+	 * Since the relation with RLS policy was replaced by a sub-query,
+	 * thus resource number to reference a particular column can be
+	 * also moditifed. If we applied RLS policy on one or more relations,
+	 * varattno of Var node that has referenced the rewritten relation
+	 * needs to be fixed up.
+	 */
+	if (needs_fixup)
+		fixup_varattnos(root);
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9eb1bed..e9c8d5b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2058,6 +2058,22 @@ alter_table_cmd:
 					n->def = (Node *)$2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> SET ROW LEVEL SECURITY (clause) */
+			| SET ROW LEVEL SECURITY '(' a_expr ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_SetRowLvSecurity;
+					n->def = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> RESET ROW LEVEL SECURITY */
+			| RESET ROW LEVEL SECURITY
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_ResetRowLvSecurity;
+					n->def = NULL;
+					$$ = (Node *)n;
+				}
 			| alter_generic_options
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 77322a1..bc34f1c 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -2863,6 +2863,40 @@ convert_column_priv_string(text *priv_type_text)
 	return convert_any_priv_string(priv_type_text, column_priv_map);
 }
 
+/*
+ * has_superuser_privilege
+ *     checks superuser privilege of current user
+ */
+Datum
+has_superuser_privilege(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_BOOL(superuser());
+}
+
+/*
+ * has_superuser_privilege_name
+ *     checks superuser privilege of the given username
+ */
+Datum
+has_superuser_privilege_name(PG_FUNCTION_ARGS)
+{
+	Name	username = PG_GETARG_NAME(0);
+	Oid		roleid = get_role_oid_or_public(NameStr(*username));
+
+	PG_RETURN_BOOL(superuser_arg(roleid));
+}
+
+/*
+ * has_superuser_privilege_id
+ *     checks superuser privilege of the given user-id
+ */
+Datum
+has_superuser_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid		roleid = PG_GETARG_OID(0);
+
+	PG_RETURN_BOOL(superuser_arg(roleid));
+}
 
 /*
  * has_database_privilege variants
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index dd58f4e..69fa60a 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -35,10 +35,12 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_rowlvsec.h"
 #include "catalog/pg_type.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
+#include "optimizer/rowlvsec.h"
 #include "parser/parse_coerce.h"
 #include "parser/parse_relation.h"
 #include "miscadmin.h"
@@ -3341,6 +3343,7 @@ ri_PerformCheck(RI_QueryKey *qkey, SPIPlanPtr qplan,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
+	RowLvSecMode	save_rls_mode;
 	Datum		vals[RI_MAX_NUMKEYS * 2];
 	char		nulls[RI_MAX_NUMKEYS * 2];
 
@@ -3424,11 +3427,31 @@ ri_PerformCheck(RI_QueryKey *qkey, SPIPlanPtr qplan,
 	SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
 						   save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
 
-	/* Finally we can run the query. */
-	spi_result = SPI_execute_snapshot(qplan,
-									  vals, nulls,
-									  test_snapshot, crosscheck_snapshot,
-									  false, false, limit);
+	/*
+	 * Switch row-level security model to prevent unexpected update or
+	 * delete of PKs being referenced by invisible tuples in FK-side.
+	 * However, we don't need to have special treatment towards INSERT
+	 * or UPDATE FKs that tries to reference invisible PKs.
+	 */
+	save_rls_mode = switch_rowlv_security_mode(query_rel == fk_rel
+											   ? RowLvSecDisabled
+											   : RowLvSecEnabled);
+	PG_TRY();
+	{
+		/* Finally we can run the query. */
+		spi_result = SPI_execute_snapshot(qplan,
+										  vals, nulls,
+										  test_snapshot,
+										  crosscheck_snapshot,
+										  false, false, limit);
+	}
+	PG_CATCH();
+	{
+		switch_rowlv_security_mode(save_rls_mode);
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+	switch_rowlv_security_mode(save_rls_mode);
 
 	/* Restore UID and security context */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index c365ec7..f34bfa5 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -45,6 +45,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_range.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlvsec.h"
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_ts_config.h"
@@ -588,6 +589,17 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		1024
 	},
+	{RowLevelSecurityRelationId,	/* RLSRELID */
+		RowLevelSecurityIndexId,
+		1,
+		{
+			Anum_pg_rowlvsec_rlsrelid,
+			0,
+			0,
+			0
+		},
+		128
+	},
 	{RewriteRelationId,			/* RULERELNAME */
 		RewriteRelRulenameIndexId,
 		2,
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index f0eb564..37af71d 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -146,6 +146,7 @@ typedef enum ObjectClass
 	OCLASS_USER_MAPPING,		/* pg_user_mapping */
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
+	OCLASS_ROWLVSEC,			/* pg_rowlvsec */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 450ec25..6c0ca5e 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -306,6 +306,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
 DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
 #define RangeTypidIndexId					3542
 
+DECLARE_UNIQUE_INDEX(pg_rowlvsec_relid_index, 3839, on pg_rowlvsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId			3839
+
 /* last step of initialization script: build the indexes declared above */
 BUILD_INDICES
 
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 1e097dd..3d970fc 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -3279,6 +3279,13 @@ DESCR("(internal)");
 DATA(insert OID = 2248 ( fmgr_sql_validator PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 2278 "26" _null_ _null_ _null_ _null_ fmgr_sql_validator _null_ _null_ _null_ ));
 DESCR("(internal)");
 
+DATA(insert OID = 3819 (  has_superuser_privilege	PGNSP PGUID 12 1 0 0 0 f f f f t f s 0 0 16 "" _null_ _null_ _null_ _null_	has_superuser_privilege _null_ _null_ _null_ ));
+DESCR("superuser privilege of current user");
+DATA(insert OID = 3813 (  has_superuser_privilege	PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 16 "19" _null_ _null_ _null_ _null_	has_superuser_privilege_name _null_ _null_ _null_ ));
+DESCR("superuser privilege by user name");
+DATA(insert OID = 3814 (  has_superuser_privilege	PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 16 "26" _null_ _null_ _null_ _null_	has_superuser_privilege_id _null_ _null_ _null_ ));
+DESCR("superuser privilege by user oid");
+
 DATA(insert OID = 2250 (  has_database_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s 3 0 16 "19 25 25" _null_ _null_ _null_ _null_	has_database_privilege_name_name _null_ _null_ _null_ ));
 DESCR("user privilege on database by username, database name");
 DATA(insert OID = 2251 (  has_database_privilege		   PGNSP PGUID 12 1 0 0 0 f f f f t f s 3 0 16 "19 26 25" _null_ _null_ _null_ _null_	has_database_privilege_name_id _null_ _null_ _null_ ));
diff --git a/src/include/catalog/pg_rowlvsec.h b/src/include/catalog/pg_rowlvsec.h
new file mode 100644
index 0000000..629c024
--- /dev/null
+++ b/src/include/catalog/pg_rowlvsec.h
@@ -0,0 +1,39 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlvsec.h
+ *	  definition of the system row-level security policy relation (pg_rowlvsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef PG_ROWLVSEC_H
+#define PG_ROWLVSEC_H
+
+#include "catalog/genbki.h"
+
+/* ----------------
+ *		pg_rowlvsec definition.  cpp turns this into
+ *		typedef struct FormData_pg_rowlvsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId	3838
+
+CATALOG(pg_rowlvsec,3838) BKI_WITHOUT_OIDS
+{
+	Oid			rlsrelid;	/* OID of the relation with RLS policy */
+#ifdef CATALOG_VARLEN
+	text		rlsqual;	/* Expression tree of the security policy */
+#endif
+} Form_pg_rowlvsec;
+
+/* ----------------
+ *		compiler constants for pg_rowlvsec
+ * ----------------
+ */
+#define Natts_pg_rowlvsec			2
+#define Anum_pg_rowlvsec_rlsrelid	1
+#define Anum_pg_rowlvsec_rlsqual	2
+
+#endif   /* PG_ROWLVSEC_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 9ceb086..66c02a9 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -40,6 +40,8 @@ extern void AlterRelationNamespaceInternal(Relation classRel, Oid relOid,
 							   Oid oldNspOid, Oid newNspOid,
 							   bool hasDependEntry);
 
+extern void RemoveRowLvSecById(Oid relationId);
+
 extern void CheckTableNotInUse(Relation rel, const char *stmt);
 
 extern void ExecuteTruncate(TruncateStmt *stmt);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index deff1a3..69726e2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
 	QSRC_PARSER,				/* added by parse analysis (now unused) */
 	QSRC_INSTEAD_RULE,			/* added by unconditional INSTEAD rule */
 	QSRC_QUAL_INSTEAD_RULE,		/* added by conditional INSTEAD rule */
-	QSRC_NON_INSTEAD_RULE		/* added by non-INSTEAD rule */
+	QSRC_NON_INSTEAD_RULE,		/* added by non-INSTEAD rule */
+	QSRC_ROW_LEVEL_SECURITY,	/* added by row-level security */
 } QuerySource;
 
 /* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -1227,6 +1228,8 @@ typedef enum AlterTableType
 	AT_DropInherit,				/* NO INHERIT parent */
 	AT_AddOf,					/* OF <type_name> */
 	AT_DropOf,					/* NOT OF */
+	AT_SetRowLvSecurity,		/* SET ROW LEVEL SECURITY (...) */
+	AT_ResetRowLvSecurity,		/* RESET ROW LEVEL SECURITY (...) */
 	AT_GenericOptions			/* OPTIONS (...) */
 } AlterTableType;
 
diff --git a/src/include/optimizer/rowlvsec.h b/src/include/optimizer/rowlvsec.h
new file mode 100644
index 0000000..92b912f
--- /dev/null
+++ b/src/include/optimizer/rowlvsec.h
@@ -0,0 +1,31 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlvsec.h
+ *    prototypes for rowlvsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLVSEC_H
+#define ROWLVSEC_H
+
+#include "nodes/relation.h"
+
+typedef Expr *(*rowlv_security_hook_type)(PlannerInfo *root,
+										  RangeTblEntry *rte,
+										  Expr *qual_expr);
+extern PGDLLIMPORT rowlv_security_hook_type rowlv_security_hook;
+
+typedef enum
+{
+	RowLvSecEnabled,
+	RowLvSecDisabled,
+} RowLvSecMode;
+
+extern RowLvSecMode switch_rowlv_security_mode(RowLvSecMode new_mode);
+
+extern void apply_rowlv_security(PlannerInfo *root);
+
+#endif	/* ROWLVSEC_H */
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d59dd4e..4caf971 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -73,6 +73,7 @@ enum SysCacheIdentifier
 	RANGETYPE,
 	RELNAMENSP,
 	RELOID,
+	RLSRELID,
 	RULERELNAME,
 	STATRELATTINH,
 	TABLESPACEOID,
diff --git a/src/test/regress/expected/rowlvsec.out b/src/test/regress/expected/rowlvsec.out
new file mode 100644
index 0000000..9ae7454
--- /dev/null
+++ b/src/test/regress/expected/rowlvsec.out
@@ -0,0 +1,438 @@
+--
+-- Test Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rowlvsec_user1;
+DROP USER IF EXISTS rowlvsec_user2;
+DROP USER IF EXISTS rowlvsec_user3;
+DROP SCHEMA IF EXISTS rowlvsec_schema CASCADE;
+RESET client_min_messages;
+-- Initial Setup
+CREATE USER rowlvsec_user0;
+CREATE USER rowlvsec_user1;
+CREATE USER rowlvsec_user2;
+CREATE USER rowlvsec_user3;
+CREATE SCHEMA rowlvsec_schema;
+GRANT ALL ON SCHEMA rowlvsec_schema TO public;
+SET search_path = rowlvsec_schema,public;
+CREATE TABLE customer (
+       cid      name primary key,
+       cname    text,
+       cage	int,
+       caddr	text
+);
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "customer_pkey" for table "customer"
+GRANT SELECT ON TABLE customer TO public;
+INSERT INTO customer VALUES
+       ('rowlvsec_user1', 'alice', 18, 'USA'),
+       ('rowlvsec_user2', 'bob',   24, 'Japan'),
+       ('rowlvsec_user3', 'carol', 29, 'USA'),
+       ('rowlvsec_user4', 'dave',  32, 'Germany');
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+    COST 0.0000001 LANGUAGE plpgsql
+    AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- XXX sub-query in RLS policy should be supported in the future
+CREATE FUNCTION customer_age() RETURNS int STABLE LANGUAGE sql
+       AS 'SELECT cage FROM customer WHERE cid = session_user';
+GRANT EXECUTE ON FUNCTION customer_age() TO public;
+CREATE FUNCTION customer_name() RETURNS text STABLE LANGUAGE sql
+       AS 'SELECT cname FROM customer WHERE cid = session_user';
+GRANT EXECUTE ON FUNCTION customer_age() TO public;
+CREATE FUNCTION customer_addr() RETURNS text STABLE LANGUAGE sql
+       AS 'SELECT caddr FROM customer WHERE cid = session_user';
+GRANT EXECUTE ON FUNCTION customer_addr() TO public;
+-- Create Test Data
+SET SESSION AUTHORIZATION rowlvsec_user0;
+CREATE TABLE drink (
+       id	   int primary key,
+       name	   text,
+       price	   int,
+       alcohol	   bool,
+       madein      text
+);
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "drink_pkey" for table "drink"
+GRANT ALL ON drink TO public;
+INSERT INTO drink VALUES
+       (10, 'water',  100, false, 'Japan'),
+       (20, 'juice',  180, false, 'Germany'),
+       (30, 'coke',   120, false, 'USA'),
+       (40, 'coffee', 200, false, 'USA'),
+       (50, 'beer',   280, true,  'Germany'),
+       (60, 'wine',   340, true,  'USA'),
+       (70, 'sake',   300, true,  'Japan');
+CREATE TABLE purchase (
+       id          int references drink(id),
+       cname       text default customer_name(),
+       ymd         date
+);
+GRANT ALL ON purchase TO public;
+INSERT INTO purchase VALUES
+       (10, 'alice', '2012-05-03'),
+       (10, 'bob',   '2012-05-04'),
+       (10, 'alice', '2012-05-08'),
+       (20, 'alice', '2012-05-15'),
+       (20, 'bob',   '2012-05-18'),
+       (20, 'carol', '2012-05-21'),
+       (30, 'alice', '2012-05-28'),
+       (40, 'bob',   '2012-06-02'),
+       (60, 'carol', '2012-06-10');
+ALTER TABLE drink SET ROW LEVEL SECURITY (customer_age() > 19 OR NOT alcohol);
+ALTER TABLE purchase SET ROW LEVEL SECURITY (cname = customer_name());
+-- Viewpoint from Alice
+SET SESSION AUTHORIZATION rowlvsec_user1;
+SELECT * FROM drink WHERE f_leak(name);
+NOTICE:  f_leak => water
+NOTICE:  f_leak => juice
+NOTICE:  f_leak => coke
+NOTICE:  f_leak => coffee
+ id |  name  | price | alcohol | madein  
+----+--------+-------+---------+---------
+ 10 | water  |   100 | f       | Japan
+ 20 | juice  |   180 | f       | Germany
+ 30 | coke   |   120 | f       | USA
+ 40 | coffee |   200 | f       | USA
+(4 rows)
+
+SELECT * FROM purchase;
+ id | cname |    ymd     
+----+-------+------------
+ 10 | alice | 05-03-2012
+ 10 | alice | 05-08-2012
+ 20 | alice | 05-15-2012
+ 30 | alice | 05-28-2012
+(4 rows)
+
+SELECT * FROM drink NATURAL JOIN purchase;
+ id | name  | price | alcohol | madein  | cname |    ymd     
+----+-------+-------+---------+---------+-------+------------
+ 10 | water |   100 | f       | Japan   | alice | 05-03-2012
+ 10 | water |   100 | f       | Japan   | alice | 05-08-2012
+ 20 | juice |   180 | f       | Germany | alice | 05-15-2012
+ 30 | coke  |   120 | f       | USA     | alice | 05-28-2012
+(4 rows)
+
+-- Viewpoint from Bob
+SET SESSION AUTHORIZATION rowlvsec_user2;
+SELECT * FROM drink;
+ id |  name  | price | alcohol | madein  
+----+--------+-------+---------+---------
+ 10 | water  |   100 | f       | Japan
+ 20 | juice  |   180 | f       | Germany
+ 30 | coke   |   120 | f       | USA
+ 40 | coffee |   200 | f       | USA
+ 50 | beer   |   280 | t       | Germany
+ 60 | wine   |   340 | t       | USA
+ 70 | sake   |   300 | t       | Japan
+(7 rows)
+
+SELECT * FROM purchase WHERE f_leak(cname);
+NOTICE:  f_leak => bob
+NOTICE:  f_leak => bob
+NOTICE:  f_leak => bob
+ id | cname |    ymd     
+----+-------+------------
+ 10 | bob   | 05-04-2012
+ 20 | bob   | 05-18-2012
+ 40 | bob   | 06-02-2012
+(3 rows)
+
+SELECT * FROM drink NATURAL JOIN purchase;
+ id |  name  | price | alcohol | madein  | cname |    ymd     
+----+--------+-------+---------+---------+-------+------------
+ 10 | water  |   100 | f       | Japan   | bob   | 05-04-2012
+ 20 | juice  |   180 | f       | Germany | bob   | 05-18-2012
+ 40 | coffee |   200 | f       | USA     | bob   | 06-02-2012
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM drink WHERE f_leak(name);
+                                      QUERY PLAN                                       
+---------------------------------------------------------------------------------------
+ Subquery Scan on drink
+   Filter: f_leak(drink.name)
+   ->  Seq Scan on drink
+         Filter: ((customer_age() > 19) OR (NOT alcohol) OR has_superuser_privilege())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM purchase WHERE f_leak(cname);
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Subquery Scan on purchase
+   Filter: f_leak(purchase.cname)
+   ->  Seq Scan on purchase
+         Filter: ((cname = customer_name()) OR has_superuser_privilege())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM drink NATURAL JOIN purchase;
+                                            QUERY PLAN                                             
+---------------------------------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (rowlvsec_schema.purchase.id = drink.id)
+   ->  Seq Scan on purchase
+         Filter: ((cname = customer_name()) OR has_superuser_privilege())
+   ->  Hash
+         ->  Subquery Scan on drink
+               ->  Seq Scan on drink
+                     Filter: ((customer_age() > 19) OR (NOT alcohol) OR has_superuser_privilege())
+(8 rows)
+
+ALTER TABLE drink RESET ROW LEVEL SECURITY;  -- failed
+ERROR:  must be owner of relation drink
+ALTER TABLE drink SET ROW LEVEL SECURITY (customer_name() = 'bob'); -- failed
+ERROR:  must be owner of relation drink
+-- Change the security policy
+SET SESSION AUTHORIZATION rowlvsec_user0;
+ALTER TABLE drink SET ROW LEVEL SECURITY (madein = customer_addr());
+-- Viewpoint from Alice again
+SET SESSION AUTHORIZATION rowlvsec_user1;
+SELECT * FROM drink WHERE id > 30;
+ id |  name  | price | alcohol | madein 
+----+--------+-------+---------+--------
+ 40 | coffee |   200 | f       | USA
+ 60 | wine   |   340 | t       | USA
+(2 rows)
+
+-- Viewpoint from Bob again
+SET SESSION AUTHORIZATION rowlvsec_user2;
+SELECT * FROM drink WHERE f_leak(name);
+NOTICE:  f_leak => water
+NOTICE:  f_leak => sake
+ id | name  | price | alcohol | madein 
+----+-------+-------+---------+--------
+ 10 | water |   100 | f       | Japan
+ 70 | sake  |   300 | t       | Japan
+(2 rows)
+
+-- Writer Access and PK&FK constraints
+SET SESSION AUTHORIZATION rowlvsec_user1;
+SELECT * FROM drink NATURAL FULL OUTER JOIN purchase;
+ id |  name  | price | alcohol | madein | cname |    ymd     
+----+--------+-------+---------+--------+-------+------------
+ 10 |        |       |         |        | alice | 05-03-2012
+ 10 |        |       |         |        | alice | 05-08-2012
+ 20 |        |       |         |        | alice | 05-15-2012
+ 30 | coke   |   120 | f       | USA    | alice | 05-28-2012
+ 40 | coffee |   200 | f       | USA    |       | 
+ 60 | wine   |   340 | t       | USA    |       | 
+(6 rows)
+
+BEGIN;
+UPDATE purchase SET ymd = ymd + 1 RETURNING *;
+ id | cname |    ymd     
+----+-------+------------
+ 10 | alice | 05-04-2012
+ 10 | alice | 05-09-2012
+ 20 | alice | 05-16-2012
+ 30 | alice | 05-29-2012
+(4 rows)
+
+EXPLAIN (costs off) UPDATE purchase SET ymd = ymd + 1;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Update on purchase
+   ->  Subquery Scan on purchase
+         ->  Seq Scan on purchase
+               Filter: ((cname = customer_name()) OR has_superuser_privilege())
+(4 rows)
+
+ABORT;
+BEGIN;
+DELETE FROM purchase RETURNING *;
+ id | cname |    ymd     
+----+-------+------------
+ 10 | alice | 05-03-2012
+ 10 | alice | 05-08-2012
+ 20 | alice | 05-15-2012
+ 30 | alice | 05-28-2012
+(4 rows)
+
+EXPLAIN (costs off) DELETE FROM purchase;
+                                   QUERY PLAN                                   
+--------------------------------------------------------------------------------
+ Delete on purchase
+   ->  Subquery Scan on purchase
+         ->  Seq Scan on purchase
+               Filter: ((cname = customer_name()) OR has_superuser_privilege())
+(4 rows)
+
+ABORT;
+-- Failed to update PK row referenced by invisible FK
+UPDATE drink SET id = 100 WHERE id = 60;  -- fail
+ERROR:  update or delete on table "drink" violates foreign key constraint "purchase_id_fkey" on table "purchase"
+DETAIL:  Key (id)=(60) is still referenced from table "purchase".
+DELETE FROM drink WHERE id = 40;          -- fail
+ERROR:  update or delete on table "drink" violates foreign key constraint "purchase_id_fkey" on table "purchase"
+DETAIL:  Key (id)=(40) is still referenced from table "purchase".
+-- Failed to insert FK to reference invisible PK
+INSERT INTO purchase (id, ymd) VALUES (20, '2012-06-12');  -- fail
+ERROR:  insert or update on table "purchase" violates foreign key constraint "purchase_id_fkey"
+DETAIL:  Key (id)=(20) is not present in table "drink".
+INSERT INTO purchase (id, ymd) VALUES (30, '2012-06-12');  -- OK
+-- Same writter access from Bob
+SET SESSION AUTHORIZATION rowlvsec_user2;
+SELECT * FROM drink NATURAL FULL OUTER JOIN purchase;
+ id | name  | price | alcohol | madein | cname |    ymd     
+----+-------+-------+---------+--------+-------+------------
+ 10 | water |   100 | f       | Japan  | bob   | 05-04-2012
+ 20 |       |       |         |        | bob   | 05-18-2012
+ 40 |       |       |         |        | bob   | 06-02-2012
+ 70 | sake  |   300 | t       | Japan  |       | 
+(4 rows)
+
+BEGIN;
+UPDATE purchase SET ymd = ymd + 1 RETURNING *;
+ id | cname |    ymd     
+----+-------+------------
+ 10 | bob   | 05-05-2012
+ 20 | bob   | 05-19-2012
+ 40 | bob   | 06-03-2012
+(3 rows)
+
+ABORT;
+BEGIN;
+DELETE FROM purchase RETURNING *;
+ id | cname |    ymd     
+----+-------+------------
+ 10 | bob   | 05-04-2012
+ 20 | bob   | 05-18-2012
+ 40 | bob   | 06-02-2012
+(3 rows)
+
+ABORT;
+-- Table inheritance and RLS policy
+SET SESSION AUTHORIZATION rowlvsec_user0;
+CREATE TABLE t1 (a int, b text, c text);
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+INSERT INTO t1 VALUES
+       (1,'aaa'), (2,'bbb'), (3,'ccc'), (4,'ddd'), ( 5,'eee'),
+       (6,'fff'), (7,'ggg'), (8,'hhh'), (9,'iii'), (10,'jjj');
+CREATE TABLE t2 (x int) INHERITS (t1);
+INSERT INTO t2 VALUES
+       (1,'abc'), (2,'bcd'), (3,'cde'), (4,'def'), ( 5,'efg'),
+       (6,'fgh'), (7,'ghi'), (8,'hij'), (9,'ijk'), (10,'jkl');
+CREATE TABLE t3 (y int) INHERITS (t1);
+INSERT INTO t3 VALUES (1,'xxx'),(2,'yyy'),(3,'zzz'),(4,'xyz');
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a     % 4 = customer_age() % 4);
+ALTER TABLE t2 SET ROW LEVEL SECURITY ((a+2) % 4 = customer_age() % 4);
+-- viewpoint from Alice
+SET SESSION AUTHORIZATION rowlvsec_user1;
+SELECT * FROM t1;
+ a  |  c  
+----+-----
+  2 | bbb
+  6 | fff
+ 10 | jjj
+  4 | def
+  8 | hij
+  1 | xxx
+  2 | yyy
+  3 | zzz
+  4 | xyz
+(9 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+                                            QUERY PLAN                                             
+---------------------------------------------------------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1
+                     Filter: (((a % 4) = (customer_age() % 4)) OR has_superuser_privilege())
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t1
+                     Filter: ((((a + 2) % 4) = (customer_age() % 4)) OR has_superuser_privilege())
+         ->  Seq Scan on t3 t1
+(9 rows)
+
+BEGIN;
+UPDATE t1 SET c = c || '_updt';
+EXPLAIN (costs off) UPDATE t1 SET c = c || '_updt';
+                                         QUERY PLAN                                          
+---------------------------------------------------------------------------------------------
+ Update on t1
+   ->  Subquery Scan on t1
+         ->  Seq Scan on t1
+               Filter: (((a % 4) = (customer_age() % 4)) OR has_superuser_privilege())
+   ->  Subquery Scan on t2
+         ->  Seq Scan on t2 t1
+               Filter: ((((a + 2) % 4) = (customer_age() % 4)) OR has_superuser_privilege())
+   ->  Seq Scan on t3 t1
+(8 rows)
+
+ABORT;
+BEGIN;
+DELETE FROM t1;
+EXPLAIN (costs off) DELETE FROM t1;
+                                         QUERY PLAN                                          
+---------------------------------------------------------------------------------------------
+ Delete on t1
+   ->  Subquery Scan on t1
+         ->  Seq Scan on t1
+               Filter: (((a % 4) = (customer_age() % 4)) OR has_superuser_privilege())
+   ->  Subquery Scan on t2
+         ->  Seq Scan on t2 t1
+               Filter: ((((a + 2) % 4) = (customer_age() % 4)) OR has_superuser_privilege())
+   ->  Seq Scan on t3 t1
+(8 rows)
+
+ABORT;
+-- viewpoint from Carol
+SET SESSION AUTHORIZATION rowlvsec_user3;
+SELECT * FROM t1;
+ a |  c  
+---+-----
+ 1 | aaa
+ 5 | eee
+ 9 | iii
+ 3 | cde
+ 7 | ghi
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+ 4 | xyz
+(9 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+                                            QUERY PLAN                                             
+---------------------------------------------------------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1
+                     Filter: (((a % 4) = (customer_age() % 4)) OR has_superuser_privilege())
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t1
+                     Filter: ((((a + 2) % 4) = (customer_age() % 4)) OR has_superuser_privilege())
+         ->  Seq Scan on t3 t1
+(9 rows)
+
+BEGIN;
+UPDATE t1 SET c = c || '_updt';
+ABORT;
+BEGIN;
+DELETE FROM t1;
+ABORT;
+-- Clean up objects
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rowlvsec_schema CASCADE;
+NOTICE:  drop cascades to 14 other objects
+DETAIL:  drop cascades to table customer
+drop cascades to function f_leak(text)
+drop cascades to function customer_age()
+drop cascades to function customer_name()
+drop cascades to function customer_addr()
+drop cascades to table drink
+drop cascades to row-level security of drink
+drop cascades to table purchase
+drop cascades to row-level security of purchase
+drop cascades to table t1
+drop cascades to row-level security of t1
+drop cascades to table t2
+drop cascades to row-level security of t2
+drop cascades to table t3
+DROP USER rowlvsec_user3;
+DROP USER rowlvsec_user2;
+DROP USER rowlvsec_user1;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 7f560d2..2b4932d 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -119,6 +119,7 @@ SELECT relname, relhasindex
  pg_proc                 | t
  pg_range                | t
  pg_rewrite              | t
+ pg_rowlvsec             | t
  pg_seclabel             | t
  pg_shdepend             | t
  pg_shdescription        | t
@@ -164,7 +165,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(153 rows)
+(154 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8852e0a..45a98c8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: privileges security_label collate
+test: privileges security_label collate rowlvsec
 
 test: misc
 # rules cannot run concurrently with any test that creates a view
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 0bc5df7..a35dace 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: privileges
+test: rowlvsec
 test: security_label
 test: collate
 test: misc
diff --git a/src/test/regress/sql/rowlvsec.sql b/src/test/regress/sql/rowlvsec.sql
new file mode 100644
index 0000000..f7d72f6
--- /dev/null
+++ b/src/test/regress/sql/rowlvsec.sql
@@ -0,0 +1,216 @@
+--
+-- Test Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rowlvsec_user1;
+DROP USER IF EXISTS rowlvsec_user2;
+DROP USER IF EXISTS rowlvsec_user3;
+
+DROP SCHEMA IF EXISTS rowlvsec_schema CASCADE;
+
+RESET client_min_messages;
+
+-- Initial Setup
+CREATE USER rowlvsec_user0;
+CREATE USER rowlvsec_user1;
+CREATE USER rowlvsec_user2;
+CREATE USER rowlvsec_user3;
+
+CREATE SCHEMA rowlvsec_schema;
+GRANT ALL ON SCHEMA rowlvsec_schema TO public;
+SET search_path = rowlvsec_schema,public;
+
+CREATE TABLE customer (
+       cid      name primary key,
+       cname    text,
+       cage	int,
+       caddr	text
+);
+GRANT SELECT ON TABLE customer TO public;
+INSERT INTO customer VALUES
+       ('rowlvsec_user1', 'alice', 18, 'USA'),
+       ('rowlvsec_user2', 'bob',   24, 'Japan'),
+       ('rowlvsec_user3', 'carol', 29, 'USA'),
+       ('rowlvsec_user4', 'dave',  32, 'Germany');
+
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+    COST 0.0000001 LANGUAGE plpgsql
+    AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- XXX sub-query in RLS policy should be supported in the future
+CREATE FUNCTION customer_age() RETURNS int STABLE LANGUAGE sql
+       AS 'SELECT cage FROM customer WHERE cid = session_user';
+GRANT EXECUTE ON FUNCTION customer_age() TO public;
+
+CREATE FUNCTION customer_name() RETURNS text STABLE LANGUAGE sql
+       AS 'SELECT cname FROM customer WHERE cid = session_user';
+GRANT EXECUTE ON FUNCTION customer_age() TO public;
+
+CREATE FUNCTION customer_addr() RETURNS text STABLE LANGUAGE sql
+       AS 'SELECT caddr FROM customer WHERE cid = session_user';
+GRANT EXECUTE ON FUNCTION customer_addr() TO public;
+
+-- Create Test Data
+SET SESSION AUTHORIZATION rowlvsec_user0;
+
+CREATE TABLE drink (
+       id	   int primary key,
+       name	   text,
+       price	   int,
+       alcohol	   bool,
+       madein      text
+);
+GRANT ALL ON drink TO public;
+INSERT INTO drink VALUES
+       (10, 'water',  100, false, 'Japan'),
+       (20, 'juice',  180, false, 'Germany'),
+       (30, 'coke',   120, false, 'USA'),
+       (40, 'coffee', 200, false, 'USA'),
+       (50, 'beer',   280, true,  'Germany'),
+       (60, 'wine',   340, true,  'USA'),
+       (70, 'sake',   300, true,  'Japan');
+
+CREATE TABLE purchase (
+       id          int references drink(id),
+       cname       text default customer_name(),
+       ymd         date
+);
+GRANT ALL ON purchase TO public;
+INSERT INTO purchase VALUES
+       (10, 'alice', '2012-05-03'),
+       (10, 'bob',   '2012-05-04'),
+       (10, 'alice', '2012-05-08'),
+       (20, 'alice', '2012-05-15'),
+       (20, 'bob',   '2012-05-18'),
+       (20, 'carol', '2012-05-21'),
+       (30, 'alice', '2012-05-28'),
+       (40, 'bob',   '2012-06-02'),
+       (60, 'carol', '2012-06-10');
+
+ALTER TABLE drink SET ROW LEVEL SECURITY (customer_age() > 19 OR NOT alcohol);
+ALTER TABLE purchase SET ROW LEVEL SECURITY (cname = customer_name());
+
+-- Viewpoint from Alice
+SET SESSION AUTHORIZATION rowlvsec_user1;
+SELECT * FROM drink WHERE f_leak(name);
+SELECT * FROM purchase;
+SELECT * FROM drink NATURAL JOIN purchase;
+
+-- Viewpoint from Bob
+SET SESSION AUTHORIZATION rowlvsec_user2;
+SELECT * FROM drink;
+SELECT * FROM purchase WHERE f_leak(cname);
+SELECT * FROM drink NATURAL JOIN purchase;
+
+EXPLAIN (costs off) SELECT * FROM drink WHERE f_leak(name);
+EXPLAIN (costs off) SELECT * FROM purchase WHERE f_leak(cname);
+EXPLAIN (costs off) SELECT * FROM drink NATURAL JOIN purchase;
+
+ALTER TABLE drink RESET ROW LEVEL SECURITY;  -- failed
+ALTER TABLE drink SET ROW LEVEL SECURITY (customer_name() = 'bob'); -- failed
+
+-- Change the security policy
+SET SESSION AUTHORIZATION rowlvsec_user0;
+ALTER TABLE drink SET ROW LEVEL SECURITY (madein = customer_addr());
+
+-- Viewpoint from Alice again
+SET SESSION AUTHORIZATION rowlvsec_user1;
+SELECT * FROM drink WHERE id > 30;
+
+-- Viewpoint from Bob again
+SET SESSION AUTHORIZATION rowlvsec_user2;
+SELECT * FROM drink WHERE f_leak(name);
+
+-- Writer Access and PK&FK constraints
+SET SESSION AUTHORIZATION rowlvsec_user1;
+
+SELECT * FROM drink NATURAL FULL OUTER JOIN purchase;
+
+BEGIN;
+UPDATE purchase SET ymd = ymd + 1 RETURNING *;
+EXPLAIN (costs off) UPDATE purchase SET ymd = ymd + 1;
+ABORT;
+
+BEGIN;
+DELETE FROM purchase RETURNING *;
+EXPLAIN (costs off) DELETE FROM purchase;
+ABORT;
+
+-- Failed to update PK row referenced by invisible FK
+UPDATE drink SET id = 100 WHERE id = 60;  -- fail
+DELETE FROM drink WHERE id = 40;          -- fail
+
+-- Failed to insert FK to reference invisible PK
+INSERT INTO purchase (id, ymd) VALUES (20, '2012-06-12');  -- fail
+INSERT INTO purchase (id, ymd) VALUES (30, '2012-06-12');  -- OK
+
+-- Same writter access from Bob
+SET SESSION AUTHORIZATION rowlvsec_user2;
+SELECT * FROM drink NATURAL FULL OUTER JOIN purchase;
+
+BEGIN;
+UPDATE purchase SET ymd = ymd + 1 RETURNING *;
+ABORT;
+
+BEGIN;
+DELETE FROM purchase RETURNING *;
+ABORT;
+
+-- Table inheritance and RLS policy
+SET SESSION AUTHORIZATION rowlvsec_user0;
+
+CREATE TABLE t1 (a int, b text, c text);
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+INSERT INTO t1 VALUES
+       (1,'aaa'), (2,'bbb'), (3,'ccc'), (4,'ddd'), ( 5,'eee'),
+       (6,'fff'), (7,'ggg'), (8,'hhh'), (9,'iii'), (10,'jjj');
+CREATE TABLE t2 (x int) INHERITS (t1);
+INSERT INTO t2 VALUES
+       (1,'abc'), (2,'bcd'), (3,'cde'), (4,'def'), ( 5,'efg'),
+       (6,'fgh'), (7,'ghi'), (8,'hij'), (9,'ijk'), (10,'jkl');
+CREATE TABLE t3 (y int) INHERITS (t1);
+INSERT INTO t3 VALUES (1,'xxx'),(2,'yyy'),(3,'zzz'),(4,'xyz');
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a     % 4 = customer_age() % 4);
+ALTER TABLE t2 SET ROW LEVEL SECURITY ((a+2) % 4 = customer_age() % 4);
+
+-- viewpoint from Alice
+SET SESSION AUTHORIZATION rowlvsec_user1;
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+BEGIN;
+UPDATE t1 SET c = c || '_updt';
+EXPLAIN (costs off) UPDATE t1 SET c = c || '_updt';
+ABORT;
+BEGIN;
+DELETE FROM t1;
+EXPLAIN (costs off) DELETE FROM t1;
+ABORT;
+
+-- viewpoint from Carol
+SET SESSION AUTHORIZATION rowlvsec_user3;
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+BEGIN;
+UPDATE t1 SET c = c || '_updt';
+ABORT;
+BEGIN;
+DELETE FROM t1;
+ABORT;
+
+-- Clean up objects
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rowlvsec_schema CASCADE;
+DROP USER rowlvsec_user3;
+DROP USER rowlvsec_user2;
+DROP USER rowlvsec_user1;
#2Robert Haas
robertmhaas@gmail.com
In reply to: Kohei KaiGai (#1)
Re: [v9.3] Row-Level Security

On Thu, Jun 14, 2012 at 11:43 AM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

In the previous discussion, we planned to add a syntax option to
clarify the command type to fire the RLS policy, such as FOR UPDATE.
But current my opinion is, it is not necessary. For example, we can
reference the contents of rows being updated / deleted using
RETURNING clause. So, it does not make sense to have different
RLS policy at UPDATE / DELETE from SELECT.

I agree. That doesn't make sense, and we don't need to support it.

On the other hand, I'm not 100% sure about my design to restrict
rows to be updated and deleted. Similarly, it expands the target
relation of UPDATE or DELETE statement into a sub-query with
condition. ExecModifyTable() pulls a tuple from the sub-query,
instead of regular table, so it seems to me working at least, but
I didn't try all the possible cases of course.

I don't think there's any reason why that shouldn't work. DELETE ..
USING already allows ModifyTable to be scanning a join product, so
this is not much different.

Of course, here is some limitations, to keep the patch size reasonable
level to review.
- The permission to bypass RLS policy was under discussion.
 If and when we should provide a special permission to bypass RLS
 policy, the "OR has_superuser_privilege()" shall be replaced by
 "OR has_table_privilege(tableoid, 'RLSBYPASS')".

I think you're missing the point. Everyone who has commented on this
issue is in favor of having some check that causes the RLS predicate
*not to get added in the first place*. Adding a modified predicate is
not the same thing. First, the query planner might not be smart
enough to optimize away the clause even when the predicate holds,
causing an unnecessary performance drain. Second, there's too much
danger of being able to set a booby-trap for the superuser this way.
Suppose that the RLS subsystem replaces f_malicious() by f_malicious
OR has_superuser_privilege(). Now the superuser comes along with the
nightly pg_dump run and the query optimizer executes SELECT * FROM
nuts WHERE f_malicious() OR has_superuser_privilege(). The query
optimizer notes that the cost of f_malicious() is very low and decides
to execute that before has_superuser_privilege(). Oops. I think it's
just not acceptable to handle this by clause-munging: we need to not
add the clause in the first place.

Comments on the patch itself:

1. Please do not abbreviate rowlevel to rowlv or RowLevel to RowLv or
ROWLEVEL to ROWLV. That makes it harder to read and harder to grep.
Spell it out.

2. Since the entirety of ATExecSetRowLvSecurity is conditional on
whether clause != NULL, you might as well split it into two functions,
one for each case.

3. The fact that you've had to hack preprocess_targetlist and
adjust_appendrel_attrs_mutator suggests to me that the insertion of
the generate subquery is happening at the wrong phase of the process.
We don't need those special cases for views, so it seems like we
shouldn't need them here, either.

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

#3Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Robert Haas (#2)
Re: [v9.3] Row-Level Security

2012/6/26 Robert Haas <robertmhaas@gmail.com>:

Of course, here is some limitations, to keep the patch size reasonable
level to review.
- The permission to bypass RLS policy was under discussion.
 If and when we should provide a special permission to bypass RLS
 policy, the "OR has_superuser_privilege()" shall be replaced by
 "OR has_table_privilege(tableoid, 'RLSBYPASS')".

I think you're missing the point.  Everyone who has commented on this
issue is in favor of having some check that causes the RLS predicate
*not to get added in the first place*.  Adding a modified predicate is
not the same thing.  First, the query planner might not be smart
enough to optimize away the clause even when the predicate holds,
causing an unnecessary performance drain.  Second, there's too much
danger of being able to set a booby-trap for the superuser this way.
Suppose that the RLS subsystem replaces f_malicious() by f_malicious
OR has_superuser_privilege().  Now the superuser comes along with the
nightly pg_dump run and the query optimizer executes SELECT * FROM
nuts WHERE f_malicious() OR has_superuser_privilege().  The query
optimizer notes that the cost of f_malicious() is very low and decides
to execute that before has_superuser_privilege().  Oops.  I think it's
just not acceptable to handle this by clause-munging: we need to not
add the clause in the first place.

Here is a simple idea to avoid the second problematic scenario; that
assign 0 as cost of has_superuser_privilege(). I allows to keep this
function more lightweight than any possible malicious function, since
CreateFunction enforces positive value.

But the first point is still remaining.

As you pointed out before, it might be a solution to have case-handling
for superusers and others in case of simple query protocol; that uses
same snapshot for planner and executor stage.

How should we handle the issue?

During the previous discussion, Tom mentioned about an idea that
saves prepared statement hashed with user-id to switch the query
plan depending on user's privilege.
Even though I hesitated to buy this idea at that time, it might be
worth to investigate this idea to satisfy both security and performance;
that will generate multiple query plans to be chosen at executor
stage later.

How about your opinion?

Comments on the patch itself:

1. Please do not abbreviate rowlevel to rowlv or RowLevel to RowLv or
ROWLEVEL to ROWLV.  That makes it harder to read and harder to grep.
Spell it out.

OK,

2. Since the entirety of ATExecSetRowLvSecurity is conditional on
whether clause != NULL, you might as well split it into two functions,
one for each case.

OK,

3. The fact that you've had to hack preprocess_targetlist and
adjust_appendrel_attrs_mutator suggests to me that the insertion of
the generate subquery is happening at the wrong phase of the process.
We don't need those special cases for views, so it seems like we
shouldn't need them here, either.

Main reason why I had to patch them is special case handling for
references to system columns; that is unavailable to have for sub-
queries.
But, I'm not 100% sure around these implementation. So, it needs
more investigations.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#4Tom Lane
tgl@sss.pgh.pa.us
In reply to: Kohei KaiGai (#3)
Re: [v9.3] Row-Level Security

Kohei KaiGai <kaigai@kaigai.gr.jp> writes:

2012/6/26 Robert Haas <robertmhaas@gmail.com>:

I think you're missing the point. �Everyone who has commented on this
issue is in favor of having some check that causes the RLS predicate
*not to get added in the first place*.

Here is a simple idea to avoid the second problematic scenario; that
assign 0 as cost of has_superuser_privilege().

I am not sure which part of "this isn't safe" isn't getting through to
you. Aside from the scenarios Robert mentioned, consider the
possibility that f_malicious() is marked immutable, so that the planner
is likely to call it (to replace the call with its value) before it will
ever think about whether has_superuser_privilege should be called first.

Please just do what everybody is asking for, and create a bypass that
does not require fragile, easily-broken-by-future-changes assumptions
about what the planner will do with a WHERE clause.

regards, tom lane

#5Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Tom Lane (#4)
Re: [v9.3] Row-Level Security

2012/6/26 Tom Lane <tgl@sss.pgh.pa.us>:

Kohei KaiGai <kaigai@kaigai.gr.jp> writes:

2012/6/26 Robert Haas <robertmhaas@gmail.com>:

I think you're missing the point.  Everyone who has commented on this
issue is in favor of having some check that causes the RLS predicate
*not to get added in the first place*.

Here is a simple idea to avoid the second problematic scenario; that
assign 0 as cost of has_superuser_privilege().

I am not sure which part of "this isn't safe" isn't getting through to
you.  Aside from the scenarios Robert mentioned, consider the
possibility that f_malicious() is marked immutable, so that the planner
is likely to call it (to replace the call with its value) before it will
ever think about whether has_superuser_privilege should be called first.

Please just do what everybody is asking for, and create a bypass that
does not require fragile, easily-broken-by-future-changes assumptions
about what the planner will do with a WHERE clause.

The problem is the way to implement it.
If we would have permission checks on planner stage, it cannot handle
a case when user-id would be switched prior to executor stage, thus
it needs something remedy to handle the scenario correctly.
Instead of a unique plan per query, it might be a solution to generate
multiple plans depending on user-id, and choose a proper one in
executor stage.

Which type of implementation is what everybody is asking for?

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#6Florian Pflug
fgp@phlo.org
In reply to: Kohei KaiGai (#5)
Re: [v9.3] Row-Level Security

On Jun27, 2012, at 07:18 , Kohei KaiGai wrote:

The problem is the way to implement it.
If we would have permission checks on planner stage, it cannot handle
a case when user-id would be switched prior to executor stage, thus
it needs something remedy to handle the scenario correctly.
Instead of a unique plan per query, it might be a solution to generate
multiple plans depending on user-id, and choose a proper one in
executor stage.

Which type of implementation is what everybody is asking for?

I think you need to

a) Determine the user-id at planning time, and insert the matching
RLS clause

b1) Either re-plan the query if the user-id changes between planning
and execution time, which means making the user-id a part of the
plan-cache key.

b2) Or decree that for RLS purposes, it's the user-id at planning time,
not execution time, that counts.

best regards,
Florian Pflug

#7Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Florian Pflug (#6)
Re: [v9.3] Row-Level Security

2012/6/27 Florian Pflug <fgp@phlo.org>:

On Jun27, 2012, at 07:18 , Kohei KaiGai wrote:

The problem is the way to implement it.
If we would have permission checks on planner stage, it cannot handle
a case when user-id would be switched prior to executor stage, thus
it needs something remedy to handle the scenario correctly.
Instead of a unique plan per query, it might be a solution to generate
multiple plans depending on user-id, and choose a proper one in
executor stage.

Which type of implementation is what everybody is asking for?

I think you need to

 a) Determine the user-id at planning time, and insert the matching
   RLS clause

b1) Either re-plan the query if the user-id changes between planning
   and execution time, which means making the user-id a part of the
   plan-cache key.

b2) Or decree that for RLS purposes, it's the user-id at planning time,
   not execution time, that counts.

My preference is b1, because b2 approach takes user visible changes
in concepts of permission checks.

Probably, plan-cache should be also invalidated when user's property
was modified or grant/revoke is issued, in addition to the table itself.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#8Robert Haas
robertmhaas@gmail.com
In reply to: Florian Pflug (#6)
Re: [v9.3] Row-Level Security

On Wed, Jun 27, 2012 at 7:21 AM, Florian Pflug <fgp@phlo.org> wrote:

On Jun27, 2012, at 07:18 , Kohei KaiGai wrote:

The problem is the way to implement it.
If we would have permission checks on planner stage, it cannot handle
a case when user-id would be switched prior to executor stage, thus
it needs something remedy to handle the scenario correctly.
Instead of a unique plan per query, it might be a solution to generate
multiple plans depending on user-id, and choose a proper one in
executor stage.

Which type of implementation is what everybody is asking for?

I think you need to

 a) Determine the user-id at planning time, and insert the matching
   RLS clause

b1) Either re-plan the query if the user-id changes between planning
   and execution time, which means making the user-id a part of the
   plan-cache key.

b2) Or decree that for RLS purposes, it's the user-id at planning time,
   not execution time, that counts.

Or b3, flag plans that depend on the user ID inside the plan-cache,
and just flush all of those (but only those) when the user ID changes.
In the common case where RLS is not used, that might ease the sting.

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

#9Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Robert Haas (#8)
Re: [v9.3] Row-Level Security

2012/6/27 Robert Haas <robertmhaas@gmail.com>:

On Wed, Jun 27, 2012 at 7:21 AM, Florian Pflug <fgp@phlo.org> wrote:

On Jun27, 2012, at 07:18 , Kohei KaiGai wrote:

The problem is the way to implement it.
If we would have permission checks on planner stage, it cannot handle
a case when user-id would be switched prior to executor stage, thus
it needs something remedy to handle the scenario correctly.
Instead of a unique plan per query, it might be a solution to generate
multiple plans depending on user-id, and choose a proper one in
executor stage.

Which type of implementation is what everybody is asking for?

I think you need to

 a) Determine the user-id at planning time, and insert the matching
   RLS clause

b1) Either re-plan the query if the user-id changes between planning
   and execution time, which means making the user-id a part of the
   plan-cache key.

b2) Or decree that for RLS purposes, it's the user-id at planning time,
   not execution time, that counts.

Or b3, flag plans that depend on the user ID inside the plan-cache,
and just flush all of those (but only those) when the user ID changes.
 In the common case where RLS is not used, that might ease the sting.

Probably, PlannedStmt->invalItems allows to handle invalidation of
plan-cache without big code changes. I'll try to put a flag of user-id
to track the query plan with RLS assumed, or InvalidOid if no RLS
was applied in this plan.
I'll investigate the implementation for more details.

Do we have any other scenario that run a query plan under different
user privilege rather than planner stage?

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#10Florian Pflug
fgp@phlo.org
In reply to: Kohei KaiGai (#9)
Re: [v9.3] Row-Level Security

On Jun27, 2012, at 15:07 , Kohei KaiGai wrote:

Probably, PlannedStmt->invalItems allows to handle invalidation of
plan-cache without big code changes. I'll try to put a flag of user-id
to track the query plan with RLS assumed, or InvalidOid if no RLS
was applied in this plan.
I'll investigate the implementation for more details.

Do we have any other scenario that run a query plan under different
user privilege rather than planner stage?

Hm, what happens if a SECURITY DEFINER functions returns a refcursor?

Actually, I wonder how we handle that today. If the executor is
responsible for permission checks, that wouldn't we apply the calling
function's privilege level in that case, at least of the cursor isn't
fetched from in the SECURITY DEFINER function? If I find some time,
I'll check...

best regards,
Florian Pflug

#11Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Florian Pflug (#10)
Re: [v9.3] Row-Level Security

2012/6/27 Florian Pflug <fgp@phlo.org>:

On Jun27, 2012, at 15:07 , Kohei KaiGai wrote:

Probably, PlannedStmt->invalItems allows to handle invalidation of
plan-cache without big code changes. I'll try to put a flag of user-id
to track the query plan with RLS assumed, or InvalidOid if no RLS
was applied in this plan.
I'll investigate the implementation for more details.

Do we have any other scenario that run a query plan under different
user privilege rather than planner stage?

Hm, what happens if a SECURITY DEFINER functions returns a refcursor?

Actually, I wonder how we handle that today. If the executor is
responsible for permission checks, that wouldn't we apply the calling
function's privilege level in that case, at least of the cursor isn't
fetched from in the SECURITY DEFINER function? If I find some time,
I'll check...

My impression is, here is no matter even if SECURITY DEFINER function
returns refcursor.

A SECURITY DEFINER function (or Trusted Procedure on sepgsql, or
Set-UID program on Linux) provides unprivileged users a particular
"limited way" to access protected data. It means owner of the security
definer function admits it is reasonable to show the protected data
as long as unprivileged users access them via the function.

It is same reason why we admit view's access for users who have
privileges on views but unprivileged to underlying tables.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#12Tom Lane
tgl@sss.pgh.pa.us
In reply to: Kohei KaiGai (#11)
Re: [v9.3] Row-Level Security

Kohei KaiGai <kaigai@kaigai.gr.jp> writes:

2012/6/27 Florian Pflug <fgp@phlo.org>:

Hm, what happens if a SECURITY DEFINER functions returns a refcursor?

My impression is, here is no matter even if SECURITY DEFINER function
returns refcursor.

I think Florian has a point: it *should* work, but *will* it?

I believe it works today, because the executor only applies permissions
checks during query startup. So those checks are executed while still
within the SECURITY DEFINER context, and should behave as expected.
Subsequently, the cursor portal is returned to caller and caller can
execute it to completion, no problem.

However, with RLS security-related checks will happen throughout the
execution of the portal. They might do the wrong thing once the
SECURITY DEFINER function has been exited.

We might need to consider that a portal has a local value of
"current_user", which is kind of a pain, but probably doable.

regards, tom lane

#13Florian Pflug
fgp@phlo.org
In reply to: Tom Lane (#12)
Re: [v9.3] Row-Level Security

On Jun28, 2012, at 17:29 , Tom Lane wrote:

Kohei KaiGai <kaigai@kaigai.gr.jp> writes:

2012/6/27 Florian Pflug <fgp@phlo.org>:

Hm, what happens if a SECURITY DEFINER functions returns a refcursor?

My impression is, here is no matter even if SECURITY DEFINER function
returns refcursor.

I think Florian has a point: it *should* work, but *will* it?

I believe it works today, because the executor only applies permissions
checks during query startup. So those checks are executed while still
within the SECURITY DEFINER context, and should behave as expected.
Subsequently, the cursor portal is returned to caller and caller can
execute it to completion, no problem.

Don't we (sometimes?) defer query startup to the first time FETCH is
called?

best regards,
Florian Pflug

#14Tom Lane
tgl@sss.pgh.pa.us
In reply to: Florian Pflug (#13)
Re: [v9.3] Row-Level Security

Florian Pflug <fgp@phlo.org> writes:

On Jun28, 2012, at 17:29 , Tom Lane wrote:

I believe it works today, because the executor only applies permissions
checks during query startup. So those checks are executed while still
within the SECURITY DEFINER context, and should behave as expected.
Subsequently, the cursor portal is returned to caller and caller can
execute it to completion, no problem.

Don't we (sometimes?) defer query startup to the first time FETCH is
called?

There are things inside individual plan node functions that may only
happen when the first row is demanded, but permissions checks are done
in ExecutorStart().

regards, tom lane

#15Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Tom Lane (#12)
Re: [v9.3] Row-Level Security

2012/6/28 Tom Lane <tgl@sss.pgh.pa.us>:

Kohei KaiGai <kaigai@kaigai.gr.jp> writes:

2012/6/27 Florian Pflug <fgp@phlo.org>:

Hm, what happens if a SECURITY DEFINER functions returns a refcursor?

My impression is, here is no matter even if SECURITY DEFINER function
returns refcursor.

I think Florian has a point: it *should* work, but *will* it?

I believe it works today, because the executor only applies permissions
checks during query startup. So those checks are executed while still
within the SECURITY DEFINER context, and should behave as expected.
Subsequently, the cursor portal is returned to caller and caller can
execute it to completion, no problem.

However, with RLS security-related checks will happen throughout the
execution of the portal. They might do the wrong thing once the
SECURITY DEFINER function has been exited.

I tried the scenario that Florian pointed out.

postgres=# CREATE OR REPLACE FUNCTION f_test(refcursor) RETURNS refcursor
postgres-# SECURITY DEFINER LANGUAGE plpgsql
postgres-# AS 'BEGIN OPEN $1 FOR SELECT current_user, * FROM t1;
RETURN $1; END';
CREATE FUNCTION
postgres=# BEGIN;
BEGIN
postgres=# SELECT f_test('abc');
f_test
--------
abc
(1 row)

postgres=# FETCH abc;
current_user | a | b
--------------+---+-----
kaigai | 1 | aaa
(1 row)

postgres=# SET SESSION AUTHORIZATION alice;
SET
postgres=> FETCH abc;
current_user | a | b
--------------+---+-----
alice | 2 | bbb
(1 row)

postgres=> SET SESSION AUTHORIZATION bob;
SET
postgres=> FETCH abc;
current_user | a | b
--------------+---+-----
bob | 3 | ccc
(1 row)

Indeed, the output of "current_user" depends on the setting of session
authorization, even though it was declared within security definer
functions (thus, its security checks are applied on the privileges of
function owner).

We might need to consider that a portal has a local value of
"current_user", which is kind of a pain, but probably doable.

It seems to me, it is an individual matter to be fixed independent
from RLS feature. How about your opinion?

If we go ahead, an idea to tackle this matter is switch user-id
into saved one in the Portal at the beginning of PortanRun or
PortalRunFetch.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#16Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Kohei KaiGai (#15)
1 attachment(s)
Re: [v9.3] Row-Level Security

The attached patch is a revised version of row-level security feature.

I added a feature to invalidate plan cache if user-id was switched
between planner and optimizer. It enabled to generate more optimized
plan than the previous approach; that adds hardwired "OR superuser()".

Example)
postgres=# PREPARE p1(int) AS SELECT * FROM t1 WHERE x > $1 AND f_leak(y);
PREPARE
postgres=# EXPLAIN (costs off) EXECUTE p1(2);
QUERY PLAN
-----------------------------------
Seq Scan on t1
Filter: (f_leak(y) AND (x > 2))
(2 rows)

postgres=# SET SESSION AUTHORIZATION alice;
SET
postgres=> EXPLAIN (costs off) EXECUTE p1(2);
QUERY PLAN
---------------------------------------------
Subquery Scan on t1
Filter: f_leak(t1.y)
-> Seq Scan on t1
Filter: ((x > 2) AND ((x % 2) = 0))
(4 rows)

On the other hand, I removed support for UPDATE / DELETE commands
in this revision, because I'm still uncertain on the implementation that I
adopted in the previous patch. I believe it helps to keep patch size being
minimum reasonable.
Due to same reason, RLS is not supported on COPY TO command.

According to the Robert's comment, I revised the place to inject
applyRowLevelSecurity(). The reason why it needed to patch on
adjust_appendrel_attrs_mutator() was, we handled expansion from
regular relation to sub-query after expand_inherited_tables().
In this revision, it was moved to the head of sub-query planner.

Even though I added a syscache entry for pg_rowlevelsec catalog,
it was revised to read the catalog on construction of relcache, like
trigger descriptor, because it enables to reduce cost to parse an
expression tree in text format and memory consumption of hash
slot.

This revision adds support on pg_dump, and also adds support
to include SubLinks in the row-level security policy.

Thanks,

2012/7/1 Kohei KaiGai <kaigai@kaigai.gr.jp>:

2012/6/28 Tom Lane <tgl@sss.pgh.pa.us>:

Kohei KaiGai <kaigai@kaigai.gr.jp> writes:

2012/6/27 Florian Pflug <fgp@phlo.org>:

Hm, what happens if a SECURITY DEFINER functions returns a refcursor?

My impression is, here is no matter even if SECURITY DEFINER function
returns refcursor.

I think Florian has a point: it *should* work, but *will* it?

I believe it works today, because the executor only applies permissions
checks during query startup. So those checks are executed while still
within the SECURITY DEFINER context, and should behave as expected.
Subsequently, the cursor portal is returned to caller and caller can
execute it to completion, no problem.

However, with RLS security-related checks will happen throughout the
execution of the portal. They might do the wrong thing once the
SECURITY DEFINER function has been exited.

I tried the scenario that Florian pointed out.

postgres=# CREATE OR REPLACE FUNCTION f_test(refcursor) RETURNS refcursor
postgres-# SECURITY DEFINER LANGUAGE plpgsql
postgres-# AS 'BEGIN OPEN $1 FOR SELECT current_user, * FROM t1;
RETURN $1; END';
CREATE FUNCTION
postgres=# BEGIN;
BEGIN
postgres=# SELECT f_test('abc');
f_test
--------
abc
(1 row)

postgres=# FETCH abc;
current_user | a | b
--------------+---+-----
kaigai | 1 | aaa
(1 row)

postgres=# SET SESSION AUTHORIZATION alice;
SET
postgres=> FETCH abc;
current_user | a | b
--------------+---+-----
alice | 2 | bbb
(1 row)

postgres=> SET SESSION AUTHORIZATION bob;
SET
postgres=> FETCH abc;
current_user | a | b
--------------+---+-----
bob | 3 | ccc
(1 row)

Indeed, the output of "current_user" depends on the setting of session
authorization, even though it was declared within security definer
functions (thus, its security checks are applied on the privileges of
function owner).

We might need to consider that a portal has a local value of
"current_user", which is kind of a pain, but probably doable.

It seems to me, it is an individual matter to be fixed independent
from RLS feature. How about your opinion?

If we go ahead, an idea to tackle this matter is switch user-id
into saved one in the Portal at the beginning of PortanRun or
PortalRunFetch.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

Attachments:

pgsql-v9.3-row-level-security.ro.v2.patchapplication/octet-stream; name=pgsql-v9.3-row-level-security.ro.v2.patchDownload
 doc/src/sgml/catalogs.sgml                 |   59 +++
 doc/src/sgml/ref/alter_table.sgml          |   38 ++
 doc/src/sgml/user-manag.sgml               |  123 +++++
 src/backend/access/transam/xact.c          |   12 +
 src/backend/catalog/Makefile               |    4 +-
 src/backend/catalog/dependency.c           |   21 +
 src/backend/catalog/heap.c                 |    1 +
 src/backend/catalog/pg_rowlevelsec.c       |  250 +++++++++
 src/backend/commands/tablecmds.c           |   64 +++
 src/backend/optimizer/plan/planner.c       |   17 +
 src/backend/optimizer/util/Makefile        |    2 +-
 src/backend/optimizer/util/rowlevelsec.c   |  677 +++++++++++++++++++++++++
 src/backend/parser/gram.y                  |   16 +
 src/backend/utils/adt/ri_triggers.c        |   14 +
 src/backend/utils/cache/plancache.c        |   32 ++
 src/backend/utils/cache/relcache.c         |   17 +-
 src/bin/pg_dump/pg_dump.c                  |   76 ++-
 src/bin/pg_dump/pg_dump.h                  |    1 +
 src/include/catalog/dependency.h           |    1 +
 src/include/catalog/indexing.h             |    3 +
 src/include/catalog/pg_class.h             |   20 +-
 src/include/catalog/pg_rowlevelsec.h       |   60 +++
 src/include/nodes/parsenodes.h             |    5 +-
 src/include/nodes/plannodes.h              |    2 +
 src/include/nodes/relation.h               |    2 +
 src/include/optimizer/rowlevelsec.h        |   31 ++
 src/include/utils/plancache.h              |    2 +
 src/include/utils/rel.h                    |    2 +
 src/test/regress/expected/rowlevelsec.out  |  753 ++++++++++++++++++++++++++++
 src/test/regress/expected/sanity_check.out |    3 +-
 src/test/regress/parallel_schedule         |    2 +-
 src/test/regress/serial_schedule           |    1 +
 src/test/regress/sql/rowlevelsec.sql       |  237 +++++++++
 33 files changed, 2522 insertions(+), 26 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fbd6d0d..426274c 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -229,6 +229,11 @@
      </row>
 
      <row>
+      <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+      <entry>row-level security policy of relation</entry>
+     </row>
+
+     <row>
       <entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
       <entry>security labels on database objects</entry>
      </row>
@@ -1802,6 +1807,16 @@
      </row>
 
      <row>
+      <entry><structfield>relhasrowlevelsec</structfield></entry>
+      <entry><type>bool</type></entry>
+      <entry></entry>
+      <entry>
+       True if table has row-level security policy; see
+       <link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link> catalog
+      </entry>
+     </row>
+
+     <row>
       <entry><structfield>relhassubclass</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
@@ -4819,6 +4834,50 @@
 
  </sect1>
 
+ <sect1 id="catalog-pg-rowlevelsec">
+  <title><structname>pg_rowlevelsec</structname></title>
+
+  <indexterm zone="catalog-pg-rowlevelsec">
+   <primary>pg_rowlevelsec</primary>
+  </indexterm>
+  <para>
+   The catalog <structname>pg_rowlevelsec</structname> expression tree of
+   row-level security policy to be performed on a particular relation.
+  </para>
+  <table>
+   <title><structname>pg_rowlevelsec</structname> Columns</title>
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>rlsrelid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+      <entry>The table this row-level security is for</entry>
+     </row>
+     <row>
+      <entry><structfield>rlsqual</structfield></entry>
+      <entry><type>text</type></entry>
+      <entry></entry>
+      <entry>An expression tree to be performed as rowl-level security policy</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+  <note>
+   <para>
+    <literal>pg_class.relhasrowlevelsec</literal>
+    must be true if a table has row-level security policy in this catalog.
+   </para>
+  </note>
+ </sect1>
 
  <sect1 id="catalog-pg-seclabel">
   <title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 3f61d7d..6cae6c3 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     NOT OF
     OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
     SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+    SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)
+    RESET ROW LEVEL SECURITY
 
 <phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
 
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
    </varlistentry>
 
    <varlistentry>
+    <term><literal>SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+    <listitem>
+     <para>
+      This form set row-level security policy of the table.
+      Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+      as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+      clause, although mechanism guarantees to evaluate this condition earlier
+      than any other user given condition.
+      See also <xref linkend="ROW-LEVEL-SECURITY">.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET ROW LEVEL SECURITY</literal></term>
+    <listitem>
+     <para>
+      This form reset row-level security policy of the table, if exists.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
@@ -806,6 +831,19 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="PARAMETER">condition</replaceable></term>
+      <listitem>
+       <para>
+        An expression that returns a value of type boolean. Expect for a case
+        when queries are executed with superuser privilege, only rows for which
+        this expression returns true will be fetched, updated or deleted.
+        This expression can reference columns of the relation being configured,
+        however, unavailable to include sub-query right now.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c283e07 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,127 @@ DROP ROLE <replaceable>name</replaceable>;
   </para>
  </sect1>
 
+ <sect1 id="row-level-security">
+  <title>Row-level Security</title>
+  <para>
+   <productname>PostgreSQL</productname> v9.3 or later provides
+   row-level security feature, like several commercial database
+   management system. It allows table owner to assign a particular
+   condition that performs as a security policy of the table; only
+   rows that satisfies the condition should be visible, except for
+   a case when superuser runs queries.
+  </para>
+  <para>
+   Row-level security policy can be set using
+   <literal>SET ROW LEVEL SECURITY</literal> command of
+   <xref linkend="SQL-ALTERTABLE"> statement, as an expression
+    form that returns a value of type boolean. This expression can
+    contain references to columns of the relation, so it enables
+    to construct arbitrary rule to make access control decision
+    based on contents of each rows.
+  </para>
+  <para>
+   For example, the following <literal>customer</literal> table
+   has <literal>uname</literal> field to store user name, and
+   it assume we don't want to expose any properties of other
+   customers.
+   The following command set <literal>current_user = uname</literal>
+   as row-level security policy on the <literal>customer</literal>
+   table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+</screen>
+   <xref linkend="SQL-EXPLAIN"> command shows how row-level
+   security policy works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+                 QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+   Filter: f_leak(customer.upasswd)
+   ->  Seq Scan on customer
+         Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+   This query execution plan means the preconfigured row-level
+   security policy is implicitly added, and scan plan on the
+   target relation being wrapped up with a sub-query.
+   It ensures user given qualifiers, including functions with
+   side effects, are never executed earlier than the row-level
+   security policy regardless of its cost, except for the cases
+   when these were fully leakproof.
+   This design helps to tackle the scenario described in
+   <xref linkend="RULES-PRIVILEGES">; that introduces the order
+   to evaluate qualifiers is significant to keep confidentiality
+   of invisible rows.
+  </para>
+
+  <para>
+   On the other hand, this design allows superusers to bypass
+   checks with row-level security.
+   It ensures <application>pg_dump</application> can obtain
+   a complete set of database backup, and avoid to execute
+   Trojan horse trap, being injected as a row-level security
+   policy of user-defined table, with privileges of superuser.
+  </para>
+
+  <para>
+   In case of queries on inherited tables, row-level security
+   policy of the parent relation is not applied to child
+   relations. Scope of the row-level security policy is limited
+   to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+                QUERY PLAN
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.y)
+               ->  Seq Scan on t1
+                     Filter: ((x % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: f_leak(y)
+         ->  Subquery Scan on t3
+               Filter: f_leak(t3.y)
+               ->  Seq Scan on t3
+                     Filter: ((x % 2) = 1)
+(12 rows)
+</screen>
+    In the above example, <literal>t1</literal> has inherited
+    child table <literal>t2</literal> and <literal>t3</literal>,
+    and row-level security policy is set on only <literal>t1</literal>,
+    and <literal>t3</literal>, not <literal>t2</literal>.
+
+    The row-level security policy of <literal>t1</literal>,
+    <literal>x</literal> must be even-number, is appended only
+    <literal>t1</literal>, neither <literal>t2</literal> nor
+    <literal>t3</literal>. On the contrary, <literal>t3</literal>
+    has different row-level security policy; <literal>x</literal>
+    must be odd-number.
+  </para>
+
+  <para>
+   Right now, row-level security feature has several limitation,
+   although these shall be improved in the future version.
+
+   Row-level security policy is not applied to
+   <command>UPDATE</command> or <command>DELETE</command>
+   commands in this revision, even though it should be
+   supported soon.
+
+   Row-level security policy is not applied to rows to be inserted
+   and newer revision of updated rows, thus, it requires to
+   define before-row-insert or before-row-update trigger to check
+   whether the row's contents satisfies the policy individually.
+
+   Although it is not a specific matter in row-level security,
+   <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH">
+   allows to switch current user identifier during execution
+   of the query. Thus, it may cause unpredicated behavior
+   if and when <literal>current_user</literal> is used as
+   a part of row-level security policy.
+  </para>
+ </sect1>
 </chapter>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index 49def6a..1b88a96 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
 #include "executor/spi.h"
 #include "libpq/be-fsstubs.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "pgstat.h"
 #include "replication/walsender.h"
 #include "replication/syncrep.h"
@@ -142,6 +143,7 @@ typedef struct TransactionStateData
 	int			maxChildXids;	/* allocated size of childXids[] */
 	Oid			prevUser;		/* previous CurrentUserId setting */
 	int			prevSecContext; /* previous SecurityRestrictionContext */
+	RowLevelSecMode	prevRowLevelSecMode;	/* previous RLS-mode setting */
 	bool		prevXactReadOnly;		/* entry-time xact r/o state */
 	bool		startedInRecovery;		/* did we start in recovery? */
 	struct TransactionStateData *parent;		/* back link to parent */
@@ -171,6 +173,7 @@ static TransactionStateData TopTransactionStateData = {
 	0,							/* allocated size of childXids[] */
 	InvalidOid,					/* previous CurrentUserId setting */
 	0,							/* previous SecurityRestrictionContext */
+	RowLevelSecModeEnabled,		/* previous RowLevelSecMode setting */
 	false,						/* entry-time xact r/o state */
 	false,						/* startedInRecovery */
 	NULL						/* link to parent state block */
@@ -1762,6 +1765,8 @@ StartTransaction(void)
 	/* SecurityRestrictionContext should never be set outside a transaction */
 	Assert(s->prevSecContext == 0);
 
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
+
 	/*
 	 * initialize other subsystems for new transaction
 	 */
@@ -2293,6 +2298,9 @@ AbortTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Also, mode setting of row-level security should be restored. */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * do abort processing
 	 */
@@ -4186,6 +4194,9 @@ AbortSubTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Reset row-level security mode */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * We can skip all this stuff if the subxact failed before creating a
 	 * ResourceOwner...
@@ -4325,6 +4336,7 @@ PushTransaction(void)
 	s->state = TRANS_DEFAULT;
 	s->blockState = TBLOCK_SUBBEGIN;
 	GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext);
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
 	s->prevXactReadOnly = XactReadOnly;
 
 	CurrentTransactionState = s;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 62fc9b0..de840b2 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
        objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \
        pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
        pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
-       pg_type.o storage.o toasting.o
+       pg_rowlevelsec.o pg_type.o storage.o toasting.o
 
 BKIFILES = postgres.bki postgres.description postgres.shdescription
 
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
-	pg_foreign_table.h \
+	pg_foreign_table.h pg_rowlevelsec.h \
 	pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
 	toasting.h indexing.h \
     )
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 98ce598..d18584b 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -45,6 +45,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
@@ -1221,6 +1222,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveExtensionById(object->objectId);
 			break;
 
+		case OCLASS_ROWLEVELSEC:
+			RemoveRowLevelSecurityById(object->objectId);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized object class: %u",
 				 object->classId);
@@ -2269,6 +2274,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case ExtensionRelationId:
 			return OCLASS_EXTENSION;
+
+		case RowLevelSecurityRelationId:
+			return OCLASS_ROWLEVELSEC;
 	}
 
 	/* shouldn't get here */
@@ -2903,6 +2911,19 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_ROWLEVELSEC:
+			{
+				char	   *relname;
+
+				relname = get_rel_name(object->objectId);
+				if (!relname)
+					elog(ERROR, "cache lookup failed for relation %u",
+						 object->objectId);
+				appendStringInfo(&buffer,
+								 _("row-level security of %s"), relname);
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index c91df90..0d806de 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -775,6 +775,7 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
 	values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
 	values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+	values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec);
 	values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	if (relacl != (Datum) 0)
diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c
new file mode 100644
index 0000000..bd89c48
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,250 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlevelsec.c
+ *    routines to support manipulation of the pg_rowlevelsec relation
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+void
+RelationBuildRowLevelSecurity(Relation relation)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+
+	tuple = systable_getnext(sscan);
+	if (HeapTupleIsValid(tuple))
+	{
+		RowLevelSecDesc	*rlsdesc;
+		MemoryContext	rlscxt;
+		MemoryContext	oldcxt;
+		Datum	datum;
+		bool	isnull;
+		char   *temp;
+
+		/*
+		 * Make the private memory context to store RowLevelSecDesc that
+		 * includes expression tree also.
+		 */
+		rlscxt = AllocSetContextCreate(CacheMemoryContext,
+									   RelationGetRelationName(relation),
+									   ALLOCSET_SMALL_MINSIZE,
+									   ALLOCSET_SMALL_INITSIZE,
+									   ALLOCSET_SMALL_MAXSIZE);
+		PG_TRY();
+		{
+			datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual,
+								 RelationGetDescr(rlsrel), &isnull);
+			Assert(!isnull);
+			temp = TextDatumGetCString(datum);
+
+			oldcxt = MemoryContextSwitchTo(rlscxt);
+
+			rlsdesc = palloc0(sizeof(RowLevelSecDesc));
+			rlsdesc->rlscxt = rlscxt;
+			rlsdesc->rlsqual = (Expr *) stringToNode(temp);
+			Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID);
+
+			rlsdesc->rlshassublinks
+				= contain_subplans((Node *)rlsdesc->rlsqual);
+
+			MemoryContextSwitchTo(oldcxt);
+
+			pfree(temp);
+		}
+		PG_CATCH();
+		{
+			MemoryContextDelete(rlscxt);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		relation->rlsdesc = rlsdesc;
+	}
+	else
+	{
+		relation->rlsdesc = NULL;
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, AccessShareLock);
+}
+
+void
+SetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid				relationId = RelationGetRelid(relation);
+	ParseState	   *pstate;
+	RangeTblEntry  *rte;
+	Node		   *qual;
+	Relation		rlsrel;
+	ScanKeyData		skey;
+	SysScanDesc		sscan;
+	HeapTuple		oldtup;
+	HeapTuple		newtup;
+	Datum			values[Natts_pg_rowlevelsec];
+	bool			isnull[Natts_pg_rowlevelsec];
+	bool			replaces[Natts_pg_rowlevelsec];
+	ObjectAddress	target;
+	ObjectAddress	myself;
+
+	/* Parse the supplied clause */
+	pstate = make_parsestate(NULL);
+
+	rte = addRangeTableEntryForRelation(pstate, relation,
+										NULL, false, false);
+	addRTEtoQuery(pstate, rte, false, true, true);
+
+	qual = transformWhereClause(pstate, copyObject(clause),
+								"ROW LEVEL SECURITY");
+	/* No aggregate function support */
+	if (pstate->p_hasAggs)
+		ereport(ERROR,
+				(errcode(ERRCODE_GROUPING_ERROR),
+				 errmsg("cannot use aggregate in row-level security")));
+	/* No window function support */
+	if (pstate->p_hasWindowFuncs)
+		ereport(ERROR,
+				(errcode(ERRCODE_WINDOWING_ERROR),
+				 errmsg("cannot use window function in row-level security")));
+
+	/* zero-clear */
+	memset(values,   0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(isnull,   0, sizeof(isnull));
+
+	/* Update or Indert an entry to pg_rowlevelsec catalog  */
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	oldtup = systable_getnext(sscan);
+	if (HeapTupleIsValid(oldtup))
+	{
+		replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true;
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+
+		newtup = heap_modify_tuple(oldtup,
+								   RelationGetDescr(rlsrel),
+								   values, isnull, replaces);
+		simple_heap_update(rlsrel, &newtup->t_self, newtup);
+
+		deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+								   relationId, false);
+	}
+	else
+	{
+		values[Anum_pg_rowlevelsec_rlsrelid - 1]
+			= ObjectIdGetDatum(relationId);
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+		newtup = heap_form_tuple(RelationGetDescr(rlsrel),
+								 values, isnull);
+		simple_heap_insert(rlsrel, newtup);
+	}
+	CatalogUpdateIndexes(rlsrel, newtup);
+
+	heap_freetuple(newtup);
+
+	/* records dependencies of RLS-policy and relation/columns */
+	target.classId = RelationRelationId;
+	target.objectId = relationId;
+	target.objectSubId = 0;
+
+	myself.classId = RowLevelSecurityRelationId;
+	myself.objectId = relationId;
+	myself.objectSubId = 0;
+
+	recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+	recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+						   DEPENDENCY_NORMAL);
+	free_parsestate(pstate);
+
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+void
+ResetRowLevelSecurity(Relation relation)
+{
+	if (relation->rlsdesc)
+	{
+		ObjectAddress	address;
+
+		address.classId = RowLevelSecurityRelationId;
+		address.objectId = RelationGetRelid(relation);
+		address.objectSubId = 0;
+
+		performDeletion(&address, DROP_RESTRICT, 0);
+	}
+	else
+	{
+		/* Nothing to do here */
+		elog(INFO, "relation %s has no row-level security policy, skipped",
+			 RelationGetRelationName(relation));
+	}
+}
+
+/*
+ * Guts of Row-level security policy deletion.
+ */
+void
+RemoveRowLevelSecurityById(Oid relationId)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relationId));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+	{
+		simple_heap_delete(rlsrel, &tuple->t_self);
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d69809a..6aae6f8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -384,6 +385,7 @@ static void ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
 static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid);
 static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode);
 static void ATExecDropOf(Relation rel, LOCKMODE lockmode);
+static void ATExecSetRowLevelSecurity(Relation relation, Node *clause);
 static void ATExecGenericOptions(Relation rel, List *options);
 
 static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
@@ -2746,6 +2748,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_SetTableSpace:		/* must rewrite heap */
 			case AT_DropNotNull:		/* may change some SQL plans */
 			case AT_SetNotNull:
+			case AT_SetRowLevelSecurity:
+			case AT_ResetRowLevelSecurity:
 			case AT_GenericOptions:
 			case AT_AlterColumnGenericOptions:
 				cmd_lockmode = AccessExclusiveLock;
@@ -3108,6 +3112,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_DropInherit:	/* NO INHERIT */
 		case AT_AddOf:			/* OF */
 		case AT_DropOf: /* NOT OF */
+		case AT_SetRowLevelSecurity:
+		case AT_ResetRowLevelSecurity:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* These commands never recurse */
 			/* No command-specific prep needed */
@@ -3383,6 +3389,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropOf:
 			ATExecDropOf(rel, lockmode);
 			break;
+		case AT_SetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, (Node *) cmd->def);
+			break;
+		case AT_ResetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, NULL);
+			break;
 		case AT_GenericOptions:
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
@@ -7558,6 +7570,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				Assert(defaultexpr);
 				break;
 
+			case OCLASS_ROWLEVELSEC:
+				/*
+				 * A row-level security policy can depend on a column in case
+				 * when the policy clause references a particular column.
+				 * Due to same reason why TRIGGER ... WHEN does not support
+				 * to change column's type being referenced in clause, row-
+				 * level security policy also does not support it.
+				 */
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used in a row-level security policy"),
+						 errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject),
+								   colName)));
+				break;
+
 			case OCLASS_PROC:
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
@@ -9656,6 +9684,42 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode)
 }
 
 /*
+ * ALTER TABLE <name> SET ROW LEVEL SECURITY (...) OR
+ *                    RESET ROW LEVEL SECURITY
+ */
+static void
+ATExecSetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid			relid = RelationGetRelid(relation);
+	Relation	class_rel;
+	HeapTuple	tuple;
+	Form_pg_class	class_form;
+
+	if (clause != NULL)
+		SetRowLevelSecurity(relation, clause);
+	else
+		ResetRowLevelSecurity(relation);
+
+	class_rel = heap_open(RelationRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	class_form = (Form_pg_class) GETSTRUCT(tuple);
+	if (clause != NULL)
+		class_form->relhasrowlevelsec = true;
+	else
+		class_form->relhasrowlevelsec = false;
+
+	simple_heap_update(class_rel, &tuple->t_self, tuple);
+	CatalogUpdateIndexes(class_rel, tuple);
+
+	heap_close(class_rel, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
+/*
  * ALTER FOREIGN TABLE <name> OPTIONS (...)
  */
 static void
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index df76341..dabcfc6 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -32,6 +32,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/analyze.h"
@@ -163,6 +164,13 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	glob->lastPHId = 0;
 	glob->lastRowMarkId = 0;
 	glob->transientPlan = false;
+	glob->planUserId = InvalidOid;
+	/*
+	 * XXX - a valid user-id shall be set on planUserId later, if constructed
+	 * plan assumes being executed under privilege of a particular user-id.
+	 * Elsewhere, keep InvalidOid; that means the constructed plan is portable
+	 * for any users.
+	 */
 
 	/* Determine what fraction of the plan is likely to be scanned */
 	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -240,6 +248,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->nParamExec = list_length(glob->paramlist);
+	result->planUserId = glob->planUserId;
 
 	return result;
 }
@@ -314,6 +323,14 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		SS_process_ctes(root);
 
 	/*
+	 * Apply row-level security policy of appeared tables, if configured.
+	 * It must be applied prior to preprocess_rowmarks().
+	 *
+	 *
+	 */
+	applyRowLevelSecurity(root);
+
+	/*
 	 * Look for ANY and EXISTS SubLinks in WHERE and JOIN/ON clauses, and try
 	 * to transform them into joins.  Note that this step does not descend
 	 * into subqueries; if we pull up any subqueries below, their SubLinks are
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3430689 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
-       relnode.o restrictinfo.o tlist.o var.o
+       relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c
new file mode 100644
index 0000000..70a1336
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,677 @@
+/*
+ * optimizer/util/rowlvsec.c
+ *    Row-level security support routines
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
+#include "parser/parsetree.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* flags to pull row-level security policy */
+#define RLS_FLAG_HAS_SUBLINKS			0x0001
+
+/* hook to allow extensions to apply their own security policy */
+rowlevel_security_hook_type	rowlevel_security_hook = NULL;
+
+/* current performing mode of row-level security policy */
+static RowLevelSecMode	rowlevel_security_mode = RowLevelSecModeEnabled;
+
+/*
+ * getRowLevelSecurityMode / setRowLevelSecurityMode
+ *
+ * These functions allow to get or set current performing mode of row-
+ * level security feature. It enables to disable this feature temporarily
+ * for some cases in which row-level security prevent correct behavior
+ * such as foreign-key checks to prohibit update of PKs being referenced
+ * by others.
+ * The caller must ensure the saved previous mode shall be restored, but
+ * no need to care about cases when an error would be raised.
+ */
+RowLevelSecMode
+getRowLevelSecurityMode(void)
+{
+	return rowlevel_security_mode;
+}
+
+void
+setRowLevelSecurityMode(RowLevelSecMode new_mode)
+{
+	rowlevel_security_mode = new_mode;
+}
+
+/*
+ * pull_rowlevel_security_policy
+ *
+ * This routine tries to pull expression node of row-level security policy
+ * on the target relation, and its children if configured.
+ * If one or more relation has a security policy at least, this function
+ * returns true, or false elsewhere.
+ */
+static bool
+pull_rowlevel_security_policy(PlannerInfo *root,
+							  RangeTblEntry *rte,
+							  Index rtindex,
+							  List **rls_relids,
+							  List **rls_quals,
+							  List **rls_flags)
+{
+	Relation		rel;
+	LOCKMODE		lockmode;
+	List		   *relid_list = NIL;
+	List		   *qual_list = NIL;
+	List		   *flag_list = NIL;
+	ListCell	   *cell;
+	bool			result = false;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+	if (!rte->inh)
+	{
+		lockmode = NoLock;
+		relid_list = list_make1_oid(rte->relid);
+	}
+	else
+	{
+		/*
+		 * In case when the target relation may have inheritances,
+		 * we need to suggest an appropriate lock mode because it
+		 * is the first time to reference these tables in a series
+		 * of processes. For more details, see the comments in
+		 * expand_inherited_tables.
+		 * Also note that it does not guarantee the locks on child
+		 * tables being already acquired at expand_inherited_tables,
+		 * because row-level security routines can be bypassed if
+		 * RowLevelSecModeDisabled.
+		 */
+		if (rtindex == root->parse->resultRelation)
+			lockmode = RowExclusiveLock;
+		else
+		{
+			foreach (cell, root->parse->rowMarks)
+			{
+				Assert(IsA(lfirst(cell), RowMarkClause));
+				if (((RowMarkClause *) lfirst(cell))->rti == rtindex)
+					break;
+			}
+			if (cell)
+				lockmode = RowShareLock;
+			else
+				lockmode = AccessShareLock;
+		}
+		relid_list = find_all_inheritors(rte->relid, lockmode, NULL);
+	}
+
+	/*
+	 * Try to fetch row-level security policy of the target relation
+	 * or its children.
+	 */
+	foreach (cell, relid_list)
+	{
+		Expr   *qual = NULL;
+		int		flags = 0;
+
+		rel = heap_open(lfirst_oid(cell), lockmode);
+
+		/*
+		 * Pull out row-level security policy configured with built-in
+		 * features, if unprivileged users. Please note that superuser
+		 * can bypass it.
+		 */
+		if (rel->rlsdesc && !superuser())
+		{
+			RowLevelSecDesc	*rlsdesc = rel->rlsdesc;
+
+			qual = copyObject(rlsdesc->rlsqual);
+			if (rlsdesc->rlshassublinks)
+				flags |= RLS_FLAG_HAS_SUBLINKS;
+		}
+
+		/*
+		 * Also, ask extensions whether they want to apply their own
+		 * row-level security policy. If both built-in and extension
+		 * has their own policy, it shall be merged.
+		 */
+		if (rowlevel_security_hook)
+		{
+			List   *qual_list;
+
+			qual_list = (*rowlevel_security_hook)(root, rel);
+			if (qual_list != NIL)
+			{
+				if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+					contain_subplans((Node *)qual_list))
+					flags |= RLS_FLAG_HAS_SUBLINKS;
+
+				if (qual != NULL)
+					qual_list = lappend(qual_list, qual);
+
+				if (list_length(qual_list) == 1)
+					qual = (Expr *)list_head(qual_list);
+				else
+					qual = makeBoolExpr(AND_EXPR, qual_list, -1);
+			}
+		}
+
+		qual_list = lappend(qual_list, qual);
+		if (qual)
+			result = true;
+		flag_list = lappend_int(flag_list, flags);
+
+		heap_close(rel, NoLock);  /* close the relation, but keep locks */
+	}
+
+	/*
+	 * Inform the caller list of relation Oid, qualifier of row-level
+	 * security policy and its flag, if one or more target relations
+	 * have its row-level security policy. Elsewhere, release it.
+	 */
+	if (result)
+	{
+		*rls_relids = relid_list;
+		*rls_quals = qual_list;
+		*rls_flags = flag_list;
+	}
+	else
+	{
+		list_free(relid_list);
+		list_free(qual_list);
+		list_free(flag_list);
+	}
+	return result;
+}
+
+/*
+ * fixup_varattnos
+ *
+ * It fixes up varattno of Var node that referenced the relation with
+ * RLS policy, thus replaced to a sub-query. Here is no guarantee the
+ * varattno matches with TargetEntry's resno of the sub-query, so needs
+ * to adjust them.
+ */
+typedef struct {
+	PlannerInfo *root;
+	int		varlevelsup;
+} fixup_var_context;
+
+static bool
+fixup_var_references_walker(Node *node, fixup_var_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var            *var = (Var *)node;
+		RangeTblEntry  *rte;
+
+		/*
+		 * Does this Var node reference the Query node currently we focused
+		 * on. If not, we simply ignore it.
+		 */
+		if (var->varlevelsup != context->varlevelsup)
+			return false;
+
+		rte = rt_fetch(var->varno, context->root->parse->rtable);
+		if (!rte)
+			elog(ERROR, "invalid varno %d", var->varno);
+
+		if (rte->rtekind == RTE_SUBQUERY &&
+			rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+		{
+			List       *targetList = rte->subquery->targetList;
+			ListCell   *cell;
+
+			foreach (cell, targetList)
+			{
+				TargetEntry    *subtle = lfirst(cell);
+
+				if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+					 var->varattno == InvalidAttrNumber) ||
+					(IsA(subtle->expr, Var) &&
+					 var->varattno == ((Var *)(subtle->expr))->varattno))
+				{
+					var->varattno = subtle->resno;
+					return false;
+				}
+			}
+			elog(ERROR, "invalid varattno %d", var->varattno);
+		}
+		return false;
+	}
+	else if (IsA(node, Query))
+	{
+		bool    result;
+
+		context->varlevelsup++;
+		result = query_tree_walker((Query *) node,
+								   fixup_var_references_walker,
+								   (void *) context, 0);
+		context->varlevelsup--;
+
+		return result;
+	}
+	return expression_tree_walker(node,
+								  fixup_var_references_walker,
+								  (void *) context);
+}
+
+static void
+fixup_varattnos(PlannerInfo *root)
+{
+	fixup_var_context   context;
+
+	/*
+	 * Fixup Var->varattno that references the sub-queries originated from
+	 * regular relations with RLS policy.
+	 */
+	context.root = root;
+	context.varlevelsup = 0;
+
+	query_tree_walker(root->parse,
+					  fixup_var_references_walker,
+					  (void *) &context, 0);
+}
+
+/*
+ * make_pseudo_column
+ *
+ * make a TargetEntry object which references a particular column of
+ * the underlying table.
+ */
+static TargetEntry *
+make_pseudo_column(Oid relid_head, Oid relid, AttrNumber attnum)
+{
+	Form_pg_attribute attform;
+	HeapTuple	tuple;
+	Var		   *var;
+	char	   *resname;
+
+	if (attnum == InvalidAttrNumber)
+	{
+		ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);
+
+		r->arg = (Expr *) makeVar((Index) 1,
+								  InvalidAttrNumber,
+								  get_rel_type_id(relid),
+								  -1,
+								  InvalidOid,
+								  0);
+		r->resulttype = get_rel_type_id(relid_head);
+		r->convertformat = COERCE_IMPLICIT_CAST;
+		r->location = -1;
+
+		return makeTargetEntry((Expr *) r, -1, get_rel_name(relid), false);
+	}
+
+	tuple = SearchSysCache2(ATTNUM,
+							ObjectIdGetDatum(relid),
+							Int16GetDatum(attnum));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+			 attnum, relid);
+	attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+	var = makeVar((Index) 1,
+				  attform->attnum,
+				  attform->atttypid,
+				  attform->atttypmod,
+				  InvalidOid,
+				  0);
+	resname = pstrdup(NameStr(attform->attname));
+
+	ReleaseSysCache(tuple);
+
+	return makeTargetEntry((Expr *)var, -1, resname, false);
+}
+
+/*
+ * make_pseudo_subquery
+ *
+ * This routine makes a sub-query that references the target relation
+ * with given row-level security policy. This sub-query shall have
+ * security_barrier attribute to prevent unexpected push-down.
+ */
+static Query *
+make_pseudo_subquery(Oid relid_head,
+					 Oid relid,
+					 Node *qual,
+					 int flags,
+					 List *targetList,
+					 RangeTblEntry *rte,
+					 RowMarkClause *rclause)
+{
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+	List		   *colnameList = NIL;
+	ListCell	   *cell;
+
+	subqry = makeNode(Query);
+	subqry->commandType = CMD_SELECT;
+	subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	subrte = makeNode(RangeTblEntry);
+	subrte->rtekind = RTE_RELATION;
+	subrte->relid = relid;
+	subrte->relkind = get_rel_relkind(relid);
+	subrte->inFromCl = true;
+	subrte->requiredPerms = rte->requiredPerms;
+	subrte->selectedCols = NULL;
+	subrte->modifiedCols = NULL;
+
+	foreach (cell, targetList)
+	{
+		TargetEntry	   *oldtle = lfirst(cell);
+		TargetEntry	   *subtle;
+		AttrNumber		attnum;
+
+		if (IsA(oldtle->expr, ConvertRowtypeExpr))
+			attnum = InvalidAttrNumber;
+		else
+		{
+			Assert(IsA(oldtle->expr, Var));
+
+			attnum = get_attnum(relid, oldtle->resname);
+			if (attnum == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_COLUMN),
+					errmsg("column \"%s\" of relation \"%s\" does not exist",
+								oldtle->resname, get_rel_name(relid))));
+		}
+		subtle = make_pseudo_column(relid_head, relid, attnum);
+		subtle->resno = oldtle->resno;
+		subqry->targetList = lappend(subqry->targetList, subtle);
+
+		colnameList = lappend(colnameList,
+							  makeString(pstrdup(subtle->resname)));
+		attnum -= FirstLowInvalidHeapAttributeNumber;
+		subrte->selectedCols = bms_add_member(rte->selectedCols, attnum);
+	}
+	subrte->eref = makeAlias(get_rel_name(relid), colnameList);
+
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		subqry->hasSubLinks = true;
+
+	subqry->rtable = list_make1(subrte);
+
+	subrtr = makeNode(RangeTblRef);
+	subrtr->rtindex = 1;
+	subqry->jointree = makeFromExpr(list_make1(subrtr), qual);
+
+	if (rclause)
+	{
+		RowMarkClause  *rclause_sub;
+
+		rclause_sub = copyObject(rclause);
+		rclause_sub->rti = 1;
+
+		subqry->rowMarks = list_make1(rclause);
+		subqry->hasForUpdate = true;
+	}
+	return subqry;
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * This routine expand reference to the given RangeTblEntry by a sub-query
+ * which simply references the target relation with the qualifier of row-
+ * level security policy.
+ */
+static void
+expand_rtentry_with_policy(PlannerInfo *root,
+						   RangeTblEntry *rte,
+						   Index rtindex,
+						   List *rls_relids,
+						   List *rls_quals,
+						   List *rls_flags)
+{
+	Query		   *parse = root->parse;
+	Oid				relid_head;
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	List		   *targetList;
+	List		   *colnameList;
+	Bitmapset	   *attr_used;
+	AttrNumber		attnum;
+	RowMarkClause  *rclause;
+	ListCell	   *cell1;
+	ListCell	   *cell2;
+	ListCell	   *cell3;
+	ListCell	   *lc;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+	Assert(list_length(rls_relids) == list_length(rls_quals));
+	Assert(list_length(rls_relids) == list_length(rls_flags));
+	Assert(list_length(rls_relids) > 0);
+
+	/*
+	 * Construct a target-entry list
+	 */
+	targetList = NIL;
+	colnameList = NIL;
+	relid_head = linitial_oid(rls_relids);
+	attr_used = bms_union(rte->selectedCols,
+						  rte->modifiedCols);
+	while ((attnum = bms_first_member(attr_used)) >= 0)
+	{
+		attnum += FirstLowInvalidHeapAttributeNumber;
+
+		subtle = make_pseudo_column(relid_head, relid_head, attnum);
+		subtle->resno = list_length(targetList) + 1;
+
+		targetList = lappend(targetList, subtle);
+		colnameList = lappend(colnameList,
+							  makeString(pstrdup(subtle->resname)));
+	}
+	bms_free(attr_used);
+
+	/*
+	 * Push-down row-level lock of the target relation, since sub-query
+	 * does not support FOR SHARE/FOR UPDATE locks being assigned.
+	 */
+	rclause = NULL;
+	foreach (lc, parse->rowMarks)
+	{
+		if (((RowMarkClause *) lfirst(lc))->rti == rtindex)
+		{
+			rclause = lfirst(lc);
+			parse->rowMarks = list_delete(parse->rowMarks, rclause);
+			break;
+		}
+	}
+
+	/*
+	 * Construct sub-query structures
+	 */
+	forthree (cell1, rls_relids, cell2, rls_quals, cell3, rls_flags)
+	{
+		Oid			relid = lfirst_oid(cell1);
+		Node	   *qual = lfirst(cell2);
+		int			flags = lfirst_int(cell3);
+
+		subqry = make_pseudo_subquery(relid_head, relid, qual, flags,
+									  targetList, rte, rclause);
+		if (cell1 == list_head(rls_relids))
+		{
+			Assert(rte->relid == relid);
+			Assert(rte->inh == true || list_length(rls_relids) == 1);
+
+			rte->relid = InvalidOid;
+			rte->rtekind = RTE_SUBQUERY;
+			rte->subquery = subqry;
+			rte->security_barrier = true;
+
+			/* no permission checks are needed to subquery itself */
+			rte->requiredPerms = 0;
+			rte->checkAsUser = InvalidOid;
+			rte->selectedCols = NULL;
+			rte->modifiedCols = NULL;
+
+			rte->alias = NULL;
+			rte->eref = makeAlias(get_rel_name(relid),
+								  copyObject(colnameList));
+			if (list_length(rls_relids) == 1)
+				rte->inh = false;
+		}
+
+		/*
+		 * RTE's for child relations and AppendRelInfo in case when
+		 * the target relation has its children.
+		 */
+		if (list_length(rls_relids) > 1)
+		{
+			AppendRelInfo  *apinfo;
+			AttrNumber		child_rtindex;
+			ListCell	   *l;
+
+			/*
+			 * XXX - Set up RangeTblEntry for the child relation.
+			 * Note that it does not need to have security_barrier
+			 * attribute, if no row-level security policy is
+			 * configured on.
+			 */
+			subrte = makeNode(RangeTblEntry);
+			subrte->rtekind = RTE_SUBQUERY;
+			subrte->subquery = subqry;
+			if (qual)
+				subrte->security_barrier = true;
+			subrte->alias = NULL;
+			subrte->eref = makeAlias(get_rel_name(relid),
+									 copyObject(colnameList));
+			parse->rtable = lappend(parse->rtable, subrte);
+			child_rtindex = list_length(parse->rtable);
+
+			/*
+			 * reference to inherited children performs as if simple
+			 * UNION ALL operation, so add AppendRelInfo here.
+			 */
+			apinfo = makeNode(AppendRelInfo);
+			apinfo->parent_relid = rtindex;
+			apinfo->child_relid = child_rtindex;
+			foreach (l, targetList)
+			{
+				Var	   *trans_var
+					= makeVarFromTargetEntry(child_rtindex, lfirst(l));
+				apinfo->translated_vars
+					= lappend(apinfo->translated_vars, trans_var);
+			}
+			root->append_rel_list = lappend(root->append_rel_list, apinfo);
+		}
+	}
+}
+
+/*
+ * applyRowLevelSecurity
+ *
+ * It tries to apply row-level security policy of the relation.
+ * If and when a particular policy is configured on the referenced
+ * relation, it shall be replaced by a sub-query with security-barrier flag;
+ * that references the relation with row-level security policy.
+ * In the result, all users can see is rows of the relation that satisfies
+ * the condition supplied as security policy.
+ */
+void
+applyRowLevelSecurity(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	ListCell   *cell;
+	Index		rtindex;
+	bool		has_rowlevel_sec = false;
+
+	/* mode checks */
+	if (rowlevel_security_mode == RowLevelSecModeDisabled)
+		return;
+
+	/*
+	 * No need to apply row-level security on sub-query being originated
+	 * from regular relation with RLS policy any more.
+	 */
+	if (parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+		return;
+
+	rtindex = 0;
+	foreach (cell, parse->rtable)
+	{
+		RangeTblEntry  *rte = lfirst(cell);
+		List		   *rls_relids;
+		List		   *rls_quals;
+		List		   *rls_flags;
+
+		rtindex++;
+
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * XXX - In this revision, we have no support for UPDATE / DELETE
+		 * statement, so simply skip it.
+		 */
+		if (rtindex == parse->resultRelation)
+			continue;
+
+		/*
+		 * In case when a row-level security policy was configured on
+		 * the table referenced by this RangeTblEntry or its children,
+		 * it shall be rewritten to sub-query with this policy.
+		 */
+		if (pull_rowlevel_security_policy(root, rte, rtindex,
+										  &rls_relids, &rls_quals, &rls_flags))
+		{
+			expand_rtentry_with_policy(root, rte, rtindex,
+									   rls_relids, rls_quals, rls_flags);
+			has_rowlevel_sec = true;
+		}
+	}
+
+	/*
+	 * Post case handling if one or more relation was replaced to sub-query.
+	 */
+	if (has_rowlevel_sec)
+	{
+		PlanInvalItem  *pi_item;
+
+		/*
+		 * plan should be invalidated if and when userid was changed
+		 * on the executor stage, from planner stage.
+		 */
+		Assert(!OidIsValid(root->glob->planUserId) ||
+			   root->glob->planUserId == GetUserId());
+		root->glob->planUserId = GetUserId();
+
+		pi_item = makeNode(PlanInvalItem);
+		pi_item->cacheId = AUTHOID;
+		pi_item->hashValue
+			= GetSysCacheHashValue1(AUTHOID,
+									ObjectIdGetDatum(root->glob->planUserId));
+		root->glob->invalItems = lappend(root->glob->invalItems, pi_item);
+
+		/*
+		 * Since the relation with RLS policy was replaced by a sub-query,
+		 * thus resource number to reference a particular column can be
+		 * also moditifed. If we applied RLS policy on one or more relations,
+		 * varattno of Var node that has referenced the rewritten relation
+		 * needs to be fixed up.
+		 */
+		fixup_varattnos(root);
+	}
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7e6ceed..8c0a8a4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2058,6 +2058,22 @@ alter_table_cmd:
 					n->def = (Node *)$2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> SET ROW LEVEL SECURITY (expression) */
+			| SET ROW LEVEL SECURITY '(' a_expr ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_SetRowLevelSecurity;
+					n->def = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> RESET ROW LEVEL SECURITY */
+			| RESET ROW LEVEL SECURITY
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_ResetRowLevelSecurity;
+					n->def = NULL;
+					$$ = (Node *)n;
+				}
 			| alter_generic_options
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 983f631..c53cbfd 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -42,6 +42,7 @@
 #include "parser/parse_coerce.h"
 #include "parser/parse_relation.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
@@ -2998,6 +2999,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
+	RowLevelSecMode	save_rls_mode;
 	Datum		vals[RI_MAX_NUMKEYS * 2];
 	char		nulls[RI_MAX_NUMKEYS * 2];
 
@@ -3080,6 +3082,15 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
 						   save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
 
+	/*
+	 * Disabled row-level security in case when foreign-key relation is
+	 * queried to check existence of tupls that references the tuple to
+	 * be modified on the primary-key side.
+	 */
+	save_rls_mode = getRowLevelSecurityMode();
+	if (source_is_pk)
+		setRowLevelSecurityMode(RowLevelSecModeDisabled);
+
 	/* Finally we can run the query. */
 	spi_result = SPI_execute_snapshot(qplan,
 									  vals, nulls,
@@ -3089,6 +3100,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	/* Restore UID and security context */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
 
+	/* Restore row-level security performing mode */
+	setRowLevelSecurityMode(save_rls_mode);
+
 	/* Check result */
 	if (spi_result < 0)
 		elog(ERROR, "SPI_execute_snapshot returned %d", spi_result);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index c42765c..5ebd944 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -51,6 +51,7 @@
 #include "catalog/namespace.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
@@ -664,6 +665,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
 		AcquireExecutorLocks(plan->stmt_list, true);
 
 		/*
+		 * If plan was constructed with assumption of a particular user-id,
+		 * and it is different from the current one, the cached-plan shall
+		 * be invalidated to construct suitable query plan.
+		 */
+		if (plan->is_valid &&
+			OidIsValid(plan->planUserId) &&
+			plan->planUserId == GetUserId())
+			plan->is_valid = false;
+
+		/*
 		 * If plan was transient, check to see if TransactionXmin has
 		 * advanced, and if so invalidate it.
 		 */
@@ -715,6 +726,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 {
 	CachedPlan *plan;
 	List	   *plist;
+	ListCell   *cell;
+	Oid			planUserId = InvalidOid;
 	bool		snapshot_set;
 	bool		spi_pushed;
 	MemoryContext plan_context;
@@ -793,6 +806,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	PopOverrideSearchPath();
 
 	/*
+	 * Check whether the generated plan assumes a particular user-id, or not.
+	 * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+	 * it should be kept and used to validation check of the cached plan
+	 * under the "current" user-id.
+	 */
+	foreach (cell, plist)
+	{
+		PlannedStmt	*pstmt = lfirst(cell);
+
+		if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+		{
+			Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+			planUserId = pstmt->planUserId;
+		}
+	}
+
+	/*
 	 * Make a dedicated memory context for the CachedPlan and its subsidiary
 	 * data.  It's probably not going to be large, but just in case, use the
 	 * default maxsize parameter.  It's transient for the moment.
@@ -827,6 +858,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->context = plan_context;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->planUserId = planUserId;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2e6776e..11051ed 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -48,6 +48,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -895,6 +896,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	else
 		relation->trigdesc = NULL;
 
+	if (relation->rd_rel->relhasrowlevelsec)
+		RelationBuildRowLevelSecurity(relation);
+	else
+		relation->rlsdesc = NULL;
+
 	/*
 	 * if it's an index, initialize index-related information
 	 */
@@ -1784,6 +1790,8 @@ RelationDestroyRelation(Relation relation)
 		MemoryContextDelete(relation->rd_indexcxt);
 	if (relation->rd_rulescxt)
 		MemoryContextDelete(relation->rd_rulescxt);
+	if (relation->rlsdesc)
+		MemoryContextDelete(relation->rlsdesc->rlscxt);
 	pfree(relation);
 }
 
@@ -3023,7 +3031,13 @@ RelationCacheInitializePhase3(void)
 				relation->rd_rel->relhastriggers = false;
 			restart = true;
 		}
-
+		if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL)
+		{
+			RelationBuildRowLevelSecurity(relation);
+			if (relation->rlsdesc == NULL)
+				relation->rd_rel->relhasrowlevelsec = false;
+			restart = true;
+		}
 		/* Release hold on the relation */
 		RelationDecrementReferenceCount(relation);
 
@@ -4173,6 +4187,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_rules = NULL;
 		rel->rd_rulescxt = NULL;
 		rel->trigdesc = NULL;
+		rel->rlsdesc = NULL;
 		rel->rd_indexprs = NIL;
 		rel->rd_indpred = NIL;
 		rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 7d67287..0291590 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3865,6 +3865,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reloptions;
 	int			i_toastreloptions;
 	int			i_reloftype;
+	int			i_rlsqual;
 
 	/* Make sure we are in proper schema */
 	selectSourceSchema(fout, "pg_catalog");
@@ -3889,7 +3890,45 @@ getTables(Archive *fout, int *numTables)
 	 * we cannot correctly identify inherited columns, owned sequences, etc.
 	 */
 
-	if (fout->remoteVersion >= 90100)
+	if (fout->remoteVersion >= 90300)
+	{
+		/*
+		 * Left join to pick up dependency info linking sequences to their
+		 * owning column, if any (note this dependency is AUTO as of 8.2)
+		 */
+		appendPQExpBuffer(query,
+						  "SELECT c.tableoid, c.oid, c.relname, "
+						  "c.relacl, c.relkind, c.relnamespace, "
+						  "(%s c.relowner) AS rolname, "
+						  "c.relchecks, c.relhastriggers, "
+						  "c.relhasindex, c.relhasrules, c.relhasoids, "
+						  "c.relfrozenxid, tc.oid AS toid, "
+						  "tc.relfrozenxid AS tfrozenxid, "
+						  "c.relpersistence, "
+						  "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+						  "d.refobjid AS owning_tab, "
+						  "d.refobjsubid AS owning_col, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+						"array_to_string(c.reloptions, ', ') AS reloptions, "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual "
+						  "FROM pg_class c "
+						  "LEFT JOIN pg_depend d ON "
+						  "(c.relkind = '%c' AND "
+						  "d.classid = c.tableoid AND d.objid = c.oid AND "
+						  "d.objsubid = 0 AND "
+						  "d.refclassid = c.tableoid AND d.deptype = 'a') "
+					   "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+					   "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) "
+						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+						  "ORDER BY c.oid",
+						  username_subquery,
+						  RELKIND_SEQUENCE,
+						  RELKIND_RELATION, RELKIND_SEQUENCE,
+						  RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+						  RELKIND_FOREIGN_TABLE);
+	}
+	else if (fout->remoteVersion >= 90100)
 	{
 		/*
 		 * Left join to pick up dependency info linking sequences to their
@@ -3909,7 +3948,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL as rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3945,7 +3985,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3980,7 +4021,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4015,7 +4057,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4051,7 +4094,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4086,7 +4130,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4117,7 +4162,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4143,7 +4189,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4179,7 +4226,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "WHERE relkind IN ('%c', '%c') "
 						  "ORDER BY oid",
@@ -4227,6 +4275,7 @@ getTables(Archive *fout, int *numTables)
 	i_reloptions = PQfnumber(res, "reloptions");
 	i_toastreloptions = PQfnumber(res, "toast_reloptions");
 	i_reloftype = PQfnumber(res, "reloftype");
+	i_rlsqual = PQfnumber(res, "rlsqual");
 
 	if (lockWaitTimeout && fout->remoteVersion >= 70300)
 	{
@@ -4269,6 +4318,10 @@ getTables(Archive *fout, int *numTables)
 			tblinfo[i].reloftype = NULL;
 		else
 			tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype));
+		if (PQgetisnull(res, i, i_rlsqual))
+			tblinfo[i].rlsqual = NULL;
+		else
+			tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual));
 		tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
 		if (PQgetisnull(res, i, i_owning_tab))
 		{
@@ -12707,6 +12760,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 			}
 		}
 	}
+	if (tbinfo->rlsqual)
+		appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n",
+						  fmtId(tbinfo->dobj.name), tbinfo->rlsqual);
 
 	if (binary_upgrade)
 		binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index b44187bb..20803ab 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -255,6 +255,7 @@ typedef struct _tableInfo
 	uint32		toast_frozenxid;	/* for restore toast frozen xid */
 	int			ncheck;			/* # of CHECK expressions */
 	char	   *reloftype;		/* underlying type for typed table */
+	char	   *rlsqual;		/* row-level security policy */
 	/* these two are set only if table is a sequence owned by a column: */
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index f0eb564..fef31b7 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -146,6 +146,7 @@ typedef enum ObjectClass
 	OCLASS_USER_MAPPING,		/* pg_user_mapping */
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
+	OCLASS_ROWLEVELSEC,			/* pg_rowlevelsec */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 450ec25..5e4467d 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -306,6 +306,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
 DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
 #define RangeTypidIndexId					3542
 
+DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId				3839
+
 /* last step of initialization script: build the indexes declared above */
 BUILD_INDICES
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f83ce80..5b6e38b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
 	bool		relhaspkey;		/* has (or has had) PRIMARY KEY index */
 	bool		relhasrules;	/* has (or has had) any rules */
 	bool		relhastriggers; /* has (or has had) any TRIGGERs */
+	bool		relhasrowlevelsec;	/* has (or has had) row-level security */
 	bool		relhassubclass; /* has (or has had) derived classes */
 	TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
 
@@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class;
  * ----------------
  */
 
-#define Natts_pg_class					27
+#define Natts_pg_class					28
 #define Anum_pg_class_relname			1
 #define Anum_pg_class_relnamespace		2
 #define Anum_pg_class_reltype			3
@@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class;
 #define Anum_pg_class_relhaspkey		21
 #define Anum_pg_class_relhasrules		22
 #define Anum_pg_class_relhastriggers	23
-#define Anum_pg_class_relhassubclass	24
-#define Anum_pg_class_relfrozenxid		25
-#define Anum_pg_class_relacl			26
-#define Anum_pg_class_reloptions		27
+#define Anum_pg_class_relhasrowlevelsec	24
+#define Anum_pg_class_relhassubclass	25
+#define Anum_pg_class_relfrozenxid		26
+#define Anum_pg_class_relacl			27
+#define Anum_pg_class_reloptions		28
 
 /* ----------------
  *		initial contents of pg_class
@@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class;
  */
 
 /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
-DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
 
 
diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h
new file mode 100644
index 0000000..5a64d1b
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,60 @@
+/*
+ * pg_rowlevelsec.h
+ *   definition of the system catalog for row-level security policy
+ *   (pg_rowlevelsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWLEVELSEC_H
+#define PG_ROWLEVELSEC_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ *		pg_rowlevelsec definition. cpp turns this into
+ *		typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId	3838
+
+CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS
+{
+	Oid				rlsrelid;
+#ifdef CATALOG_VARLEN
+	pg_node_tree	rlsqual;
+#endif
+} FormData_pg_rowlevelsec;
+
+/* ----------------
+ *		Form_pg_rowlevelsec corresponds to a pointer to a row with
+ *		the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec;
+
+/* ----------------
+ * 		compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowlevelsec				2
+#define Anum_pg_rowlevelsec_rlsrelid		1
+#define Anum_pg_rowlevelsec_rlsqual			2
+
+typedef struct
+{
+	MemoryContext	rlscxt;
+	Expr		   *rlsqual;
+	bool			rlshassublinks;
+} RowLevelSecDesc;
+
+extern void	RelationBuildRowLevelSecurity(Relation relation);
+extern void	SetRowLevelSecurity(Relation relation, Node *clause);
+extern void	ResetRowLevelSecurity(Relation relation);
+extern void	RemoveRowLevelSecurityById(Oid relationId);
+
+#endif  /* PG_ROWLEVELSEC_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 50111cb..de85fff 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
 	QSRC_PARSER,				/* added by parse analysis (now unused) */
 	QSRC_INSTEAD_RULE,			/* added by unconditional INSTEAD rule */
 	QSRC_QUAL_INSTEAD_RULE,		/* added by conditional INSTEAD rule */
-	QSRC_NON_INSTEAD_RULE		/* added by non-INSTEAD rule */
+	QSRC_NON_INSTEAD_RULE,		/* added by non-INSTEAD rule */
+	QSRC_ROW_LEVEL_SECURITY,	/* added by row-level security */
 } QuerySource;
 
 /* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -1227,6 +1228,8 @@ typedef enum AlterTableType
 	AT_DropInherit,				/* NO INHERIT parent */
 	AT_AddOf,					/* OF <type_name> */
 	AT_DropOf,					/* NOT OF */
+	AT_SetRowLevelSecurity,		/* SET ROW LEVEL SECURITY (...) */
+	AT_ResetRowLevelSecurity,	/* RESET ROW LEVEL SECURITY */
 	AT_GenericOptions			/* OPTIONS (...) */
 } AlterTableType;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fb9a863..6b3ea3d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
 	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
 
 	int			nParamExec;		/* number of PARAM_EXEC Params used */
+
+	Oid			planUserId;		/* user-id this plan assumed, or InvalidOid */
 } PlannedStmt;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index cf0bbd9..26f986a 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
 	Index		lastRowMarkId;	/* highest PlanRowMark ID assigned */
 
 	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
+
+	Oid			planUserId;		/* User-Id to be assumed on this plan */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h
new file mode 100644
index 0000000..9afe00a
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,31 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlevelsec.h
+ *    prototypes for optimizer/rowlevelsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLEVELSEC_H
+#define ROWLEVELSEC_H
+
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(PlannerInfo *root,
+											 Relation relation);
+extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook;
+
+typedef enum {
+	RowLevelSecModeEnabled,
+	RowLevelSecModeDisabled,
+} RowLevelSecMode;
+
+extern RowLevelSecMode getRowLevelSecurityMode(void);
+extern void setRowLevelSecurityMode(RowLevelSecMode new_mode);
+
+extern void	applyRowLevelSecurity(PlannerInfo *root);
+
+#endif	/* ROWLEVELSEC_H */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e846..5f89028 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -115,6 +115,8 @@ typedef struct CachedPlan
 								 * bare utility statements) */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	Oid			planUserId;		/* is user-id that is assumed on this cached
+								   plan, or InvalidOid if portable for anybody */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4669d8a..dce8463 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_index.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "fmgr.h"
 #include "nodes/bitmapset.h"
 #include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
 	RuleLock   *rd_rules;		/* rewrite rules */
 	MemoryContext rd_rulescxt;	/* private memory cxt for rd_rules, if any */
 	TriggerDesc *trigdesc;		/* Trigger info, or NULL if rel has none */
+	RowLevelSecDesc	*rlsdesc;	/* Row-level security info, or NULL */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out
new file mode 100644
index 0000000..5673e7a
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,753 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+(16 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-C-A)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 220 |      2 | 0101      | this document is secret, category(-C-A)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+ 240 |      2 | 0111      | this document is secret, category(-CBA)
+(24 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Hash Join  (cost=30.34..57.95 rows=2 width=117)
+   Hash Cond: (rls_regress_schema.document.did = browse.did)
+   ->  Seq Scan on document  (cost=8.27..31.15 rows=343 width=49)
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=4)
+                 Index Cond: (pguser = "current_user"())
+   ->  Hash  (cost=22.06..22.06 rows=1 width=72)
+         ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+               Filter: f_leak((browse.browse)::text)
+               ->  Seq Scan on browse  (cost=0.00..22.00 rows=4 width=168)
+                     Filter: (pguser = "current_user"())
+(12 rows)
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ERROR:  must be owner of relation document
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+ERROR:  must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Nested Loop  (cost=8.27..55.90 rows=1 width=117)
+   Join Filter: (rls_regress_schema.document.did = browse.did)
+   ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+         Filter: f_leak((browse.browse)::text)
+         ->  Seq Scan on browse  (cost=0.00..22.00 rows=4 width=168)
+               Filter: (pguser = "current_user"())
+   ->  Seq Scan on document  (cost=8.27..33.72 rows=5 width=49)
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=9)
+                 Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  10 |      0 | 0000      | this document is unclassified, category(----) |                   | 
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  30 |      0 | 0010      | this document is unclassified, category(--B-) |                   | 
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+  90 |      1 | 0000      | this document is classified, category(----)   |                   | 
+ 100 |      1 | 0001      | this document is classified, category(---A)   |                   | 
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+ 120 |      1 | 0011      | this document is classified, category(--BA)   |                   | 
+ 170 |      2 | 0000      | this document is secret, category(----)       |                   | 
+ 180 |      2 | 0001      | this document is secret, category(---A)       |                   | 
+ 190 |      2 | 0010      | this document is secret, category(--B-)       |                   | 
+ 200 |      2 | 0011      | this document is secret, category(--BA)       |                   | 
+(12 rows)
+
+DELETE FROM document WHERE did = 30;			-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(30) is still referenced from table "browse".
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(90) is still referenced from table "browse".
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+SELECT * FROM t1;
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+SELECT * FROM t1 WHERE f_leak(c);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.c)
+               ->  Seq Scan on t1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.c)
+               ->  Seq Scan on t2
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+               Filter: f_leak(c)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a |  c  
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a |  c  |   t1    
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2
+                     Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  Seq Scan on t3
+(10 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  LockRows
+                     ->  Seq Scan on t1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  LockRows
+                     ->  Seq Scan on t2
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3
+(13 rows)
+
+SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.c)
+               ->  LockRows
+                     ->  Seq Scan on t1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.c)
+               ->  LockRows
+                     ->  Seq Scan on t2
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3
+                           Filter: f_leak(c)
+(16 rows)
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+                     QUERY PLAN                     
+----------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a <= 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a <= 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(c);
+NOTICE:  f_leak => aaa
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => bcd
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => def
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+           QUERY PLAN            
+---------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: f_leak(c)
+         ->  Seq Scan on t2 t1
+               Filter: f_leak(c)
+         ->  Seq Scan on t3 t1
+               Filter: f_leak(c)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a |  c  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+           QUERY PLAN           
+--------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t2 t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t3 t1
+               Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+          QUERY PLAN           
+-------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a = 2)
+         ->  Seq Scan on t2 t1
+               Filter: (a = 2)
+         ->  Seq Scan on t3 t1
+               Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+                    QUERY PLAN                     
+---------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a = 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a = 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE:  drop cascades to 7 other objects
+DETAIL:  drop cascades to table account
+drop cascades to function f_leak(text)
+drop cascades to table document
+drop cascades to table browse
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 7f560d2..fd50d9e 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -119,6 +119,7 @@ SELECT relname, relhasindex
  pg_proc                 | t
  pg_range                | t
  pg_rewrite              | t
+ pg_rowlevelsec          | t
  pg_seclabel             | t
  pg_shdepend             | t
  pg_shdescription        | t
@@ -164,7 +165,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(153 rows)
+(154 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8852e0a..d368504 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: privileges security_label collate
+test: privileges rowlevelsec security_label collate
 
 test: misc
 # rules cannot run concurrently with any test that creates a view
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 0bc5df7..290b24b 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: privileges
+test: rowlevelsec
 test: security_label
 test: collate
 test: misc
diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql
new file mode 100644
index 0000000..a4d1ccd
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,237 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+DELETE FROM document WHERE did = 30;			-- failed
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101	1	aaa
+102	2	bbb
+103	3	ccc
+104	4	ddd
+\.
+
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201	1	abc	1.1
+202	2	bcd	2.2
+203	3	cde	3.3
+204	4	def	4.4
+\.
+
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 FROM stdin WITH (oids);
+301	1	xxx	X
+302	2	yyy	Y
+303	3	zzz	Z
+\.
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
#17Robert Haas
robertmhaas@gmail.com
In reply to: Kohei KaiGai (#16)
Re: [v9.3] Row-Level Security

On Sun, Jul 15, 2012 at 5:52 AM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a revised version of row-level security feature.

I added a feature to invalidate plan cache if user-id was switched
between planner and optimizer. It enabled to generate more optimized
plan than the previous approach; that adds hardwired "OR superuser()".

Example)
postgres=# PREPARE p1(int) AS SELECT * FROM t1 WHERE x > $1 AND f_leak(y);
PREPARE
postgres=# EXPLAIN (costs off) EXECUTE p1(2);
QUERY PLAN
-----------------------------------
Seq Scan on t1
Filter: (f_leak(y) AND (x > 2))
(2 rows)

postgres=# SET SESSION AUTHORIZATION alice;
SET
postgres=> EXPLAIN (costs off) EXECUTE p1(2);
QUERY PLAN
---------------------------------------------
Subquery Scan on t1
Filter: f_leak(t1.y)
-> Seq Scan on t1
Filter: ((x > 2) AND ((x % 2) = 0))
(4 rows)

On the other hand, I removed support for UPDATE / DELETE commands
in this revision, because I'm still uncertain on the implementation that I
adopted in the previous patch. I believe it helps to keep patch size being
minimum reasonable.
Due to same reason, RLS is not supported on COPY TO command.

According to the Robert's comment, I revised the place to inject
applyRowLevelSecurity(). The reason why it needed to patch on
adjust_appendrel_attrs_mutator() was, we handled expansion from
regular relation to sub-query after expand_inherited_tables().
In this revision, it was moved to the head of sub-query planner.

Even though I added a syscache entry for pg_rowlevelsec catalog,
it was revised to read the catalog on construction of relcache, like
trigger descriptor, because it enables to reduce cost to parse an
expression tree in text format and memory consumption of hash
slot.

This revision adds support on pg_dump, and also adds support
to include SubLinks in the row-level security policy.

This revision is too late for this CommitFest; I've moved it to the
next CommitFest and will look at it then, or hopefully sooner.

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

#18Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Robert Haas (#17)
Re: [v9.3] Row-Level Security

2012/7/17 Robert Haas <robertmhaas@gmail.com>:

On Sun, Jul 15, 2012 at 5:52 AM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a revised version of row-level security feature.

I added a feature to invalidate plan cache if user-id was switched
between planner and optimizer. It enabled to generate more optimized
plan than the previous approach; that adds hardwired "OR superuser()".

Example)
postgres=# PREPARE p1(int) AS SELECT * FROM t1 WHERE x > $1 AND f_leak(y);
PREPARE
postgres=# EXPLAIN (costs off) EXECUTE p1(2);
QUERY PLAN
-----------------------------------
Seq Scan on t1
Filter: (f_leak(y) AND (x > 2))
(2 rows)

postgres=# SET SESSION AUTHORIZATION alice;
SET
postgres=> EXPLAIN (costs off) EXECUTE p1(2);
QUERY PLAN
---------------------------------------------
Subquery Scan on t1
Filter: f_leak(t1.y)
-> Seq Scan on t1
Filter: ((x > 2) AND ((x % 2) = 0))
(4 rows)

On the other hand, I removed support for UPDATE / DELETE commands
in this revision, because I'm still uncertain on the implementation that I
adopted in the previous patch. I believe it helps to keep patch size being
minimum reasonable.
Due to same reason, RLS is not supported on COPY TO command.

According to the Robert's comment, I revised the place to inject
applyRowLevelSecurity(). The reason why it needed to patch on
adjust_appendrel_attrs_mutator() was, we handled expansion from
regular relation to sub-query after expand_inherited_tables().
In this revision, it was moved to the head of sub-query planner.

Even though I added a syscache entry for pg_rowlevelsec catalog,
it was revised to read the catalog on construction of relcache, like
trigger descriptor, because it enables to reduce cost to parse an
expression tree in text format and memory consumption of hash
slot.

This revision adds support on pg_dump, and also adds support
to include SubLinks in the row-level security policy.

This revision is too late for this CommitFest; I've moved it to the
next CommitFest and will look at it then, or hopefully sooner.

It seems to me fair enough. I may be able to add UPDATE /
DELETE support until next commit-fest.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#19Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Kohei KaiGai (#18)
Re: [v9.3] Row-Level Security

On 17 July 2012 05:02, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

2012/7/17 Robert Haas <robertmhaas@gmail.com>:

On Sun, Jul 15, 2012 at 5:52 AM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a revised version of row-level security feature.
...
According to the Robert's comment, I revised the place to inject
applyRowLevelSecurity(). The reason why it needed to patch on
adjust_appendrel_attrs_mutator() was, we handled expansion from
regular relation to sub-query after expand_inherited_tables().
In this revision, it was moved to the head of sub-query planner.

Hi,

I had a quick look at this and spotted a problem - certain types of
query are able to bypass the RLS quals. For example:

SELECT * FROM (SELECT * FROM foo) foo;

since the RLS policy doesn't descend into subqueries, and is applied
before they are pulled up into the main query. Similarly for views on
top of tables with RLS, and SRF functions that query a table with RLS
that get inlined.

Also queries using UNION ALL are vulnerable if they end up being
flattened, for example:

SELECT * FROM foo UNION ALL SELECT * FROM foo;

FWIW I recently developed some similar code as part of a patch to
implement automatically updatable views
(http://archives.postgresql.org/pgsql-hackers/2012-08/msg00303.php).
Some parts of that code may be useful, possibly for adding
UPDATE/DELETE support.

Regards,
Dean

#20Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Dean Rasheed (#19)
1 attachment(s)
Re: [v9.3] Row-Level Security

2012/9/2 Dean Rasheed <dean.a.rasheed@gmail.com>:

On 17 July 2012 05:02, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

2012/7/17 Robert Haas <robertmhaas@gmail.com>:

On Sun, Jul 15, 2012 at 5:52 AM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a revised version of row-level security feature.
...
According to the Robert's comment, I revised the place to inject
applyRowLevelSecurity(). The reason why it needed to patch on
adjust_appendrel_attrs_mutator() was, we handled expansion from
regular relation to sub-query after expand_inherited_tables().
In this revision, it was moved to the head of sub-query planner.

Hi,

I had a quick look at this and spotted a problem - certain types of
query are able to bypass the RLS quals. For example:

SELECT * FROM (SELECT * FROM foo) foo;

since the RLS policy doesn't descend into subqueries, and is applied
before they are pulled up into the main query. Similarly for views on
top of tables with RLS, and SRF functions that query a table with RLS
that get inlined.

Also queries using UNION ALL are vulnerable if they end up being
flattened, for example:

SELECT * FROM foo UNION ALL SELECT * FROM foo;

Thanks for your comment.

Indeed, I missed the case of simple sub-queries and union-all being
pulled up into the main query. So, I adjusted the location to invoke
applyRowLevelSecurity() between all the pull-up stuff and expanding
inherited tables.

The attached patch is a fixed and rebased revision for CF:Sep.

FWIW I recently developed some similar code as part of a patch to
implement automatically updatable views
(http://archives.postgresql.org/pgsql-hackers/2012-08/msg00303.php).
Some parts of that code may be useful, possibly for adding
UPDATE/DELETE support.

Let me check it.

Best regards,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

Attachments:

pgsql-v9.3-row-level-security.ro.v3.patchapplication/octet-stream; name=pgsql-v9.3-row-level-security.ro.v3.patchDownload
 doc/src/sgml/catalogs.sgml                 |  59 +++
 doc/src/sgml/ref/alter_table.sgml          |  38 ++
 doc/src/sgml/user-manag.sgml               | 123 +++++
 src/backend/access/transam/xact.c          |  12 +
 src/backend/catalog/Makefile               |   4 +-
 src/backend/catalog/dependency.c           |  23 +
 src/backend/catalog/heap.c                 |   1 +
 src/backend/catalog/pg_rowlevelsec.c       | 240 +++++++++
 src/backend/commands/tablecmds.c           |  64 +++
 src/backend/optimizer/plan/planner.c       |  17 +
 src/backend/optimizer/util/Makefile        |   2 +-
 src/backend/optimizer/util/rowlevelsec.c   | 677 ++++++++++++++++++++++++++
 src/backend/parser/gram.y                  |  16 +
 src/backend/parser/parse_agg.c             |   6 +
 src/backend/parser/parse_expr.c            |   3 +
 src/backend/utils/adt/ri_triggers.c        |  14 +
 src/backend/utils/cache/plancache.c        |  32 ++
 src/backend/utils/cache/relcache.c         |  17 +-
 src/bin/pg_dump/pg_dump.c                  |  76 ++-
 src/bin/pg_dump/pg_dump.h                  |   1 +
 src/include/catalog/dependency.h           |   1 +
 src/include/catalog/indexing.h             |   3 +
 src/include/catalog/pg_class.h             |  20 +-
 src/include/catalog/pg_rowlevelsec.h       |  60 +++
 src/include/nodes/parsenodes.h             |   5 +-
 src/include/nodes/plannodes.h              |   2 +
 src/include/nodes/relation.h               |   2 +
 src/include/optimizer/rowlevelsec.h        |  31 ++
 src/include/parser/parse_node.h            |   3 +-
 src/include/utils/plancache.h              |   2 +
 src/include/utils/rel.h                    |   2 +
 src/test/regress/expected/rowlevelsec.out  | 753 +++++++++++++++++++++++++++++
 src/test/regress/expected/sanity_check.out |   3 +-
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/serial_schedule           |   1 +
 src/test/regress/sql/rowlevelsec.sql       | 237 +++++++++
 36 files changed, 2525 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 3bcd82c..5a5233e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
      </row>
 
      <row>
+      <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+      <entry>row-level security policy of relation</entry>
+     </row>
+
+     <row>
       <entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
       <entry>security labels on database objects</entry>
      </row>
@@ -1807,6 +1812,16 @@
      </row>
 
      <row>
+      <entry><structfield>relhasrowlevelsec</structfield></entry>
+      <entry><type>bool</type></entry>
+      <entry></entry>
+      <entry>
+       True if table has row-level security policy; see
+       <link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link> catalog
+      </entry>
+     </row>
+
+     <row>
       <entry><structfield>relhassubclass</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
@@ -4906,6 +4921,50 @@
 
  </sect1>
 
+ <sect1 id="catalog-pg-rowlevelsec">
+  <title><structname>pg_rowlevelsec</structname></title>
+
+  <indexterm zone="catalog-pg-rowlevelsec">
+   <primary>pg_rowlevelsec</primary>
+  </indexterm>
+  <para>
+   The catalog <structname>pg_rowlevelsec</structname> expression tree of
+   row-level security policy to be performed on a particular relation.
+  </para>
+  <table>
+   <title><structname>pg_rowlevelsec</structname> Columns</title>
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>rlsrelid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+      <entry>The table this row-level security is for</entry>
+     </row>
+     <row>
+      <entry><structfield>rlsqual</structfield></entry>
+      <entry><type>text</type></entry>
+      <entry></entry>
+      <entry>An expression tree to be performed as rowl-level security policy</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+  <note>
+   <para>
+    <literal>pg_class.relhasrowlevelsec</literal>
+    must be true if a table has row-level security policy in this catalog.
+   </para>
+  </note>
+ </sect1>
 
  <sect1 id="catalog-pg-seclabel">
   <title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index b764814..f3f2d91 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     NOT OF
     OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
     SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+    SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)
+    RESET ROW LEVEL SECURITY
 
 <phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
 
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
    </varlistentry>
 
    <varlistentry>
+    <term><literal>SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+    <listitem>
+     <para>
+      This form set row-level security policy of the table.
+      Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+      as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+      clause, although mechanism guarantees to evaluate this condition earlier
+      than any other user given condition.
+      See also <xref linkend="ROW-LEVEL-SECURITY">.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET ROW LEVEL SECURITY</literal></term>
+    <listitem>
+     <para>
+      This form reset row-level security policy of the table, if exists.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
@@ -806,6 +831,19 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="PARAMETER">condition</replaceable></term>
+      <listitem>
+       <para>
+        An expression that returns a value of type boolean. Expect for a case
+        when queries are executed with superuser privilege, only rows for which
+        this expression returns true will be fetched, updated or deleted.
+        This expression can reference columns of the relation being configured,
+        however, unavailable to include sub-query right now.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c283e07 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,127 @@ DROP ROLE <replaceable>name</replaceable>;
   </para>
  </sect1>
 
+ <sect1 id="row-level-security">
+  <title>Row-level Security</title>
+  <para>
+   <productname>PostgreSQL</productname> v9.3 or later provides
+   row-level security feature, like several commercial database
+   management system. It allows table owner to assign a particular
+   condition that performs as a security policy of the table; only
+   rows that satisfies the condition should be visible, except for
+   a case when superuser runs queries.
+  </para>
+  <para>
+   Row-level security policy can be set using
+   <literal>SET ROW LEVEL SECURITY</literal> command of
+   <xref linkend="SQL-ALTERTABLE"> statement, as an expression
+    form that returns a value of type boolean. This expression can
+    contain references to columns of the relation, so it enables
+    to construct arbitrary rule to make access control decision
+    based on contents of each rows.
+  </para>
+  <para>
+   For example, the following <literal>customer</literal> table
+   has <literal>uname</literal> field to store user name, and
+   it assume we don't want to expose any properties of other
+   customers.
+   The following command set <literal>current_user = uname</literal>
+   as row-level security policy on the <literal>customer</literal>
+   table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+</screen>
+   <xref linkend="SQL-EXPLAIN"> command shows how row-level
+   security policy works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+                 QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+   Filter: f_leak(customer.upasswd)
+   ->  Seq Scan on customer
+         Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+   This query execution plan means the preconfigured row-level
+   security policy is implicitly added, and scan plan on the
+   target relation being wrapped up with a sub-query.
+   It ensures user given qualifiers, including functions with
+   side effects, are never executed earlier than the row-level
+   security policy regardless of its cost, except for the cases
+   when these were fully leakproof.
+   This design helps to tackle the scenario described in
+   <xref linkend="RULES-PRIVILEGES">; that introduces the order
+   to evaluate qualifiers is significant to keep confidentiality
+   of invisible rows.
+  </para>
+
+  <para>
+   On the other hand, this design allows superusers to bypass
+   checks with row-level security.
+   It ensures <application>pg_dump</application> can obtain
+   a complete set of database backup, and avoid to execute
+   Trojan horse trap, being injected as a row-level security
+   policy of user-defined table, with privileges of superuser.
+  </para>
+
+  <para>
+   In case of queries on inherited tables, row-level security
+   policy of the parent relation is not applied to child
+   relations. Scope of the row-level security policy is limited
+   to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+                QUERY PLAN
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.y)
+               ->  Seq Scan on t1
+                     Filter: ((x % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: f_leak(y)
+         ->  Subquery Scan on t3
+               Filter: f_leak(t3.y)
+               ->  Seq Scan on t3
+                     Filter: ((x % 2) = 1)
+(12 rows)
+</screen>
+    In the above example, <literal>t1</literal> has inherited
+    child table <literal>t2</literal> and <literal>t3</literal>,
+    and row-level security policy is set on only <literal>t1</literal>,
+    and <literal>t3</literal>, not <literal>t2</literal>.
+
+    The row-level security policy of <literal>t1</literal>,
+    <literal>x</literal> must be even-number, is appended only
+    <literal>t1</literal>, neither <literal>t2</literal> nor
+    <literal>t3</literal>. On the contrary, <literal>t3</literal>
+    has different row-level security policy; <literal>x</literal>
+    must be odd-number.
+  </para>
+
+  <para>
+   Right now, row-level security feature has several limitation,
+   although these shall be improved in the future version.
+
+   Row-level security policy is not applied to
+   <command>UPDATE</command> or <command>DELETE</command>
+   commands in this revision, even though it should be
+   supported soon.
+
+   Row-level security policy is not applied to rows to be inserted
+   and newer revision of updated rows, thus, it requires to
+   define before-row-insert or before-row-update trigger to check
+   whether the row's contents satisfies the policy individually.
+
+   Although it is not a specific matter in row-level security,
+   <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH">
+   allows to switch current user identifier during execution
+   of the query. Thus, it may cause unpredicated behavior
+   if and when <literal>current_user</literal> is used as
+   a part of row-level security policy.
+  </para>
+ </sect1>
 </chapter>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e7a6606..851f9db 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
 #include "executor/spi.h"
 #include "libpq/be-fsstubs.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "pgstat.h"
 #include "replication/walsender.h"
 #include "replication/syncrep.h"
@@ -144,6 +145,7 @@ typedef struct TransactionStateData
 	int			maxChildXids;	/* allocated size of childXids[] */
 	Oid			prevUser;		/* previous CurrentUserId setting */
 	int			prevSecContext; /* previous SecurityRestrictionContext */
+	RowLevelSecMode	prevRowLevelSecMode;	/* previous RLS-mode setting */
 	bool		prevXactReadOnly;		/* entry-time xact r/o state */
 	bool		startedInRecovery;		/* did we start in recovery? */
 	struct TransactionStateData *parent;		/* back link to parent */
@@ -173,6 +175,7 @@ static TransactionStateData TopTransactionStateData = {
 	0,							/* allocated size of childXids[] */
 	InvalidOid,					/* previous CurrentUserId setting */
 	0,							/* previous SecurityRestrictionContext */
+	RowLevelSecModeEnabled,		/* previous RowLevelSecMode setting */
 	false,						/* entry-time xact r/o state */
 	false,						/* startedInRecovery */
 	NULL						/* link to parent state block */
@@ -1764,6 +1767,8 @@ StartTransaction(void)
 	/* SecurityRestrictionContext should never be set outside a transaction */
 	Assert(s->prevSecContext == 0);
 
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
+
 	/*
 	 * initialize other subsystems for new transaction
 	 */
@@ -2295,6 +2300,9 @@ AbortTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Also, mode setting of row-level security should be restored. */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * do abort processing
 	 */
@@ -4188,6 +4196,9 @@ AbortSubTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Reset row-level security mode */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * We can skip all this stuff if the subxact failed before creating a
 	 * ResourceOwner...
@@ -4327,6 +4338,7 @@ PushTransaction(void)
 	s->state = TRANS_DEFAULT;
 	s->blockState = TBLOCK_SUBBEGIN;
 	GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext);
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
 	s->prevXactReadOnly = XactReadOnly;
 
 	CurrentTransactionState = s;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index df6da1f..965aa38 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
        objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \
        pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
        pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
-       pg_type.o storage.o toasting.o
+       pg_rowlevelsec.o pg_type.o storage.o toasting.o
 
 BKIFILES = postgres.bki postgres.description postgres.shdescription
 
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
-	pg_foreign_table.h \
+	pg_foreign_table.h pg_rowlevelsec.h \
 	pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
 	toasting.h indexing.h \
     )
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 87d6f02..f0736b1 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
@@ -1229,6 +1230,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveExtensionById(object->objectId);
 			break;
 
+		case OCLASS_ROWLEVELSEC:
+			RemoveRowLevelSecurityById(object->objectId);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized object class: %u",
 				 object->classId);
@@ -2280,6 +2285,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case EventTriggerRelationId:
 			return OCLASS_EVENT_TRIGGER;
+
+		case RowLevelSecurityRelationId:
+			return OCLASS_ROWLEVELSEC;
 	}
 
 	/* shouldn't get here */
@@ -2929,6 +2937,21 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_ROWLEVELSEC:
+			{
+				char	   *relname;
+
+				relname = get_rel_name(object->objectId);
+				if (!relname)
+					elog(ERROR, "cache lookup failed for relation %u",
+						 object->objectId);
+				appendStringInfo(&buffer,
+								 _("row-level security of %s"), relname);
+
+
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index c80df41..60de76d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -776,6 +776,7 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
 	values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
 	values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+	values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec);
 	values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	if (relacl != (Datum) 0)
diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c
new file mode 100644
index 0000000..4241b95
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,240 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlevelsec.c
+ *    routines to support manipulation of the pg_rowlevelsec relation
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+void
+RelationBuildRowLevelSecurity(Relation relation)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+
+	tuple = systable_getnext(sscan);
+	if (HeapTupleIsValid(tuple))
+	{
+		RowLevelSecDesc	*rlsdesc;
+		MemoryContext	rlscxt;
+		MemoryContext	oldcxt;
+		Datum	datum;
+		bool	isnull;
+		char   *temp;
+
+		/*
+		 * Make the private memory context to store RowLevelSecDesc that
+		 * includes expression tree also.
+		 */
+		rlscxt = AllocSetContextCreate(CacheMemoryContext,
+									   RelationGetRelationName(relation),
+									   ALLOCSET_SMALL_MINSIZE,
+									   ALLOCSET_SMALL_INITSIZE,
+									   ALLOCSET_SMALL_MAXSIZE);
+		PG_TRY();
+		{
+			datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual,
+								 RelationGetDescr(rlsrel), &isnull);
+			Assert(!isnull);
+			temp = TextDatumGetCString(datum);
+
+			oldcxt = MemoryContextSwitchTo(rlscxt);
+
+			rlsdesc = palloc0(sizeof(RowLevelSecDesc));
+			rlsdesc->rlscxt = rlscxt;
+			rlsdesc->rlsqual = (Expr *) stringToNode(temp);
+			Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID);
+
+			rlsdesc->rlshassublinks
+				= contain_subplans((Node *)rlsdesc->rlsqual);
+
+			MemoryContextSwitchTo(oldcxt);
+
+			pfree(temp);
+		}
+		PG_CATCH();
+		{
+			MemoryContextDelete(rlscxt);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		relation->rlsdesc = rlsdesc;
+	}
+	else
+	{
+		relation->rlsdesc = NULL;
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, AccessShareLock);
+}
+
+void
+SetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid				relationId = RelationGetRelid(relation);
+	ParseState	   *pstate;
+	RangeTblEntry  *rte;
+	Node		   *qual;
+	Relation		rlsrel;
+	ScanKeyData		skey;
+	SysScanDesc		sscan;
+	HeapTuple		oldtup;
+	HeapTuple		newtup;
+	Datum			values[Natts_pg_rowlevelsec];
+	bool			isnull[Natts_pg_rowlevelsec];
+	bool			replaces[Natts_pg_rowlevelsec];
+	ObjectAddress	target;
+	ObjectAddress	myself;
+
+	/* Parse the supplied clause */
+	pstate = make_parsestate(NULL);
+
+	rte = addRangeTableEntryForRelation(pstate, relation,
+										NULL, false, false);
+	addRTEtoQuery(pstate, rte, false, true, true);
+
+	qual = transformWhereClause(pstate, copyObject(clause),
+								EXPR_KIND_ROW_LEVEL_SEC,
+								"ROW LEVEL SECURITY");
+	/* zero-clear */
+	memset(values,   0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(isnull,   0, sizeof(isnull));
+
+	/* Update or Indert an entry to pg_rowlevelsec catalog  */
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	oldtup = systable_getnext(sscan);
+	if (HeapTupleIsValid(oldtup))
+	{
+		replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true;
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+
+		newtup = heap_modify_tuple(oldtup,
+								   RelationGetDescr(rlsrel),
+								   values, isnull, replaces);
+		simple_heap_update(rlsrel, &newtup->t_self, newtup);
+
+		deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+								   relationId, false);
+	}
+	else
+	{
+		values[Anum_pg_rowlevelsec_rlsrelid - 1]
+			= ObjectIdGetDatum(relationId);
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+		newtup = heap_form_tuple(RelationGetDescr(rlsrel),
+								 values, isnull);
+		simple_heap_insert(rlsrel, newtup);
+	}
+	CatalogUpdateIndexes(rlsrel, newtup);
+
+	heap_freetuple(newtup);
+
+	/* records dependencies of RLS-policy and relation/columns */
+	target.classId = RelationRelationId;
+	target.objectId = relationId;
+	target.objectSubId = 0;
+
+	myself.classId = RowLevelSecurityRelationId;
+	myself.objectId = relationId;
+	myself.objectSubId = 0;
+
+	recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+	recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+						   DEPENDENCY_NORMAL);
+	free_parsestate(pstate);
+
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+void
+ResetRowLevelSecurity(Relation relation)
+{
+	if (relation->rlsdesc)
+	{
+		ObjectAddress	address;
+
+		address.classId = RowLevelSecurityRelationId;
+		address.objectId = RelationGetRelid(relation);
+		address.objectSubId = 0;
+
+		performDeletion(&address, DROP_RESTRICT, 0);
+	}
+	else
+	{
+		/* Nothing to do here */
+		elog(INFO, "relation %s has no row-level security policy, skipped",
+			 RelationGetRelationName(relation));
+	}
+}
+
+/*
+ * Guts of Row-level security policy deletion.
+ */
+void
+RemoveRowLevelSecurityById(Oid relationId)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relationId));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+	{
+		simple_heap_delete(rlsrel, &tuple->t_self);
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 359d478..83ca86b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -384,6 +385,7 @@ static void ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
 static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid);
 static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode);
 static void ATExecDropOf(Relation rel, LOCKMODE lockmode);
+static void ATExecSetRowLevelSecurity(Relation relation, Node *clause);
 static void ATExecGenericOptions(Relation rel, List *options);
 
 static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
@@ -2746,6 +2748,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_SetTableSpace:		/* must rewrite heap */
 			case AT_DropNotNull:		/* may change some SQL plans */
 			case AT_SetNotNull:
+			case AT_SetRowLevelSecurity:
+			case AT_ResetRowLevelSecurity:
 			case AT_GenericOptions:
 			case AT_AlterColumnGenericOptions:
 				cmd_lockmode = AccessExclusiveLock;
@@ -3108,6 +3112,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_DropInherit:	/* NO INHERIT */
 		case AT_AddOf:			/* OF */
 		case AT_DropOf: /* NOT OF */
+		case AT_SetRowLevelSecurity:
+		case AT_ResetRowLevelSecurity:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* These commands never recurse */
 			/* No command-specific prep needed */
@@ -3383,6 +3389,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropOf:
 			ATExecDropOf(rel, lockmode);
 			break;
+		case AT_SetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, (Node *) cmd->def);
+			break;
+		case AT_ResetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, NULL);
+			break;
 		case AT_GenericOptions:
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
@@ -7533,6 +7545,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				Assert(defaultexpr);
 				break;
 
+			case OCLASS_ROWLEVELSEC:
+				/*
+				 * A row-level security policy can depend on a column in case
+				 * when the policy clause references a particular column.
+				 * Due to same reason why TRIGGER ... WHEN does not support
+				 * to change column's type being referenced in clause, row-
+				 * level security policy also does not support it.
+				 */
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used in a row-level security policy"),
+						 errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject),
+								   colName)));
+				break;
+
 			case OCLASS_PROC:
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
@@ -9631,6 +9659,42 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode)
 }
 
 /*
+ * ALTER TABLE <name> SET ROW LEVEL SECURITY (...) OR
+ *                    RESET ROW LEVEL SECURITY
+ */
+static void
+ATExecSetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid			relid = RelationGetRelid(relation);
+	Relation	class_rel;
+	HeapTuple	tuple;
+	Form_pg_class	class_form;
+
+	if (clause != NULL)
+		SetRowLevelSecurity(relation, clause);
+	else
+		ResetRowLevelSecurity(relation);
+
+	class_rel = heap_open(RelationRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	class_form = (Form_pg_class) GETSTRUCT(tuple);
+	if (clause != NULL)
+		class_form->relhasrowlevelsec = true;
+	else
+		class_form->relhasrowlevelsec = false;
+
+	simple_heap_update(class_rel, &tuple->t_self, tuple);
+	CatalogUpdateIndexes(class_rel, tuple);
+
+	heap_close(class_rel, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
+/*
  * ALTER FOREIGN TABLE <name> OPTIONS (...)
  */
 static void
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 4284eed..19758c0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/analyze.h"
@@ -167,6 +168,13 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	glob->lastPHId = 0;
 	glob->lastRowMarkId = 0;
 	glob->transientPlan = false;
+	glob->planUserId = InvalidOid;
+	/*
+	 * XXX - a valid user-id shall be set on planUserId later, if constructed
+	 * plan assumes being executed under privilege of a particular user-id.
+	 * Elsewhere, keep InvalidOid; that means the constructed plan is portable
+	 * for any users.
+	 */
 
 	/* Determine what fraction of the plan is likely to be scanned */
 	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -244,6 +252,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->nParamExec = list_length(glob->paramlist);
+	result->planUserId = glob->planUserId;
 
 	return result;
 }
@@ -318,6 +327,14 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		SS_process_ctes(root);
 
 	/*
+	 * Apply row-level security policy of appeared tables, if configured.
+	 * It must be applied prior to preprocess_rowmarks().
+	 *
+	 *
+	 */
+	applyRowLevelSecurity(root);
+
+	/*
 	 * Look for ANY and EXISTS SubLinks in WHERE and JOIN/ON clauses, and try
 	 * to transform them into joins.  Note that this step does not descend
 	 * into subqueries; if we pull up any subqueries below, their SubLinks are
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3430689 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
-       relnode.o restrictinfo.o tlist.o var.o
+       relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c
new file mode 100644
index 0000000..70a1336
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,677 @@
+/*
+ * optimizer/util/rowlvsec.c
+ *    Row-level security support routines
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
+#include "parser/parsetree.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* flags to pull row-level security policy */
+#define RLS_FLAG_HAS_SUBLINKS			0x0001
+
+/* hook to allow extensions to apply their own security policy */
+rowlevel_security_hook_type	rowlevel_security_hook = NULL;
+
+/* current performing mode of row-level security policy */
+static RowLevelSecMode	rowlevel_security_mode = RowLevelSecModeEnabled;
+
+/*
+ * getRowLevelSecurityMode / setRowLevelSecurityMode
+ *
+ * These functions allow to get or set current performing mode of row-
+ * level security feature. It enables to disable this feature temporarily
+ * for some cases in which row-level security prevent correct behavior
+ * such as foreign-key checks to prohibit update of PKs being referenced
+ * by others.
+ * The caller must ensure the saved previous mode shall be restored, but
+ * no need to care about cases when an error would be raised.
+ */
+RowLevelSecMode
+getRowLevelSecurityMode(void)
+{
+	return rowlevel_security_mode;
+}
+
+void
+setRowLevelSecurityMode(RowLevelSecMode new_mode)
+{
+	rowlevel_security_mode = new_mode;
+}
+
+/*
+ * pull_rowlevel_security_policy
+ *
+ * This routine tries to pull expression node of row-level security policy
+ * on the target relation, and its children if configured.
+ * If one or more relation has a security policy at least, this function
+ * returns true, or false elsewhere.
+ */
+static bool
+pull_rowlevel_security_policy(PlannerInfo *root,
+							  RangeTblEntry *rte,
+							  Index rtindex,
+							  List **rls_relids,
+							  List **rls_quals,
+							  List **rls_flags)
+{
+	Relation		rel;
+	LOCKMODE		lockmode;
+	List		   *relid_list = NIL;
+	List		   *qual_list = NIL;
+	List		   *flag_list = NIL;
+	ListCell	   *cell;
+	bool			result = false;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+	if (!rte->inh)
+	{
+		lockmode = NoLock;
+		relid_list = list_make1_oid(rte->relid);
+	}
+	else
+	{
+		/*
+		 * In case when the target relation may have inheritances,
+		 * we need to suggest an appropriate lock mode because it
+		 * is the first time to reference these tables in a series
+		 * of processes. For more details, see the comments in
+		 * expand_inherited_tables.
+		 * Also note that it does not guarantee the locks on child
+		 * tables being already acquired at expand_inherited_tables,
+		 * because row-level security routines can be bypassed if
+		 * RowLevelSecModeDisabled.
+		 */
+		if (rtindex == root->parse->resultRelation)
+			lockmode = RowExclusiveLock;
+		else
+		{
+			foreach (cell, root->parse->rowMarks)
+			{
+				Assert(IsA(lfirst(cell), RowMarkClause));
+				if (((RowMarkClause *) lfirst(cell))->rti == rtindex)
+					break;
+			}
+			if (cell)
+				lockmode = RowShareLock;
+			else
+				lockmode = AccessShareLock;
+		}
+		relid_list = find_all_inheritors(rte->relid, lockmode, NULL);
+	}
+
+	/*
+	 * Try to fetch row-level security policy of the target relation
+	 * or its children.
+	 */
+	foreach (cell, relid_list)
+	{
+		Expr   *qual = NULL;
+		int		flags = 0;
+
+		rel = heap_open(lfirst_oid(cell), lockmode);
+
+		/*
+		 * Pull out row-level security policy configured with built-in
+		 * features, if unprivileged users. Please note that superuser
+		 * can bypass it.
+		 */
+		if (rel->rlsdesc && !superuser())
+		{
+			RowLevelSecDesc	*rlsdesc = rel->rlsdesc;
+
+			qual = copyObject(rlsdesc->rlsqual);
+			if (rlsdesc->rlshassublinks)
+				flags |= RLS_FLAG_HAS_SUBLINKS;
+		}
+
+		/*
+		 * Also, ask extensions whether they want to apply their own
+		 * row-level security policy. If both built-in and extension
+		 * has their own policy, it shall be merged.
+		 */
+		if (rowlevel_security_hook)
+		{
+			List   *qual_list;
+
+			qual_list = (*rowlevel_security_hook)(root, rel);
+			if (qual_list != NIL)
+			{
+				if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+					contain_subplans((Node *)qual_list))
+					flags |= RLS_FLAG_HAS_SUBLINKS;
+
+				if (qual != NULL)
+					qual_list = lappend(qual_list, qual);
+
+				if (list_length(qual_list) == 1)
+					qual = (Expr *)list_head(qual_list);
+				else
+					qual = makeBoolExpr(AND_EXPR, qual_list, -1);
+			}
+		}
+
+		qual_list = lappend(qual_list, qual);
+		if (qual)
+			result = true;
+		flag_list = lappend_int(flag_list, flags);
+
+		heap_close(rel, NoLock);  /* close the relation, but keep locks */
+	}
+
+	/*
+	 * Inform the caller list of relation Oid, qualifier of row-level
+	 * security policy and its flag, if one or more target relations
+	 * have its row-level security policy. Elsewhere, release it.
+	 */
+	if (result)
+	{
+		*rls_relids = relid_list;
+		*rls_quals = qual_list;
+		*rls_flags = flag_list;
+	}
+	else
+	{
+		list_free(relid_list);
+		list_free(qual_list);
+		list_free(flag_list);
+	}
+	return result;
+}
+
+/*
+ * fixup_varattnos
+ *
+ * It fixes up varattno of Var node that referenced the relation with
+ * RLS policy, thus replaced to a sub-query. Here is no guarantee the
+ * varattno matches with TargetEntry's resno of the sub-query, so needs
+ * to adjust them.
+ */
+typedef struct {
+	PlannerInfo *root;
+	int		varlevelsup;
+} fixup_var_context;
+
+static bool
+fixup_var_references_walker(Node *node, fixup_var_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var            *var = (Var *)node;
+		RangeTblEntry  *rte;
+
+		/*
+		 * Does this Var node reference the Query node currently we focused
+		 * on. If not, we simply ignore it.
+		 */
+		if (var->varlevelsup != context->varlevelsup)
+			return false;
+
+		rte = rt_fetch(var->varno, context->root->parse->rtable);
+		if (!rte)
+			elog(ERROR, "invalid varno %d", var->varno);
+
+		if (rte->rtekind == RTE_SUBQUERY &&
+			rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+		{
+			List       *targetList = rte->subquery->targetList;
+			ListCell   *cell;
+
+			foreach (cell, targetList)
+			{
+				TargetEntry    *subtle = lfirst(cell);
+
+				if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+					 var->varattno == InvalidAttrNumber) ||
+					(IsA(subtle->expr, Var) &&
+					 var->varattno == ((Var *)(subtle->expr))->varattno))
+				{
+					var->varattno = subtle->resno;
+					return false;
+				}
+			}
+			elog(ERROR, "invalid varattno %d", var->varattno);
+		}
+		return false;
+	}
+	else if (IsA(node, Query))
+	{
+		bool    result;
+
+		context->varlevelsup++;
+		result = query_tree_walker((Query *) node,
+								   fixup_var_references_walker,
+								   (void *) context, 0);
+		context->varlevelsup--;
+
+		return result;
+	}
+	return expression_tree_walker(node,
+								  fixup_var_references_walker,
+								  (void *) context);
+}
+
+static void
+fixup_varattnos(PlannerInfo *root)
+{
+	fixup_var_context   context;
+
+	/*
+	 * Fixup Var->varattno that references the sub-queries originated from
+	 * regular relations with RLS policy.
+	 */
+	context.root = root;
+	context.varlevelsup = 0;
+
+	query_tree_walker(root->parse,
+					  fixup_var_references_walker,
+					  (void *) &context, 0);
+}
+
+/*
+ * make_pseudo_column
+ *
+ * make a TargetEntry object which references a particular column of
+ * the underlying table.
+ */
+static TargetEntry *
+make_pseudo_column(Oid relid_head, Oid relid, AttrNumber attnum)
+{
+	Form_pg_attribute attform;
+	HeapTuple	tuple;
+	Var		   *var;
+	char	   *resname;
+
+	if (attnum == InvalidAttrNumber)
+	{
+		ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);
+
+		r->arg = (Expr *) makeVar((Index) 1,
+								  InvalidAttrNumber,
+								  get_rel_type_id(relid),
+								  -1,
+								  InvalidOid,
+								  0);
+		r->resulttype = get_rel_type_id(relid_head);
+		r->convertformat = COERCE_IMPLICIT_CAST;
+		r->location = -1;
+
+		return makeTargetEntry((Expr *) r, -1, get_rel_name(relid), false);
+	}
+
+	tuple = SearchSysCache2(ATTNUM,
+							ObjectIdGetDatum(relid),
+							Int16GetDatum(attnum));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+			 attnum, relid);
+	attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+	var = makeVar((Index) 1,
+				  attform->attnum,
+				  attform->atttypid,
+				  attform->atttypmod,
+				  InvalidOid,
+				  0);
+	resname = pstrdup(NameStr(attform->attname));
+
+	ReleaseSysCache(tuple);
+
+	return makeTargetEntry((Expr *)var, -1, resname, false);
+}
+
+/*
+ * make_pseudo_subquery
+ *
+ * This routine makes a sub-query that references the target relation
+ * with given row-level security policy. This sub-query shall have
+ * security_barrier attribute to prevent unexpected push-down.
+ */
+static Query *
+make_pseudo_subquery(Oid relid_head,
+					 Oid relid,
+					 Node *qual,
+					 int flags,
+					 List *targetList,
+					 RangeTblEntry *rte,
+					 RowMarkClause *rclause)
+{
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+	List		   *colnameList = NIL;
+	ListCell	   *cell;
+
+	subqry = makeNode(Query);
+	subqry->commandType = CMD_SELECT;
+	subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	subrte = makeNode(RangeTblEntry);
+	subrte->rtekind = RTE_RELATION;
+	subrte->relid = relid;
+	subrte->relkind = get_rel_relkind(relid);
+	subrte->inFromCl = true;
+	subrte->requiredPerms = rte->requiredPerms;
+	subrte->selectedCols = NULL;
+	subrte->modifiedCols = NULL;
+
+	foreach (cell, targetList)
+	{
+		TargetEntry	   *oldtle = lfirst(cell);
+		TargetEntry	   *subtle;
+		AttrNumber		attnum;
+
+		if (IsA(oldtle->expr, ConvertRowtypeExpr))
+			attnum = InvalidAttrNumber;
+		else
+		{
+			Assert(IsA(oldtle->expr, Var));
+
+			attnum = get_attnum(relid, oldtle->resname);
+			if (attnum == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_COLUMN),
+					errmsg("column \"%s\" of relation \"%s\" does not exist",
+								oldtle->resname, get_rel_name(relid))));
+		}
+		subtle = make_pseudo_column(relid_head, relid, attnum);
+		subtle->resno = oldtle->resno;
+		subqry->targetList = lappend(subqry->targetList, subtle);
+
+		colnameList = lappend(colnameList,
+							  makeString(pstrdup(subtle->resname)));
+		attnum -= FirstLowInvalidHeapAttributeNumber;
+		subrte->selectedCols = bms_add_member(rte->selectedCols, attnum);
+	}
+	subrte->eref = makeAlias(get_rel_name(relid), colnameList);
+
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		subqry->hasSubLinks = true;
+
+	subqry->rtable = list_make1(subrte);
+
+	subrtr = makeNode(RangeTblRef);
+	subrtr->rtindex = 1;
+	subqry->jointree = makeFromExpr(list_make1(subrtr), qual);
+
+	if (rclause)
+	{
+		RowMarkClause  *rclause_sub;
+
+		rclause_sub = copyObject(rclause);
+		rclause_sub->rti = 1;
+
+		subqry->rowMarks = list_make1(rclause);
+		subqry->hasForUpdate = true;
+	}
+	return subqry;
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * This routine expand reference to the given RangeTblEntry by a sub-query
+ * which simply references the target relation with the qualifier of row-
+ * level security policy.
+ */
+static void
+expand_rtentry_with_policy(PlannerInfo *root,
+						   RangeTblEntry *rte,
+						   Index rtindex,
+						   List *rls_relids,
+						   List *rls_quals,
+						   List *rls_flags)
+{
+	Query		   *parse = root->parse;
+	Oid				relid_head;
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	List		   *targetList;
+	List		   *colnameList;
+	Bitmapset	   *attr_used;
+	AttrNumber		attnum;
+	RowMarkClause  *rclause;
+	ListCell	   *cell1;
+	ListCell	   *cell2;
+	ListCell	   *cell3;
+	ListCell	   *lc;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+	Assert(list_length(rls_relids) == list_length(rls_quals));
+	Assert(list_length(rls_relids) == list_length(rls_flags));
+	Assert(list_length(rls_relids) > 0);
+
+	/*
+	 * Construct a target-entry list
+	 */
+	targetList = NIL;
+	colnameList = NIL;
+	relid_head = linitial_oid(rls_relids);
+	attr_used = bms_union(rte->selectedCols,
+						  rte->modifiedCols);
+	while ((attnum = bms_first_member(attr_used)) >= 0)
+	{
+		attnum += FirstLowInvalidHeapAttributeNumber;
+
+		subtle = make_pseudo_column(relid_head, relid_head, attnum);
+		subtle->resno = list_length(targetList) + 1;
+
+		targetList = lappend(targetList, subtle);
+		colnameList = lappend(colnameList,
+							  makeString(pstrdup(subtle->resname)));
+	}
+	bms_free(attr_used);
+
+	/*
+	 * Push-down row-level lock of the target relation, since sub-query
+	 * does not support FOR SHARE/FOR UPDATE locks being assigned.
+	 */
+	rclause = NULL;
+	foreach (lc, parse->rowMarks)
+	{
+		if (((RowMarkClause *) lfirst(lc))->rti == rtindex)
+		{
+			rclause = lfirst(lc);
+			parse->rowMarks = list_delete(parse->rowMarks, rclause);
+			break;
+		}
+	}
+
+	/*
+	 * Construct sub-query structures
+	 */
+	forthree (cell1, rls_relids, cell2, rls_quals, cell3, rls_flags)
+	{
+		Oid			relid = lfirst_oid(cell1);
+		Node	   *qual = lfirst(cell2);
+		int			flags = lfirst_int(cell3);
+
+		subqry = make_pseudo_subquery(relid_head, relid, qual, flags,
+									  targetList, rte, rclause);
+		if (cell1 == list_head(rls_relids))
+		{
+			Assert(rte->relid == relid);
+			Assert(rte->inh == true || list_length(rls_relids) == 1);
+
+			rte->relid = InvalidOid;
+			rte->rtekind = RTE_SUBQUERY;
+			rte->subquery = subqry;
+			rte->security_barrier = true;
+
+			/* no permission checks are needed to subquery itself */
+			rte->requiredPerms = 0;
+			rte->checkAsUser = InvalidOid;
+			rte->selectedCols = NULL;
+			rte->modifiedCols = NULL;
+
+			rte->alias = NULL;
+			rte->eref = makeAlias(get_rel_name(relid),
+								  copyObject(colnameList));
+			if (list_length(rls_relids) == 1)
+				rte->inh = false;
+		}
+
+		/*
+		 * RTE's for child relations and AppendRelInfo in case when
+		 * the target relation has its children.
+		 */
+		if (list_length(rls_relids) > 1)
+		{
+			AppendRelInfo  *apinfo;
+			AttrNumber		child_rtindex;
+			ListCell	   *l;
+
+			/*
+			 * XXX - Set up RangeTblEntry for the child relation.
+			 * Note that it does not need to have security_barrier
+			 * attribute, if no row-level security policy is
+			 * configured on.
+			 */
+			subrte = makeNode(RangeTblEntry);
+			subrte->rtekind = RTE_SUBQUERY;
+			subrte->subquery = subqry;
+			if (qual)
+				subrte->security_barrier = true;
+			subrte->alias = NULL;
+			subrte->eref = makeAlias(get_rel_name(relid),
+									 copyObject(colnameList));
+			parse->rtable = lappend(parse->rtable, subrte);
+			child_rtindex = list_length(parse->rtable);
+
+			/*
+			 * reference to inherited children performs as if simple
+			 * UNION ALL operation, so add AppendRelInfo here.
+			 */
+			apinfo = makeNode(AppendRelInfo);
+			apinfo->parent_relid = rtindex;
+			apinfo->child_relid = child_rtindex;
+			foreach (l, targetList)
+			{
+				Var	   *trans_var
+					= makeVarFromTargetEntry(child_rtindex, lfirst(l));
+				apinfo->translated_vars
+					= lappend(apinfo->translated_vars, trans_var);
+			}
+			root->append_rel_list = lappend(root->append_rel_list, apinfo);
+		}
+	}
+}
+
+/*
+ * applyRowLevelSecurity
+ *
+ * It tries to apply row-level security policy of the relation.
+ * If and when a particular policy is configured on the referenced
+ * relation, it shall be replaced by a sub-query with security-barrier flag;
+ * that references the relation with row-level security policy.
+ * In the result, all users can see is rows of the relation that satisfies
+ * the condition supplied as security policy.
+ */
+void
+applyRowLevelSecurity(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	ListCell   *cell;
+	Index		rtindex;
+	bool		has_rowlevel_sec = false;
+
+	/* mode checks */
+	if (rowlevel_security_mode == RowLevelSecModeDisabled)
+		return;
+
+	/*
+	 * No need to apply row-level security on sub-query being originated
+	 * from regular relation with RLS policy any more.
+	 */
+	if (parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+		return;
+
+	rtindex = 0;
+	foreach (cell, parse->rtable)
+	{
+		RangeTblEntry  *rte = lfirst(cell);
+		List		   *rls_relids;
+		List		   *rls_quals;
+		List		   *rls_flags;
+
+		rtindex++;
+
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * XXX - In this revision, we have no support for UPDATE / DELETE
+		 * statement, so simply skip it.
+		 */
+		if (rtindex == parse->resultRelation)
+			continue;
+
+		/*
+		 * In case when a row-level security policy was configured on
+		 * the table referenced by this RangeTblEntry or its children,
+		 * it shall be rewritten to sub-query with this policy.
+		 */
+		if (pull_rowlevel_security_policy(root, rte, rtindex,
+										  &rls_relids, &rls_quals, &rls_flags))
+		{
+			expand_rtentry_with_policy(root, rte, rtindex,
+									   rls_relids, rls_quals, rls_flags);
+			has_rowlevel_sec = true;
+		}
+	}
+
+	/*
+	 * Post case handling if one or more relation was replaced to sub-query.
+	 */
+	if (has_rowlevel_sec)
+	{
+		PlanInvalItem  *pi_item;
+
+		/*
+		 * plan should be invalidated if and when userid was changed
+		 * on the executor stage, from planner stage.
+		 */
+		Assert(!OidIsValid(root->glob->planUserId) ||
+			   root->glob->planUserId == GetUserId());
+		root->glob->planUserId = GetUserId();
+
+		pi_item = makeNode(PlanInvalItem);
+		pi_item->cacheId = AUTHOID;
+		pi_item->hashValue
+			= GetSysCacheHashValue1(AUTHOID,
+									ObjectIdGetDatum(root->glob->planUserId));
+		root->glob->invalItems = lappend(root->glob->invalItems, pi_item);
+
+		/*
+		 * Since the relation with RLS policy was replaced by a sub-query,
+		 * thus resource number to reference a particular column can be
+		 * also moditifed. If we applied RLS policy on one or more relations,
+		 * varattno of Var node that has referenced the rewritten relation
+		 * needs to be fixed up.
+		 */
+		fixup_varattnos(root);
+	}
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 5894cb0..0a86ce8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2068,6 +2068,22 @@ alter_table_cmd:
 					n->def = (Node *)$2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> SET ROW LEVEL SECURITY (expression) */
+			| SET ROW LEVEL SECURITY '(' a_expr ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_SetRowLevelSecurity;
+					n->def = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> RESET ROW LEVEL SECURITY */
+			| RESET ROW LEVEL SECURITY
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_ResetRowLevelSecurity;
+					n->def = NULL;
+					$$ = (Node *)n;
+				}
 			| alter_generic_options
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index d1d835b..87423d8 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("aggregate functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("aggregate functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("window functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("window functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e9267c5..94c25d1 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1443,6 +1443,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
+		case EXPR_KIND_ROW_LEVEL_SEC:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2609,6 +2610,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "EXECUTE";
 		case EXPR_KIND_TRIGGER_WHEN:
 			return "WHEN";
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			return "ROW LEVEL SECURITY";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 97e68b1..19a2255 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -43,6 +43,7 @@
 #include "parser/parse_coerce.h"
 #include "parser/parse_relation.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
@@ -2999,6 +3000,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
+	RowLevelSecMode	save_rls_mode;
 	Datum		vals[RI_MAX_NUMKEYS * 2];
 	char		nulls[RI_MAX_NUMKEYS * 2];
 
@@ -3081,6 +3083,15 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
 						   save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
 
+	/*
+	 * Disabled row-level security in case when foreign-key relation is
+	 * queried to check existence of tupls that references the tuple to
+	 * be modified on the primary-key side.
+	 */
+	save_rls_mode = getRowLevelSecurityMode();
+	if (source_is_pk)
+		setRowLevelSecurityMode(RowLevelSecModeDisabled);
+
 	/* Finally we can run the query. */
 	spi_result = SPI_execute_snapshot(qplan,
 									  vals, nulls,
@@ -3090,6 +3101,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	/* Restore UID and security context */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
 
+	/* Restore row-level security performing mode */
+	setRowLevelSecurityMode(save_rls_mode);
+
 	/* Check result */
 	if (spi_result < 0)
 		elog(ERROR, "SPI_execute_snapshot returned %d", spi_result);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 8c0391f..36a8750 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -51,6 +51,7 @@
 #include "catalog/namespace.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
@@ -665,6 +666,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
 		AcquireExecutorLocks(plan->stmt_list, true);
 
 		/*
+		 * If plan was constructed with assumption of a particular user-id,
+		 * and it is different from the current one, the cached-plan shall
+		 * be invalidated to construct suitable query plan.
+		 */
+		if (plan->is_valid &&
+			OidIsValid(plan->planUserId) &&
+			plan->planUserId == GetUserId())
+			plan->is_valid = false;
+
+		/*
 		 * If plan was transient, check to see if TransactionXmin has
 		 * advanced, and if so invalidate it.
 		 */
@@ -716,6 +727,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 {
 	CachedPlan *plan;
 	List	   *plist;
+	ListCell   *cell;
+	Oid			planUserId = InvalidOid;
 	bool		snapshot_set;
 	bool		spi_pushed;
 	MemoryContext plan_context;
@@ -794,6 +807,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	PopOverrideSearchPath();
 
 	/*
+	 * Check whether the generated plan assumes a particular user-id, or not.
+	 * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+	 * it should be kept and used to validation check of the cached plan
+	 * under the "current" user-id.
+	 */
+	foreach (cell, plist)
+	{
+		PlannedStmt	*pstmt = lfirst(cell);
+
+		if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+		{
+			Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+			planUserId = pstmt->planUserId;
+		}
+	}
+
+	/*
 	 * Make a dedicated memory context for the CachedPlan and its subsidiary
 	 * data.  It's probably not going to be large, but just in case, use the
 	 * default maxsize parameter.  It's transient for the moment.
@@ -828,6 +859,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->context = plan_context;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->planUserId = planUserId;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0cdd30d..f37586e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -896,6 +897,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	else
 		relation->trigdesc = NULL;
 
+	if (relation->rd_rel->relhasrowlevelsec)
+		RelationBuildRowLevelSecurity(relation);
+	else
+		relation->rlsdesc = NULL;
+
 	/*
 	 * if it's an index, initialize index-related information
 	 */
@@ -1785,6 +1791,8 @@ RelationDestroyRelation(Relation relation)
 		MemoryContextDelete(relation->rd_indexcxt);
 	if (relation->rd_rulescxt)
 		MemoryContextDelete(relation->rd_rulescxt);
+	if (relation->rlsdesc)
+		MemoryContextDelete(relation->rlsdesc->rlscxt);
 	pfree(relation);
 }
 
@@ -3024,7 +3032,13 @@ RelationCacheInitializePhase3(void)
 				relation->rd_rel->relhastriggers = false;
 			restart = true;
 		}
-
+		if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL)
+		{
+			RelationBuildRowLevelSecurity(relation);
+			if (relation->rlsdesc == NULL)
+				relation->rd_rel->relhasrowlevelsec = false;
+			restart = true;
+		}
 		/* Release hold on the relation */
 		RelationDecrementReferenceCount(relation);
 
@@ -4174,6 +4188,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_rules = NULL;
 		rel->rd_rulescxt = NULL;
 		rel->trigdesc = NULL;
+		rel->rlsdesc = NULL;
 		rel->rd_indexprs = NIL;
 		rel->rd_indpred = NIL;
 		rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index cdbed20..e781dae 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3867,6 +3867,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reloptions;
 	int			i_toastreloptions;
 	int			i_reloftype;
+	int			i_rlsqual;
 
 	/* Make sure we are in proper schema */
 	selectSourceSchema(fout, "pg_catalog");
@@ -3891,7 +3892,45 @@ getTables(Archive *fout, int *numTables)
 	 * we cannot correctly identify inherited columns, owned sequences, etc.
 	 */
 
-	if (fout->remoteVersion >= 90100)
+	if (fout->remoteVersion >= 90300)
+	{
+		/*
+		 * Left join to pick up dependency info linking sequences to their
+		 * owning column, if any (note this dependency is AUTO as of 8.2)
+		 */
+		appendPQExpBuffer(query,
+						  "SELECT c.tableoid, c.oid, c.relname, "
+						  "c.relacl, c.relkind, c.relnamespace, "
+						  "(%s c.relowner) AS rolname, "
+						  "c.relchecks, c.relhastriggers, "
+						  "c.relhasindex, c.relhasrules, c.relhasoids, "
+						  "c.relfrozenxid, tc.oid AS toid, "
+						  "tc.relfrozenxid AS tfrozenxid, "
+						  "c.relpersistence, "
+						  "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+						  "d.refobjid AS owning_tab, "
+						  "d.refobjsubid AS owning_col, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+						"array_to_string(c.reloptions, ', ') AS reloptions, "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual "
+						  "FROM pg_class c "
+						  "LEFT JOIN pg_depend d ON "
+						  "(c.relkind = '%c' AND "
+						  "d.classid = c.tableoid AND d.objid = c.oid AND "
+						  "d.objsubid = 0 AND "
+						  "d.refclassid = c.tableoid AND d.deptype = 'a') "
+					   "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+					   "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) "
+						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+						  "ORDER BY c.oid",
+						  username_subquery,
+						  RELKIND_SEQUENCE,
+						  RELKIND_RELATION, RELKIND_SEQUENCE,
+						  RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+						  RELKIND_FOREIGN_TABLE);
+	}
+	else if (fout->remoteVersion >= 90100)
 	{
 		/*
 		 * Left join to pick up dependency info linking sequences to their
@@ -3911,7 +3950,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL as rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3947,7 +3987,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3982,7 +4023,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4017,7 +4059,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4053,7 +4096,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4088,7 +4132,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4119,7 +4164,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4145,7 +4191,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4181,7 +4228,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "WHERE relkind IN ('%c', '%c') "
 						  "ORDER BY oid",
@@ -4229,6 +4277,7 @@ getTables(Archive *fout, int *numTables)
 	i_reloptions = PQfnumber(res, "reloptions");
 	i_toastreloptions = PQfnumber(res, "toast_reloptions");
 	i_reloftype = PQfnumber(res, "reloftype");
+	i_rlsqual = PQfnumber(res, "rlsqual");
 
 	if (lockWaitTimeout && fout->remoteVersion >= 70300)
 	{
@@ -4271,6 +4320,10 @@ getTables(Archive *fout, int *numTables)
 			tblinfo[i].reloftype = NULL;
 		else
 			tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype));
+		if (PQgetisnull(res, i, i_rlsqual))
+			tblinfo[i].rlsqual = NULL;
+		else
+			tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual));
 		tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
 		if (PQgetisnull(res, i, i_owning_tab))
 		{
@@ -12803,6 +12856,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 			}
 		}
 	}
+	if (tbinfo->rlsqual)
+		appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n",
+						  fmtId(tbinfo->dobj.name), tbinfo->rlsqual);
 
 	if (binary_upgrade)
 		binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2aa2060..5485101 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -256,6 +256,7 @@ typedef struct _tableInfo
 	uint32		toast_frozenxid;	/* for restore toast frozen xid */
 	int			ncheck;			/* # of CHECK expressions */
 	char	   *reloftype;		/* underlying type for typed table */
+	char	   *rlsqual;		/* row-level security policy */
 	/* these two are set only if table is a sequence owned by a column: */
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8499768..49baa0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
 	OCLASS_EVENT_TRIGGER,		/* pg_event_trigger */
+	OCLASS_ROWLEVELSEC,			/* pg_rowlevelsec */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 238fe58..a3b07e2 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
 DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
 #define RangeTypidIndexId					3542
 
+DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId				3839
+
 /* last step of initialization script: build the indexes declared above */
 BUILD_INDICES
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f83ce80..5b6e38b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
 	bool		relhaspkey;		/* has (or has had) PRIMARY KEY index */
 	bool		relhasrules;	/* has (or has had) any rules */
 	bool		relhastriggers; /* has (or has had) any TRIGGERs */
+	bool		relhasrowlevelsec;	/* has (or has had) row-level security */
 	bool		relhassubclass; /* has (or has had) derived classes */
 	TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
 
@@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class;
  * ----------------
  */
 
-#define Natts_pg_class					27
+#define Natts_pg_class					28
 #define Anum_pg_class_relname			1
 #define Anum_pg_class_relnamespace		2
 #define Anum_pg_class_reltype			3
@@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class;
 #define Anum_pg_class_relhaspkey		21
 #define Anum_pg_class_relhasrules		22
 #define Anum_pg_class_relhastriggers	23
-#define Anum_pg_class_relhassubclass	24
-#define Anum_pg_class_relfrozenxid		25
-#define Anum_pg_class_relacl			26
-#define Anum_pg_class_reloptions		27
+#define Anum_pg_class_relhasrowlevelsec	24
+#define Anum_pg_class_relhassubclass	25
+#define Anum_pg_class_relfrozenxid		26
+#define Anum_pg_class_relacl			27
+#define Anum_pg_class_reloptions		28
 
 /* ----------------
  *		initial contents of pg_class
@@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class;
  */
 
 /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
-DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
 
 
diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h
new file mode 100644
index 0000000..5a64d1b
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,60 @@
+/*
+ * pg_rowlevelsec.h
+ *   definition of the system catalog for row-level security policy
+ *   (pg_rowlevelsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWLEVELSEC_H
+#define PG_ROWLEVELSEC_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ *		pg_rowlevelsec definition. cpp turns this into
+ *		typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId	3838
+
+CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS
+{
+	Oid				rlsrelid;
+#ifdef CATALOG_VARLEN
+	pg_node_tree	rlsqual;
+#endif
+} FormData_pg_rowlevelsec;
+
+/* ----------------
+ *		Form_pg_rowlevelsec corresponds to a pointer to a row with
+ *		the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec;
+
+/* ----------------
+ * 		compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowlevelsec				2
+#define Anum_pg_rowlevelsec_rlsrelid		1
+#define Anum_pg_rowlevelsec_rlsqual			2
+
+typedef struct
+{
+	MemoryContext	rlscxt;
+	Expr		   *rlsqual;
+	bool			rlshassublinks;
+} RowLevelSecDesc;
+
+extern void	RelationBuildRowLevelSecurity(Relation relation);
+extern void	SetRowLevelSecurity(Relation relation, Node *clause);
+extern void	ResetRowLevelSecurity(Relation relation);
+extern void	RemoveRowLevelSecurityById(Oid relationId);
+
+#endif  /* PG_ROWLEVELSEC_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 19178b5..2f9de9f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
 	QSRC_PARSER,				/* added by parse analysis (now unused) */
 	QSRC_INSTEAD_RULE,			/* added by unconditional INSTEAD rule */
 	QSRC_QUAL_INSTEAD_RULE,		/* added by conditional INSTEAD rule */
-	QSRC_NON_INSTEAD_RULE		/* added by non-INSTEAD rule */
+	QSRC_NON_INSTEAD_RULE,		/* added by non-INSTEAD rule */
+	QSRC_ROW_LEVEL_SECURITY,	/* added by row-level security */
 } QuerySource;
 
 /* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -1231,6 +1232,8 @@ typedef enum AlterTableType
 	AT_DropInherit,				/* NO INHERIT parent */
 	AT_AddOf,					/* OF <type_name> */
 	AT_DropOf,					/* NOT OF */
+	AT_SetRowLevelSecurity,		/* SET ROW LEVEL SECURITY (...) */
+	AT_ResetRowLevelSecurity,	/* RESET ROW LEVEL SECURITY */
 	AT_GenericOptions			/* OPTIONS (...) */
 } AlterTableType;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fb9a863..6b3ea3d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
 	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
 
 	int			nParamExec;		/* number of PARAM_EXEC Params used */
+
+	Oid			planUserId;		/* user-id this plan assumed, or InvalidOid */
 } PlannedStmt;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index af9425a..73c6ff1 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
 	Index		lastRowMarkId;	/* highest PlanRowMark ID assigned */
 
 	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
+
+	Oid			planUserId;		/* User-Id to be assumed on this plan */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h
new file mode 100644
index 0000000..9afe00a
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,31 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlevelsec.h
+ *    prototypes for optimizer/rowlevelsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLEVELSEC_H
+#define ROWLEVELSEC_H
+
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(PlannerInfo *root,
+											 Relation relation);
+extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook;
+
+typedef enum {
+	RowLevelSecModeEnabled,
+	RowLevelSecModeDisabled,
+} RowLevelSecMode;
+
+extern RowLevelSecMode getRowLevelSecurityMode(void);
+extern void setRowLevelSecurityMode(RowLevelSecMode new_mode);
+
+extern void	applyRowLevelSecurity(PlannerInfo *root);
+
+#endif	/* ROWLEVELSEC_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index e3bb35f..d13ef32 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_INDEX_PREDICATE,		/* index predicate */
 	EXPR_KIND_ALTER_COL_TRANSFORM,	/* transform expr in ALTER COLUMN TYPE */
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
-	EXPR_KIND_TRIGGER_WHEN			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_TRIGGER_WHEN,			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_ROW_LEVEL_SEC,		/* policy of ROW LEVEL SECURITY */
 } ParseExprKind;
 
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e846..5f89028 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -115,6 +115,8 @@ typedef struct CachedPlan
 								 * bare utility statements) */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	Oid			planUserId;		/* is user-id that is assumed on this cached
+								   plan, or InvalidOid if portable for anybody */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4669d8a..dce8463 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_index.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "fmgr.h"
 #include "nodes/bitmapset.h"
 #include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
 	RuleLock   *rd_rules;		/* rewrite rules */
 	MemoryContext rd_rulescxt;	/* private memory cxt for rd_rules, if any */
 	TriggerDesc *trigdesc;		/* Trigger info, or NULL if rel has none */
+	RowLevelSecDesc	*rlsdesc;	/* Row-level security info, or NULL */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out
new file mode 100644
index 0000000..5673e7a
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,753 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+(16 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-C-A)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 220 |      2 | 0101      | this document is secret, category(-C-A)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+ 240 |      2 | 0111      | this document is secret, category(-CBA)
+(24 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Hash Join  (cost=30.34..57.95 rows=2 width=117)
+   Hash Cond: (rls_regress_schema.document.did = browse.did)
+   ->  Seq Scan on document  (cost=8.27..31.15 rows=343 width=49)
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=4)
+                 Index Cond: (pguser = "current_user"())
+   ->  Hash  (cost=22.06..22.06 rows=1 width=72)
+         ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+               Filter: f_leak((browse.browse)::text)
+               ->  Seq Scan on browse  (cost=0.00..22.00 rows=4 width=168)
+                     Filter: (pguser = "current_user"())
+(12 rows)
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ERROR:  must be owner of relation document
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+ERROR:  must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Nested Loop  (cost=8.27..55.90 rows=1 width=117)
+   Join Filter: (rls_regress_schema.document.did = browse.did)
+   ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+         Filter: f_leak((browse.browse)::text)
+         ->  Seq Scan on browse  (cost=0.00..22.00 rows=4 width=168)
+               Filter: (pguser = "current_user"())
+   ->  Seq Scan on document  (cost=8.27..33.72 rows=5 width=49)
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=9)
+                 Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  10 |      0 | 0000      | this document is unclassified, category(----) |                   | 
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  30 |      0 | 0010      | this document is unclassified, category(--B-) |                   | 
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+  90 |      1 | 0000      | this document is classified, category(----)   |                   | 
+ 100 |      1 | 0001      | this document is classified, category(---A)   |                   | 
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+ 120 |      1 | 0011      | this document is classified, category(--BA)   |                   | 
+ 170 |      2 | 0000      | this document is secret, category(----)       |                   | 
+ 180 |      2 | 0001      | this document is secret, category(---A)       |                   | 
+ 190 |      2 | 0010      | this document is secret, category(--B-)       |                   | 
+ 200 |      2 | 0011      | this document is secret, category(--BA)       |                   | 
+(12 rows)
+
+DELETE FROM document WHERE did = 30;			-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(30) is still referenced from table "browse".
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(90) is still referenced from table "browse".
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+SELECT * FROM t1;
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+SELECT * FROM t1 WHERE f_leak(c);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.c)
+               ->  Seq Scan on t1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.c)
+               ->  Seq Scan on t2
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+               Filter: f_leak(c)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a |  c  
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a |  c  |   t1    
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2
+                     Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  Seq Scan on t3
+(10 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  LockRows
+                     ->  Seq Scan on t1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  LockRows
+                     ->  Seq Scan on t2
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3
+(13 rows)
+
+SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.c)
+               ->  LockRows
+                     ->  Seq Scan on t1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.c)
+               ->  LockRows
+                     ->  Seq Scan on t2
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3
+                           Filter: f_leak(c)
+(16 rows)
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+                     QUERY PLAN                     
+----------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a <= 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a <= 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(c);
+NOTICE:  f_leak => aaa
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => bcd
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => def
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+           QUERY PLAN            
+---------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: f_leak(c)
+         ->  Seq Scan on t2 t1
+               Filter: f_leak(c)
+         ->  Seq Scan on t3 t1
+               Filter: f_leak(c)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a |  c  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+           QUERY PLAN           
+--------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t2 t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t3 t1
+               Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+          QUERY PLAN           
+-------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a = 2)
+         ->  Seq Scan on t2 t1
+               Filter: (a = 2)
+         ->  Seq Scan on t3 t1
+               Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+                    QUERY PLAN                     
+---------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a = 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a = 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE:  drop cascades to 7 other objects
+DETAIL:  drop cascades to table account
+drop cascades to function f_leak(text)
+drop cascades to table document
+drop cascades to table browse
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 3f04442..ffcd8d3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
  pg_proc                 | t
  pg_range                | t
  pg_rewrite              | t
+ pg_rowlevelsec          | t
  pg_seclabel             | t
  pg_shdepend             | t
  pg_shdescription        | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(155 rows)
+(156 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ac29194..f0541c0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: privileges security_label collate
+test: privileges rowlevelsec security_label collate
 
 test: misc
 # rules cannot run concurrently with any test that creates a view
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 8576a7f..e789395 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: privileges
+test: rowlevelsec
 test: security_label
 test: collate
 test: misc
diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql
new file mode 100644
index 0000000..a4d1ccd
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,237 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+DELETE FROM document WHERE did = 30;			-- failed
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101	1	aaa
+102	2	bbb
+103	3	ccc
+104	4	ddd
+\.
+
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201	1	abc	1.1
+202	2	bcd	2.2
+203	3	cde	3.3
+204	4	def	4.4
+\.
+
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 FROM stdin WITH (oids);
+301	1	xxx	X
+302	2	yyy	Y
+303	3	zzz	Z
+\.
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
#21Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Kohei KaiGai (#20)
1 attachment(s)
Re: [v9.3] Row-Level Security

2012/9/3 Kohei KaiGai <kaigai@kaigai.gr.jp>:

2012/9/2 Dean Rasheed <dean.a.rasheed@gmail.com>:

On 17 July 2012 05:02, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

2012/7/17 Robert Haas <robertmhaas@gmail.com>:

On Sun, Jul 15, 2012 at 5:52 AM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a revised version of row-level security feature.
...
According to the Robert's comment, I revised the place to inject
applyRowLevelSecurity(). The reason why it needed to patch on
adjust_appendrel_attrs_mutator() was, we handled expansion from
regular relation to sub-query after expand_inherited_tables().
In this revision, it was moved to the head of sub-query planner.

Hi,

I had a quick look at this and spotted a problem - certain types of
query are able to bypass the RLS quals. For example:

SELECT * FROM (SELECT * FROM foo) foo;

since the RLS policy doesn't descend into subqueries, and is applied
before they are pulled up into the main query. Similarly for views on
top of tables with RLS, and SRF functions that query a table with RLS
that get inlined.

Also queries using UNION ALL are vulnerable if they end up being
flattened, for example:

SELECT * FROM foo UNION ALL SELECT * FROM foo;

Thanks for your comment.

Indeed, I missed the case of simple sub-queries and union-all being
pulled up into the main query. So, I adjusted the location to invoke
applyRowLevelSecurity() between all the pull-up stuff and expanding
inherited tables.

The attached patch is a fixed and rebased revision for CF:Sep.

Sorry! I attached incorrect revision. The attached patch is right one.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

Attachments:

pgsql-v9.3-row-level-security.ro.v3.patchapplication/octet-stream; name=pgsql-v9.3-row-level-security.ro.v3.patchDownload
 doc/src/sgml/catalogs.sgml                 |  59 +++
 doc/src/sgml/ref/alter_table.sgml          |  38 ++
 doc/src/sgml/user-manag.sgml               | 123 +++++
 src/backend/access/transam/xact.c          |  12 +
 src/backend/catalog/Makefile               |   4 +-
 src/backend/catalog/dependency.c           |  23 +
 src/backend/catalog/heap.c                 |   1 +
 src/backend/catalog/pg_rowlevelsec.c       | 241 +++++++++
 src/backend/commands/tablecmds.c           |  64 +++
 src/backend/optimizer/plan/planner.c       |  16 +
 src/backend/optimizer/util/Makefile        |   2 +-
 src/backend/optimizer/util/rowlevelsec.c   | 678 ++++++++++++++++++++++++++
 src/backend/parser/gram.y                  |  16 +
 src/backend/parser/parse_agg.c             |   6 +
 src/backend/parser/parse_expr.c            |   3 +
 src/backend/utils/adt/ri_triggers.c        |  14 +
 src/backend/utils/cache/plancache.c        |  32 ++
 src/backend/utils/cache/relcache.c         |  17 +-
 src/bin/pg_dump/pg_dump.c                  |  76 ++-
 src/bin/pg_dump/pg_dump.h                  |   1 +
 src/include/catalog/dependency.h           |   1 +
 src/include/catalog/indexing.h             |   3 +
 src/include/catalog/pg_class.h             |  20 +-
 src/include/catalog/pg_rowlevelsec.h       |  60 +++
 src/include/nodes/parsenodes.h             |   5 +-
 src/include/nodes/plannodes.h              |   2 +
 src/include/nodes/relation.h               |   2 +
 src/include/optimizer/rowlevelsec.h        |  31 ++
 src/include/parser/parse_node.h            |   3 +-
 src/include/utils/plancache.h              |   2 +
 src/include/utils/rel.h                    |   2 +
 src/test/regress/expected/rowlevelsec.out  | 753 +++++++++++++++++++++++++++++
 src/test/regress/expected/sanity_check.out |   3 +-
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/serial_schedule           |   1 +
 src/test/regress/sql/rowlevelsec.sql       | 237 +++++++++
 36 files changed, 2526 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 3bcd82c..5a5233e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
      </row>
 
      <row>
+      <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+      <entry>row-level security policy of relation</entry>
+     </row>
+
+     <row>
       <entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
       <entry>security labels on database objects</entry>
      </row>
@@ -1807,6 +1812,16 @@
      </row>
 
      <row>
+      <entry><structfield>relhasrowlevelsec</structfield></entry>
+      <entry><type>bool</type></entry>
+      <entry></entry>
+      <entry>
+       True if table has row-level security policy; see
+       <link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link> catalog
+      </entry>
+     </row>
+
+     <row>
       <entry><structfield>relhassubclass</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
@@ -4906,6 +4921,50 @@
 
  </sect1>
 
+ <sect1 id="catalog-pg-rowlevelsec">
+  <title><structname>pg_rowlevelsec</structname></title>
+
+  <indexterm zone="catalog-pg-rowlevelsec">
+   <primary>pg_rowlevelsec</primary>
+  </indexterm>
+  <para>
+   The catalog <structname>pg_rowlevelsec</structname> expression tree of
+   row-level security policy to be performed on a particular relation.
+  </para>
+  <table>
+   <title><structname>pg_rowlevelsec</structname> Columns</title>
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>rlsrelid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+      <entry>The table this row-level security is for</entry>
+     </row>
+     <row>
+      <entry><structfield>rlsqual</structfield></entry>
+      <entry><type>text</type></entry>
+      <entry></entry>
+      <entry>An expression tree to be performed as rowl-level security policy</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+  <note>
+   <para>
+    <literal>pg_class.relhasrowlevelsec</literal>
+    must be true if a table has row-level security policy in this catalog.
+   </para>
+  </note>
+ </sect1>
 
  <sect1 id="catalog-pg-seclabel">
   <title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index b764814..f3f2d91 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     NOT OF
     OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
     SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+    SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)
+    RESET ROW LEVEL SECURITY
 
 <phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
 
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
    </varlistentry>
 
    <varlistentry>
+    <term><literal>SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+    <listitem>
+     <para>
+      This form set row-level security policy of the table.
+      Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+      as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+      clause, although mechanism guarantees to evaluate this condition earlier
+      than any other user given condition.
+      See also <xref linkend="ROW-LEVEL-SECURITY">.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET ROW LEVEL SECURITY</literal></term>
+    <listitem>
+     <para>
+      This form reset row-level security policy of the table, if exists.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
@@ -806,6 +831,19 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="PARAMETER">condition</replaceable></term>
+      <listitem>
+       <para>
+        An expression that returns a value of type boolean. Expect for a case
+        when queries are executed with superuser privilege, only rows for which
+        this expression returns true will be fetched, updated or deleted.
+        This expression can reference columns of the relation being configured,
+        however, unavailable to include sub-query right now.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c283e07 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,127 @@ DROP ROLE <replaceable>name</replaceable>;
   </para>
  </sect1>
 
+ <sect1 id="row-level-security">
+  <title>Row-level Security</title>
+  <para>
+   <productname>PostgreSQL</productname> v9.3 or later provides
+   row-level security feature, like several commercial database
+   management system. It allows table owner to assign a particular
+   condition that performs as a security policy of the table; only
+   rows that satisfies the condition should be visible, except for
+   a case when superuser runs queries.
+  </para>
+  <para>
+   Row-level security policy can be set using
+   <literal>SET ROW LEVEL SECURITY</literal> command of
+   <xref linkend="SQL-ALTERTABLE"> statement, as an expression
+    form that returns a value of type boolean. This expression can
+    contain references to columns of the relation, so it enables
+    to construct arbitrary rule to make access control decision
+    based on contents of each rows.
+  </para>
+  <para>
+   For example, the following <literal>customer</literal> table
+   has <literal>uname</literal> field to store user name, and
+   it assume we don't want to expose any properties of other
+   customers.
+   The following command set <literal>current_user = uname</literal>
+   as row-level security policy on the <literal>customer</literal>
+   table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+</screen>
+   <xref linkend="SQL-EXPLAIN"> command shows how row-level
+   security policy works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+                 QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+   Filter: f_leak(customer.upasswd)
+   ->  Seq Scan on customer
+         Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+   This query execution plan means the preconfigured row-level
+   security policy is implicitly added, and scan plan on the
+   target relation being wrapped up with a sub-query.
+   It ensures user given qualifiers, including functions with
+   side effects, are never executed earlier than the row-level
+   security policy regardless of its cost, except for the cases
+   when these were fully leakproof.
+   This design helps to tackle the scenario described in
+   <xref linkend="RULES-PRIVILEGES">; that introduces the order
+   to evaluate qualifiers is significant to keep confidentiality
+   of invisible rows.
+  </para>
+
+  <para>
+   On the other hand, this design allows superusers to bypass
+   checks with row-level security.
+   It ensures <application>pg_dump</application> can obtain
+   a complete set of database backup, and avoid to execute
+   Trojan horse trap, being injected as a row-level security
+   policy of user-defined table, with privileges of superuser.
+  </para>
+
+  <para>
+   In case of queries on inherited tables, row-level security
+   policy of the parent relation is not applied to child
+   relations. Scope of the row-level security policy is limited
+   to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+                QUERY PLAN
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.y)
+               ->  Seq Scan on t1
+                     Filter: ((x % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: f_leak(y)
+         ->  Subquery Scan on t3
+               Filter: f_leak(t3.y)
+               ->  Seq Scan on t3
+                     Filter: ((x % 2) = 1)
+(12 rows)
+</screen>
+    In the above example, <literal>t1</literal> has inherited
+    child table <literal>t2</literal> and <literal>t3</literal>,
+    and row-level security policy is set on only <literal>t1</literal>,
+    and <literal>t3</literal>, not <literal>t2</literal>.
+
+    The row-level security policy of <literal>t1</literal>,
+    <literal>x</literal> must be even-number, is appended only
+    <literal>t1</literal>, neither <literal>t2</literal> nor
+    <literal>t3</literal>. On the contrary, <literal>t3</literal>
+    has different row-level security policy; <literal>x</literal>
+    must be odd-number.
+  </para>
+
+  <para>
+   Right now, row-level security feature has several limitation,
+   although these shall be improved in the future version.
+
+   Row-level security policy is not applied to
+   <command>UPDATE</command> or <command>DELETE</command>
+   commands in this revision, even though it should be
+   supported soon.
+
+   Row-level security policy is not applied to rows to be inserted
+   and newer revision of updated rows, thus, it requires to
+   define before-row-insert or before-row-update trigger to check
+   whether the row's contents satisfies the policy individually.
+
+   Although it is not a specific matter in row-level security,
+   <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH">
+   allows to switch current user identifier during execution
+   of the query. Thus, it may cause unpredicated behavior
+   if and when <literal>current_user</literal> is used as
+   a part of row-level security policy.
+  </para>
+ </sect1>
 </chapter>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e7a6606..851f9db 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
 #include "executor/spi.h"
 #include "libpq/be-fsstubs.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "pgstat.h"
 #include "replication/walsender.h"
 #include "replication/syncrep.h"
@@ -144,6 +145,7 @@ typedef struct TransactionStateData
 	int			maxChildXids;	/* allocated size of childXids[] */
 	Oid			prevUser;		/* previous CurrentUserId setting */
 	int			prevSecContext; /* previous SecurityRestrictionContext */
+	RowLevelSecMode	prevRowLevelSecMode;	/* previous RLS-mode setting */
 	bool		prevXactReadOnly;		/* entry-time xact r/o state */
 	bool		startedInRecovery;		/* did we start in recovery? */
 	struct TransactionStateData *parent;		/* back link to parent */
@@ -173,6 +175,7 @@ static TransactionStateData TopTransactionStateData = {
 	0,							/* allocated size of childXids[] */
 	InvalidOid,					/* previous CurrentUserId setting */
 	0,							/* previous SecurityRestrictionContext */
+	RowLevelSecModeEnabled,		/* previous RowLevelSecMode setting */
 	false,						/* entry-time xact r/o state */
 	false,						/* startedInRecovery */
 	NULL						/* link to parent state block */
@@ -1764,6 +1767,8 @@ StartTransaction(void)
 	/* SecurityRestrictionContext should never be set outside a transaction */
 	Assert(s->prevSecContext == 0);
 
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
+
 	/*
 	 * initialize other subsystems for new transaction
 	 */
@@ -2295,6 +2300,9 @@ AbortTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Also, mode setting of row-level security should be restored. */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * do abort processing
 	 */
@@ -4188,6 +4196,9 @@ AbortSubTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Reset row-level security mode */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * We can skip all this stuff if the subxact failed before creating a
 	 * ResourceOwner...
@@ -4327,6 +4338,7 @@ PushTransaction(void)
 	s->state = TRANS_DEFAULT;
 	s->blockState = TBLOCK_SUBBEGIN;
 	GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext);
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
 	s->prevXactReadOnly = XactReadOnly;
 
 	CurrentTransactionState = s;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index df6da1f..965aa38 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
        objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \
        pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
        pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
-       pg_type.o storage.o toasting.o
+       pg_rowlevelsec.o pg_type.o storage.o toasting.o
 
 BKIFILES = postgres.bki postgres.description postgres.shdescription
 
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
-	pg_foreign_table.h \
+	pg_foreign_table.h pg_rowlevelsec.h \
 	pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
 	toasting.h indexing.h \
     )
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 87d6f02..f0736b1 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
@@ -1229,6 +1230,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveExtensionById(object->objectId);
 			break;
 
+		case OCLASS_ROWLEVELSEC:
+			RemoveRowLevelSecurityById(object->objectId);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized object class: %u",
 				 object->classId);
@@ -2280,6 +2285,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case EventTriggerRelationId:
 			return OCLASS_EVENT_TRIGGER;
+
+		case RowLevelSecurityRelationId:
+			return OCLASS_ROWLEVELSEC;
 	}
 
 	/* shouldn't get here */
@@ -2929,6 +2937,21 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_ROWLEVELSEC:
+			{
+				char	   *relname;
+
+				relname = get_rel_name(object->objectId);
+				if (!relname)
+					elog(ERROR, "cache lookup failed for relation %u",
+						 object->objectId);
+				appendStringInfo(&buffer,
+								 _("row-level security of %s"), relname);
+
+
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index c80df41..60de76d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -776,6 +776,7 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
 	values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
 	values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+	values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec);
 	values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	if (relacl != (Datum) 0)
diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c
new file mode 100644
index 0000000..e554f20
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,241 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlevelsec.c
+ *    routines to support manipulation of the pg_rowlevelsec relation
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+void
+RelationBuildRowLevelSecurity(Relation relation)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+
+	tuple = systable_getnext(sscan);
+	if (HeapTupleIsValid(tuple))
+	{
+		RowLevelSecDesc	*rlsdesc;
+		MemoryContext	rlscxt;
+		MemoryContext	oldcxt;
+		Datum	datum;
+		bool	isnull;
+		char   *temp;
+
+		/*
+		 * Make the private memory context to store RowLevelSecDesc that
+		 * includes expression tree also.
+		 */
+		rlscxt = AllocSetContextCreate(CacheMemoryContext,
+									   RelationGetRelationName(relation),
+									   ALLOCSET_SMALL_MINSIZE,
+									   ALLOCSET_SMALL_INITSIZE,
+									   ALLOCSET_SMALL_MAXSIZE);
+		PG_TRY();
+		{
+			datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual,
+								 RelationGetDescr(rlsrel), &isnull);
+			Assert(!isnull);
+			temp = TextDatumGetCString(datum);
+
+			oldcxt = MemoryContextSwitchTo(rlscxt);
+
+			rlsdesc = palloc0(sizeof(RowLevelSecDesc));
+			rlsdesc->rlscxt = rlscxt;
+			rlsdesc->rlsqual = (Expr *) stringToNode(temp);
+			Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID);
+
+			rlsdesc->rlshassublinks
+				= contain_subplans((Node *)rlsdesc->rlsqual);
+
+			MemoryContextSwitchTo(oldcxt);
+
+			pfree(temp);
+		}
+		PG_CATCH();
+		{
+			MemoryContextDelete(rlscxt);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		relation->rlsdesc = rlsdesc;
+	}
+	else
+	{
+		relation->rlsdesc = NULL;
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, AccessShareLock);
+}
+
+void
+SetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid				relationId = RelationGetRelid(relation);
+	ParseState	   *pstate;
+	RangeTblEntry  *rte;
+	Node		   *qual;
+	Relation		rlsrel;
+	ScanKeyData		skey;
+	SysScanDesc		sscan;
+	HeapTuple		oldtup;
+	HeapTuple		newtup;
+	Datum			values[Natts_pg_rowlevelsec];
+	bool			isnull[Natts_pg_rowlevelsec];
+	bool			replaces[Natts_pg_rowlevelsec];
+	ObjectAddress	target;
+	ObjectAddress	myself;
+
+	/* Parse the supplied clause */
+	pstate = make_parsestate(NULL);
+
+	rte = addRangeTableEntryForRelation(pstate, relation,
+										NULL, false, false);
+	addRTEtoQuery(pstate, rte, false, true, true);
+
+	qual = transformWhereClause(pstate, copyObject(clause),
+								EXPR_KIND_ROW_LEVEL_SEC,
+								"ROW LEVEL SECURITY");
+	/* zero-clear */
+	memset(values,   0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(isnull,   0, sizeof(isnull));
+
+	/* Update or Indert an entry to pg_rowlevelsec catalog  */
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	oldtup = systable_getnext(sscan);
+	if (HeapTupleIsValid(oldtup))
+	{
+		replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true;
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+
+		newtup = heap_modify_tuple(oldtup,
+								   RelationGetDescr(rlsrel),
+								   values, isnull, replaces);
+		simple_heap_update(rlsrel, &newtup->t_self, newtup);
+
+		deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+								   relationId, false);
+	}
+	else
+	{
+		values[Anum_pg_rowlevelsec_rlsrelid - 1]
+			= ObjectIdGetDatum(relationId);
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+		newtup = heap_form_tuple(RelationGetDescr(rlsrel),
+								 values, isnull);
+		simple_heap_insert(rlsrel, newtup);
+	}
+	CatalogUpdateIndexes(rlsrel, newtup);
+
+	heap_freetuple(newtup);
+
+	/* records dependencies of RLS-policy and relation/columns */
+	target.classId = RelationRelationId;
+	target.objectId = relationId;
+	target.objectSubId = 0;
+
+	myself.classId = RowLevelSecurityRelationId;
+	myself.objectId = relationId;
+	myself.objectSubId = 0;
+
+	recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+	recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+						   DEPENDENCY_NORMAL);
+	free_parsestate(pstate);
+
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+void
+ResetRowLevelSecurity(Relation relation)
+{
+	if (relation->rlsdesc)
+	{
+		ObjectAddress	address;
+
+		address.classId = RowLevelSecurityRelationId;
+		address.objectId = RelationGetRelid(relation);
+		address.objectSubId = 0;
+
+		performDeletion(&address, DROP_RESTRICT, 0);
+	}
+	else
+	{
+		/* Nothing to do here */
+		elog(INFO, "relation %s has no row-level security policy, skipped",
+			 RelationGetRelationName(relation));
+	}
+}
+
+/*
+ * Guts of Row-level security policy deletion.
+ */
+void
+RemoveRowLevelSecurityById(Oid relationId)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relationId));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+	{
+		simple_heap_delete(rlsrel, &tuple->t_self);
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 359d478..83ca86b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -384,6 +385,7 @@ static void ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
 static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid);
 static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode);
 static void ATExecDropOf(Relation rel, LOCKMODE lockmode);
+static void ATExecSetRowLevelSecurity(Relation relation, Node *clause);
 static void ATExecGenericOptions(Relation rel, List *options);
 
 static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
@@ -2746,6 +2748,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_SetTableSpace:		/* must rewrite heap */
 			case AT_DropNotNull:		/* may change some SQL plans */
 			case AT_SetNotNull:
+			case AT_SetRowLevelSecurity:
+			case AT_ResetRowLevelSecurity:
 			case AT_GenericOptions:
 			case AT_AlterColumnGenericOptions:
 				cmd_lockmode = AccessExclusiveLock;
@@ -3108,6 +3112,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_DropInherit:	/* NO INHERIT */
 		case AT_AddOf:			/* OF */
 		case AT_DropOf: /* NOT OF */
+		case AT_SetRowLevelSecurity:
+		case AT_ResetRowLevelSecurity:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* These commands never recurse */
 			/* No command-specific prep needed */
@@ -3383,6 +3389,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropOf:
 			ATExecDropOf(rel, lockmode);
 			break;
+		case AT_SetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, (Node *) cmd->def);
+			break;
+		case AT_ResetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, NULL);
+			break;
 		case AT_GenericOptions:
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
@@ -7533,6 +7545,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				Assert(defaultexpr);
 				break;
 
+			case OCLASS_ROWLEVELSEC:
+				/*
+				 * A row-level security policy can depend on a column in case
+				 * when the policy clause references a particular column.
+				 * Due to same reason why TRIGGER ... WHEN does not support
+				 * to change column's type being referenced in clause, row-
+				 * level security policy also does not support it.
+				 */
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used in a row-level security policy"),
+						 errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject),
+								   colName)));
+				break;
+
 			case OCLASS_PROC:
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
@@ -9631,6 +9659,42 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode)
 }
 
 /*
+ * ALTER TABLE <name> SET ROW LEVEL SECURITY (...) OR
+ *                    RESET ROW LEVEL SECURITY
+ */
+static void
+ATExecSetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid			relid = RelationGetRelid(relation);
+	Relation	class_rel;
+	HeapTuple	tuple;
+	Form_pg_class	class_form;
+
+	if (clause != NULL)
+		SetRowLevelSecurity(relation, clause);
+	else
+		ResetRowLevelSecurity(relation);
+
+	class_rel = heap_open(RelationRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	class_form = (Form_pg_class) GETSTRUCT(tuple);
+	if (clause != NULL)
+		class_form->relhasrowlevelsec = true;
+	else
+		class_form->relhasrowlevelsec = false;
+
+	simple_heap_update(class_rel, &tuple->t_self, tuple);
+	CatalogUpdateIndexes(class_rel, tuple);
+
+	heap_close(class_rel, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
+/*
  * ALTER FOREIGN TABLE <name> OPTIONS (...)
  */
 static void
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 4284eed..7e6349c 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/analyze.h"
@@ -167,6 +168,13 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	glob->lastPHId = 0;
 	glob->lastRowMarkId = 0;
 	glob->transientPlan = false;
+	glob->planUserId = InvalidOid;
+	/*
+	 * XXX - a valid user-id shall be set on planUserId later, if constructed
+	 * plan assumes being executed under privilege of a particular user-id.
+	 * Elsewhere, keep InvalidOid; that means the constructed plan is portable
+	 * for any users.
+	 */
 
 	/* Determine what fraction of the plan is likely to be scanned */
 	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -244,6 +252,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->nParamExec = list_length(glob->paramlist);
+	result->planUserId = glob->planUserId;
 
 	return result;
 }
@@ -350,6 +359,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		flatten_simple_union_all(root);
 
 	/*
+	 * Apply row-level security policy of appeared tables, if configured.
+	 * Note that it must be processed after all the simple subqueries or
+	 * union all queries being flatten, but prior to preprocess_rowmarks().
+	 */
+	applyRowLevelSecurity(root);
+
+	/*
 	 * Detect whether any rangetable entries are RTE_JOIN kind; if not, we can
 	 * avoid the expense of doing flatten_join_alias_vars().  Also check for
 	 * outer joins --- if none, we can skip reduce_outer_joins().  And check
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3430689 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
-       relnode.o restrictinfo.o tlist.o var.o
+       relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c
new file mode 100644
index 0000000..3a01b0c
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,678 @@
+/*
+ * optimizer/util/rowlvsec.c
+ *    Row-level security support routines
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
+#include "parser/parsetree.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* flags to pull row-level security policy */
+#define RLS_FLAG_HAS_SUBLINKS			0x0001
+
+/* hook to allow extensions to apply their own security policy */
+rowlevel_security_hook_type	rowlevel_security_hook = NULL;
+
+/* current performing mode of row-level security policy */
+static RowLevelSecMode	rowlevel_security_mode = RowLevelSecModeEnabled;
+
+/*
+ * getRowLevelSecurityMode / setRowLevelSecurityMode
+ *
+ * These functions allow to get or set current performing mode of row-
+ * level security feature. It enables to disable this feature temporarily
+ * for some cases in which row-level security prevent correct behavior
+ * such as foreign-key checks to prohibit update of PKs being referenced
+ * by others.
+ * The caller must ensure the saved previous mode shall be restored, but
+ * no need to care about cases when an error would be raised.
+ */
+RowLevelSecMode
+getRowLevelSecurityMode(void)
+{
+	return rowlevel_security_mode;
+}
+
+void
+setRowLevelSecurityMode(RowLevelSecMode new_mode)
+{
+	rowlevel_security_mode = new_mode;
+}
+
+/*
+ * pull_rowlevel_security_policy
+ *
+ * This routine tries to pull expression node of row-level security policy
+ * on the target relation, and its children if configured.
+ * If one or more relation has a security policy at least, this function
+ * returns true, or false elsewhere.
+ */
+static bool
+pull_rowlevel_security_policy(PlannerInfo *root,
+							  RangeTblEntry *rte,
+							  Index rtindex,
+							  List **rls_relids,
+							  List **rls_quals,
+							  List **rls_flags)
+{
+	Relation		rel;
+	LOCKMODE		lockmode;
+	List		   *relid_list = NIL;
+	List		   *qual_list = NIL;
+	List		   *flag_list = NIL;
+	ListCell	   *cell;
+	bool			result = false;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+	if (!rte->inh)
+	{
+		lockmode = NoLock;
+		relid_list = list_make1_oid(rte->relid);
+	}
+	else
+	{
+		/*
+		 * In case when the target relation may have inheritances,
+		 * we need to suggest an appropriate lock mode because it
+		 * is the first time to reference these tables in a series
+		 * of processes. For more details, see the comments in
+		 * expand_inherited_tables.
+		 * Also note that it does not guarantee the locks on child
+		 * tables being already acquired at expand_inherited_tables,
+		 * because row-level security routines can be bypassed if
+		 * RowLevelSecModeDisabled.
+		 */
+		if (rtindex == root->parse->resultRelation)
+			lockmode = RowExclusiveLock;
+		else
+		{
+			foreach (cell, root->parse->rowMarks)
+			{
+				Assert(IsA(lfirst(cell), RowMarkClause));
+				if (((RowMarkClause *) lfirst(cell))->rti == rtindex)
+					break;
+			}
+			if (cell)
+				lockmode = RowShareLock;
+			else
+				lockmode = AccessShareLock;
+		}
+		relid_list = find_all_inheritors(rte->relid, lockmode, NULL);
+	}
+
+	/*
+	 * Try to fetch row-level security policy of the target relation
+	 * or its children.
+	 */
+	foreach (cell, relid_list)
+	{
+		Expr   *qual = NULL;
+		int		flags = 0;
+
+		rel = heap_open(lfirst_oid(cell), lockmode);
+
+		/*
+		 * Pull out row-level security policy configured with built-in
+		 * features, if unprivileged users. Please note that superuser
+		 * can bypass it.
+		 */
+		if (rel->rlsdesc && !superuser())
+		{
+			RowLevelSecDesc	*rlsdesc = rel->rlsdesc;
+
+			qual = copyObject(rlsdesc->rlsqual);
+			if (rlsdesc->rlshassublinks)
+				flags |= RLS_FLAG_HAS_SUBLINKS;
+		}
+
+		/*
+		 * Also, ask extensions whether they want to apply their own
+		 * row-level security policy. If both built-in and extension
+		 * has their own policy, it shall be merged.
+		 */
+		if (rowlevel_security_hook)
+		{
+			List   *qual_list;
+
+			qual_list = (*rowlevel_security_hook)(root, rel);
+			if (qual_list != NIL)
+			{
+				if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+					contain_subplans((Node *)qual_list))
+					flags |= RLS_FLAG_HAS_SUBLINKS;
+
+				if (qual != NULL)
+					qual_list = lappend(qual_list, qual);
+
+				if (list_length(qual_list) == 1)
+					qual = (Expr *)list_head(qual_list);
+				else
+					qual = makeBoolExpr(AND_EXPR, qual_list, -1);
+			}
+		}
+
+		qual_list = lappend(qual_list, qual);
+		if (qual)
+			result = true;
+		flag_list = lappend_int(flag_list, flags);
+
+		heap_close(rel, NoLock);  /* close the relation, but keep locks */
+	}
+
+	/*
+	 * Inform the caller list of relation Oid, qualifier of row-level
+	 * security policy and its flag, if one or more target relations
+	 * have its row-level security policy. Elsewhere, release it.
+	 */
+	if (result)
+	{
+		*rls_relids = relid_list;
+		*rls_quals = qual_list;
+		*rls_flags = flag_list;
+	}
+	else
+	{
+		list_free(relid_list);
+		list_free(qual_list);
+		list_free(flag_list);
+	}
+	return result;
+}
+
+/*
+ * fixup_varattnos
+ *
+ * It fixes up varattno of Var node that referenced the relation with
+ * RLS policy, thus replaced to a sub-query. Here is no guarantee the
+ * varattno matches with TargetEntry's resno of the sub-query, so needs
+ * to adjust them.
+ */
+typedef struct {
+	PlannerInfo *root;
+	int		varlevelsup;
+} fixup_var_context;
+
+static bool
+fixup_var_references_walker(Node *node, fixup_var_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var            *var = (Var *)node;
+		RangeTblEntry  *rte;
+
+		/*
+		 * Does this Var node reference the Query node currently we focused
+		 * on. If not, we simply ignore it.
+		 */
+		if (var->varlevelsup != context->varlevelsup)
+			return false;
+
+		rte = rt_fetch(var->varno, context->root->parse->rtable);
+		if (!rte)
+			elog(ERROR, "invalid varno %d", var->varno);
+
+		if (rte->rtekind == RTE_SUBQUERY &&
+			rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+		{
+			List       *targetList = rte->subquery->targetList;
+			ListCell   *cell;
+
+			foreach (cell, targetList)
+			{
+				TargetEntry    *subtle = lfirst(cell);
+
+				if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+					 var->varattno == InvalidAttrNumber) ||
+					(IsA(subtle->expr, Var) &&
+					 var->varattno == ((Var *)(subtle->expr))->varattno))
+				{
+					var->varattno = subtle->resno;
+					return false;
+				}
+			}
+			elog(ERROR, "invalid varattno %d", var->varattno);
+		}
+		return false;
+	}
+	else if (IsA(node, Query))
+	{
+		bool    result;
+
+		context->varlevelsup++;
+		result = query_tree_walker((Query *) node,
+								   fixup_var_references_walker,
+								   (void *) context, 0);
+		context->varlevelsup--;
+
+		return result;
+	}
+	return expression_tree_walker(node,
+								  fixup_var_references_walker,
+								  (void *) context);
+}
+
+static void
+fixup_varattnos(PlannerInfo *root)
+{
+	fixup_var_context   context;
+
+	/*
+	 * Fixup Var->varattno that references the sub-queries originated from
+	 * regular relations with RLS policy.
+	 */
+	context.root = root;
+	context.varlevelsup = 0;
+
+	query_tree_walker(root->parse,
+					  fixup_var_references_walker,
+					  (void *) &context, 0);
+}
+
+/*
+ * make_pseudo_column
+ *
+ * make a TargetEntry object which references a particular column of
+ * the underlying table.
+ */
+static TargetEntry *
+make_pseudo_column(Oid relid_head, Oid relid, AttrNumber attnum)
+{
+	Form_pg_attribute attform;
+	HeapTuple	tuple;
+	Var		   *var;
+	char	   *resname;
+
+	if (attnum == InvalidAttrNumber)
+	{
+		ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);
+
+		r->arg = (Expr *) makeVar((Index) 1,
+								  InvalidAttrNumber,
+								  get_rel_type_id(relid),
+								  -1,
+								  InvalidOid,
+								  0);
+		r->resulttype = get_rel_type_id(relid_head);
+		r->convertformat = COERCE_IMPLICIT_CAST;
+		r->location = -1;
+
+		return makeTargetEntry((Expr *) r, -1, get_rel_name(relid), false);
+	}
+
+	tuple = SearchSysCache2(ATTNUM,
+							ObjectIdGetDatum(relid),
+							Int16GetDatum(attnum));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+			 attnum, relid);
+	attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+	var = makeVar((Index) 1,
+				  attform->attnum,
+				  attform->atttypid,
+				  attform->atttypmod,
+				  InvalidOid,
+				  0);
+	resname = pstrdup(NameStr(attform->attname));
+
+	ReleaseSysCache(tuple);
+
+	return makeTargetEntry((Expr *)var, -1, resname, false);
+}
+
+/*
+ * make_pseudo_subquery
+ *
+ * This routine makes a sub-query that references the target relation
+ * with given row-level security policy. This sub-query shall have
+ * security_barrier attribute to prevent unexpected push-down.
+ */
+static Query *
+make_pseudo_subquery(Oid relid_head,
+					 Oid relid,
+					 Node *qual,
+					 int flags,
+					 List *targetList,
+					 RangeTblEntry *rte,
+					 RowMarkClause *rclause)
+{
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+	List		   *colnameList = NIL;
+	ListCell	   *cell;
+
+	subqry = makeNode(Query);
+	subqry->commandType = CMD_SELECT;
+	subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	subrte = makeNode(RangeTblEntry);
+	subrte->rtekind = RTE_RELATION;
+	subrte->relid = relid;
+	subrte->relkind = get_rel_relkind(relid);
+	subrte->inFromCl = true;
+	subrte->requiredPerms = rte->requiredPerms;
+	subrte->selectedCols = NULL;
+	subrte->modifiedCols = NULL;
+
+	foreach (cell, targetList)
+	{
+		TargetEntry	   *oldtle = lfirst(cell);
+		TargetEntry	   *subtle;
+		AttrNumber		attnum;
+
+		if (IsA(oldtle->expr, ConvertRowtypeExpr))
+			attnum = InvalidAttrNumber;
+		else
+		{
+			Assert(IsA(oldtle->expr, Var));
+
+			attnum = get_attnum(relid, oldtle->resname);
+			if (attnum == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_COLUMN),
+					errmsg("column \"%s\" of relation \"%s\" does not exist",
+								oldtle->resname, get_rel_name(relid))));
+		}
+		subtle = make_pseudo_column(relid_head, relid, attnum);
+		subtle->resno = oldtle->resno;
+		subqry->targetList = lappend(subqry->targetList, subtle);
+
+		colnameList = lappend(colnameList,
+							  makeString(pstrdup(subtle->resname)));
+		attnum -= FirstLowInvalidHeapAttributeNumber;
+		subrte->selectedCols = bms_add_member(rte->selectedCols, attnum);
+	}
+	subrte->eref = makeAlias(get_rel_name(relid), colnameList);
+
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		subqry->hasSubLinks = true;
+
+	subqry->rtable = list_make1(subrte);
+
+	subrtr = makeNode(RangeTblRef);
+	subrtr->rtindex = 1;
+	subqry->jointree = makeFromExpr(list_make1(subrtr), qual);
+
+	if (rclause)
+	{
+		RowMarkClause  *rclause_sub;
+
+		rclause_sub = copyObject(rclause);
+		rclause_sub->rti = 1;
+
+		subqry->rowMarks = list_make1(rclause);
+		subqry->hasForUpdate = true;
+	}
+	return subqry;
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * This routine expand reference to the given RangeTblEntry by a sub-query
+ * which simply references the target relation with the qualifier of row-
+ * level security policy.
+ */
+static void
+expand_rtentry_with_policy(PlannerInfo *root,
+						   RangeTblEntry *rte,
+						   Index rtindex,
+						   List *rls_relids,
+						   List *rls_quals,
+						   List *rls_flags)
+{
+	Query		   *parse = root->parse;
+	Oid				relid_head;
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	List		   *targetList;
+	List		   *colnameList;
+	Bitmapset	   *attr_used;
+	AttrNumber		attnum;
+	RowMarkClause  *rclause;
+	ListCell	   *cell1;
+	ListCell	   *cell2;
+	ListCell	   *cell3;
+	ListCell	   *lc;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+	Assert(list_length(rls_relids) == list_length(rls_quals));
+	Assert(list_length(rls_relids) == list_length(rls_flags));
+	Assert(list_length(rls_relids) > 0);
+
+	/*
+	 * Construct a target-entry list
+	 */
+	targetList = NIL;
+	colnameList = NIL;
+	relid_head = linitial_oid(rls_relids);
+	attr_used = bms_union(rte->selectedCols,
+						  rte->modifiedCols);
+	while ((attnum = bms_first_member(attr_used)) >= 0)
+	{
+		attnum += FirstLowInvalidHeapAttributeNumber;
+
+		subtle = make_pseudo_column(relid_head, relid_head, attnum);
+		subtle->resno = list_length(targetList) + 1;
+
+		targetList = lappend(targetList, subtle);
+		colnameList = lappend(colnameList,
+							  makeString(pstrdup(subtle->resname)));
+	}
+	bms_free(attr_used);
+
+	/*
+	 * Push-down row-level lock of the target relation, since sub-query
+	 * does not support FOR SHARE/FOR UPDATE locks being assigned.
+	 */
+	rclause = NULL;
+	foreach (lc, parse->rowMarks)
+	{
+		if (((RowMarkClause *) lfirst(lc))->rti == rtindex)
+		{
+			rclause = lfirst(lc);
+			parse->rowMarks = list_delete(parse->rowMarks, rclause);
+			break;
+		}
+	}
+
+	/*
+	 * Construct sub-query structures
+	 */
+	forthree (cell1, rls_relids, cell2, rls_quals, cell3, rls_flags)
+	{
+		Oid			relid = lfirst_oid(cell1);
+		Node	   *qual = lfirst(cell2);
+		int			flags = lfirst_int(cell3);
+
+		subqry = make_pseudo_subquery(relid_head, relid, qual, flags,
+									  targetList, rte, rclause);
+		if (cell1 == list_head(rls_relids))
+		{
+			Assert(rte->relid == relid);
+			Assert(rte->inh == true || list_length(rls_relids) == 1);
+
+			rte->relid = InvalidOid;
+			rte->rtekind = RTE_SUBQUERY;
+			rte->subquery = subqry;
+			rte->security_barrier = true;
+
+			/* no permission checks are needed to subquery itself */
+			rte->requiredPerms = 0;
+			rte->checkAsUser = InvalidOid;
+			rte->selectedCols = NULL;
+			rte->modifiedCols = NULL;
+
+			rte->alias = NULL;
+			rte->eref = makeAlias(get_rel_name(relid),
+								  copyObject(colnameList));
+			if (list_length(rls_relids) == 1)
+				rte->inh = false;
+		}
+
+		/*
+		 * RTE's for child relations and AppendRelInfo in case when
+		 * the target relation has its children.
+		 */
+		if (list_length(rls_relids) > 1)
+		{
+			AppendRelInfo  *apinfo;
+			AttrNumber		child_rtindex;
+			ListCell	   *l;
+
+			/*
+			 * XXX - Set up RangeTblEntry for the child relation.
+			 * Note that it does not need to have security_barrier
+			 * attribute, if no row-level security policy is
+			 * configured on.
+			 */
+			subrte = makeNode(RangeTblEntry);
+			subrte->rtekind = RTE_SUBQUERY;
+			subrte->subquery = subqry;
+			if (qual)
+				subrte->security_barrier = true;
+			subrte->alias = NULL;
+			subrte->eref = makeAlias(get_rel_name(relid),
+									 copyObject(colnameList));
+			parse->rtable = lappend(parse->rtable, subrte);
+			child_rtindex = list_length(parse->rtable);
+
+			/*
+			 * reference to inherited children performs as if simple
+			 * UNION ALL operation, so add AppendRelInfo here.
+			 */
+			apinfo = makeNode(AppendRelInfo);
+			apinfo->parent_relid = rtindex;
+			apinfo->child_relid = child_rtindex;
+			foreach (l, targetList)
+			{
+				Var	   *trans_var
+					= makeVarFromTargetEntry(child_rtindex, lfirst(l));
+				apinfo->translated_vars
+					= lappend(apinfo->translated_vars, trans_var);
+			}
+			root->append_rel_list = lappend(root->append_rel_list, apinfo);
+		}
+	}
+}
+
+/*
+ * applyRowLevelSecurity
+ *
+ * It tries to apply row-level security policy of the relation.
+ * If and when a particular policy is configured on the referenced
+ * relation, it shall be replaced by a sub-query with security-barrier flag;
+ * that references the relation with row-level security policy.
+ * In the result, all users can see is rows of the relation that satisfies
+ * the condition supplied as security policy.
+ */
+void
+applyRowLevelSecurity(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	ListCell   *cell;
+	Index		rtindex;
+	bool		has_rowlevel_sec = false;
+
+	/* mode checks */
+	if (rowlevel_security_mode == RowLevelSecModeDisabled)
+		return;
+
+	/*
+	 * No need to apply row-level security on sub-query being originated
+	 * from regular relation with RLS policy any more.
+	 */
+	if (parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+		return;
+
+	rtindex = 0;
+	foreach (cell, parse->rtable)
+	{
+		RangeTblEntry  *rte = lfirst(cell);
+		List		   *rls_relids;
+		List		   *rls_quals;
+		List		   *rls_flags;
+
+		rtindex++;
+
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * XXX - In this revision, we have no support for UPDATE / DELETE
+		 * statement, so simply skip it.
+		 */
+		if (rtindex == parse->resultRelation)
+			continue;
+
+		/*
+		 * In case when a row-level security policy was configured on
+		 * the table referenced by this RangeTblEntry or its children,
+		 * it shall be rewritten to sub-query with this policy.
+		 */
+		if (pull_rowlevel_security_policy(root, rte, rtindex,
+										  &rls_relids, &rls_quals, &rls_flags))
+		{
+			expand_rtentry_with_policy(root, rte, rtindex,
+									   rls_relids, rls_quals, rls_flags);
+			has_rowlevel_sec = true;
+		}
+	}
+
+	/*
+	 * Post case handling if one or more relation was replaced to sub-query.
+	 */
+	if (has_rowlevel_sec)
+	{
+		PlanInvalItem  *pi_item;
+
+		/*
+		 * plan should be invalidated if and when userid was changed
+		 * on the executor stage, from planner stage.
+		 */
+		Assert(!OidIsValid(root->glob->planUserId) ||
+			   root->glob->planUserId == GetUserId());
+		root->glob->planUserId = GetUserId();
+
+		pi_item = makeNode(PlanInvalItem);
+		pi_item->cacheId = AUTHOID;
+		pi_item->hashValue
+			= GetSysCacheHashValue1(AUTHOID,
+									ObjectIdGetDatum(root->glob->planUserId));
+		root->glob->invalItems = lappend(root->glob->invalItems, pi_item);
+
+		/*
+		 * Since the relation with RLS policy was replaced by a sub-query,
+		 * thus resource number to reference a particular column can be
+		 * also moditifed. If we applied RLS policy on one or more relations,
+		 * varattno of Var node that has referenced the rewritten relation
+		 * needs to be fixed up.
+		 */
+		fixup_varattnos(root);
+	}
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 5894cb0..0a86ce8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2068,6 +2068,22 @@ alter_table_cmd:
 					n->def = (Node *)$2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> SET ROW LEVEL SECURITY (expression) */
+			| SET ROW LEVEL SECURITY '(' a_expr ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_SetRowLevelSecurity;
+					n->def = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> RESET ROW LEVEL SECURITY */
+			| RESET ROW LEVEL SECURITY
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_ResetRowLevelSecurity;
+					n->def = NULL;
+					$$ = (Node *)n;
+				}
 			| alter_generic_options
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index d1d835b..87423d8 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("aggregate functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("aggregate functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("window functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("window functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e9267c5..94c25d1 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1443,6 +1443,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
+		case EXPR_KIND_ROW_LEVEL_SEC:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2609,6 +2610,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "EXECUTE";
 		case EXPR_KIND_TRIGGER_WHEN:
 			return "WHEN";
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			return "ROW LEVEL SECURITY";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 97e68b1..19a2255 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -43,6 +43,7 @@
 #include "parser/parse_coerce.h"
 #include "parser/parse_relation.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
@@ -2999,6 +3000,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
+	RowLevelSecMode	save_rls_mode;
 	Datum		vals[RI_MAX_NUMKEYS * 2];
 	char		nulls[RI_MAX_NUMKEYS * 2];
 
@@ -3081,6 +3083,15 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
 						   save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
 
+	/*
+	 * Disabled row-level security in case when foreign-key relation is
+	 * queried to check existence of tupls that references the tuple to
+	 * be modified on the primary-key side.
+	 */
+	save_rls_mode = getRowLevelSecurityMode();
+	if (source_is_pk)
+		setRowLevelSecurityMode(RowLevelSecModeDisabled);
+
 	/* Finally we can run the query. */
 	spi_result = SPI_execute_snapshot(qplan,
 									  vals, nulls,
@@ -3090,6 +3101,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	/* Restore UID and security context */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
 
+	/* Restore row-level security performing mode */
+	setRowLevelSecurityMode(save_rls_mode);
+
 	/* Check result */
 	if (spi_result < 0)
 		elog(ERROR, "SPI_execute_snapshot returned %d", spi_result);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 8c0391f..36a8750 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -51,6 +51,7 @@
 #include "catalog/namespace.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
@@ -665,6 +666,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
 		AcquireExecutorLocks(plan->stmt_list, true);
 
 		/*
+		 * If plan was constructed with assumption of a particular user-id,
+		 * and it is different from the current one, the cached-plan shall
+		 * be invalidated to construct suitable query plan.
+		 */
+		if (plan->is_valid &&
+			OidIsValid(plan->planUserId) &&
+			plan->planUserId == GetUserId())
+			plan->is_valid = false;
+
+		/*
 		 * If plan was transient, check to see if TransactionXmin has
 		 * advanced, and if so invalidate it.
 		 */
@@ -716,6 +727,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 {
 	CachedPlan *plan;
 	List	   *plist;
+	ListCell   *cell;
+	Oid			planUserId = InvalidOid;
 	bool		snapshot_set;
 	bool		spi_pushed;
 	MemoryContext plan_context;
@@ -794,6 +807,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	PopOverrideSearchPath();
 
 	/*
+	 * Check whether the generated plan assumes a particular user-id, or not.
+	 * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+	 * it should be kept and used to validation check of the cached plan
+	 * under the "current" user-id.
+	 */
+	foreach (cell, plist)
+	{
+		PlannedStmt	*pstmt = lfirst(cell);
+
+		if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+		{
+			Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+			planUserId = pstmt->planUserId;
+		}
+	}
+
+	/*
 	 * Make a dedicated memory context for the CachedPlan and its subsidiary
 	 * data.  It's probably not going to be large, but just in case, use the
 	 * default maxsize parameter.  It's transient for the moment.
@@ -828,6 +859,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->context = plan_context;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->planUserId = planUserId;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0cdd30d..f37586e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -896,6 +897,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	else
 		relation->trigdesc = NULL;
 
+	if (relation->rd_rel->relhasrowlevelsec)
+		RelationBuildRowLevelSecurity(relation);
+	else
+		relation->rlsdesc = NULL;
+
 	/*
 	 * if it's an index, initialize index-related information
 	 */
@@ -1785,6 +1791,8 @@ RelationDestroyRelation(Relation relation)
 		MemoryContextDelete(relation->rd_indexcxt);
 	if (relation->rd_rulescxt)
 		MemoryContextDelete(relation->rd_rulescxt);
+	if (relation->rlsdesc)
+		MemoryContextDelete(relation->rlsdesc->rlscxt);
 	pfree(relation);
 }
 
@@ -3024,7 +3032,13 @@ RelationCacheInitializePhase3(void)
 				relation->rd_rel->relhastriggers = false;
 			restart = true;
 		}
-
+		if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL)
+		{
+			RelationBuildRowLevelSecurity(relation);
+			if (relation->rlsdesc == NULL)
+				relation->rd_rel->relhasrowlevelsec = false;
+			restart = true;
+		}
 		/* Release hold on the relation */
 		RelationDecrementReferenceCount(relation);
 
@@ -4174,6 +4188,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_rules = NULL;
 		rel->rd_rulescxt = NULL;
 		rel->trigdesc = NULL;
+		rel->rlsdesc = NULL;
 		rel->rd_indexprs = NIL;
 		rel->rd_indpred = NIL;
 		rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index cdbed20..e781dae 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3867,6 +3867,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reloptions;
 	int			i_toastreloptions;
 	int			i_reloftype;
+	int			i_rlsqual;
 
 	/* Make sure we are in proper schema */
 	selectSourceSchema(fout, "pg_catalog");
@@ -3891,7 +3892,45 @@ getTables(Archive *fout, int *numTables)
 	 * we cannot correctly identify inherited columns, owned sequences, etc.
 	 */
 
-	if (fout->remoteVersion >= 90100)
+	if (fout->remoteVersion >= 90300)
+	{
+		/*
+		 * Left join to pick up dependency info linking sequences to their
+		 * owning column, if any (note this dependency is AUTO as of 8.2)
+		 */
+		appendPQExpBuffer(query,
+						  "SELECT c.tableoid, c.oid, c.relname, "
+						  "c.relacl, c.relkind, c.relnamespace, "
+						  "(%s c.relowner) AS rolname, "
+						  "c.relchecks, c.relhastriggers, "
+						  "c.relhasindex, c.relhasrules, c.relhasoids, "
+						  "c.relfrozenxid, tc.oid AS toid, "
+						  "tc.relfrozenxid AS tfrozenxid, "
+						  "c.relpersistence, "
+						  "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+						  "d.refobjid AS owning_tab, "
+						  "d.refobjsubid AS owning_col, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+						"array_to_string(c.reloptions, ', ') AS reloptions, "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual "
+						  "FROM pg_class c "
+						  "LEFT JOIN pg_depend d ON "
+						  "(c.relkind = '%c' AND "
+						  "d.classid = c.tableoid AND d.objid = c.oid AND "
+						  "d.objsubid = 0 AND "
+						  "d.refclassid = c.tableoid AND d.deptype = 'a') "
+					   "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+					   "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) "
+						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+						  "ORDER BY c.oid",
+						  username_subquery,
+						  RELKIND_SEQUENCE,
+						  RELKIND_RELATION, RELKIND_SEQUENCE,
+						  RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+						  RELKIND_FOREIGN_TABLE);
+	}
+	else if (fout->remoteVersion >= 90100)
 	{
 		/*
 		 * Left join to pick up dependency info linking sequences to their
@@ -3911,7 +3950,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL as rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3947,7 +3987,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3982,7 +4023,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4017,7 +4059,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4053,7 +4096,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4088,7 +4132,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4119,7 +4164,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4145,7 +4191,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4181,7 +4228,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "WHERE relkind IN ('%c', '%c') "
 						  "ORDER BY oid",
@@ -4229,6 +4277,7 @@ getTables(Archive *fout, int *numTables)
 	i_reloptions = PQfnumber(res, "reloptions");
 	i_toastreloptions = PQfnumber(res, "toast_reloptions");
 	i_reloftype = PQfnumber(res, "reloftype");
+	i_rlsqual = PQfnumber(res, "rlsqual");
 
 	if (lockWaitTimeout && fout->remoteVersion >= 70300)
 	{
@@ -4271,6 +4320,10 @@ getTables(Archive *fout, int *numTables)
 			tblinfo[i].reloftype = NULL;
 		else
 			tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype));
+		if (PQgetisnull(res, i, i_rlsqual))
+			tblinfo[i].rlsqual = NULL;
+		else
+			tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual));
 		tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
 		if (PQgetisnull(res, i, i_owning_tab))
 		{
@@ -12803,6 +12856,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 			}
 		}
 	}
+	if (tbinfo->rlsqual)
+		appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n",
+						  fmtId(tbinfo->dobj.name), tbinfo->rlsqual);
 
 	if (binary_upgrade)
 		binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2aa2060..5485101 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -256,6 +256,7 @@ typedef struct _tableInfo
 	uint32		toast_frozenxid;	/* for restore toast frozen xid */
 	int			ncheck;			/* # of CHECK expressions */
 	char	   *reloftype;		/* underlying type for typed table */
+	char	   *rlsqual;		/* row-level security policy */
 	/* these two are set only if table is a sequence owned by a column: */
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8499768..49baa0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
 	OCLASS_EVENT_TRIGGER,		/* pg_event_trigger */
+	OCLASS_ROWLEVELSEC,			/* pg_rowlevelsec */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 238fe58..a3b07e2 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
 DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
 #define RangeTypidIndexId					3542
 
+DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId				3839
+
 /* last step of initialization script: build the indexes declared above */
 BUILD_INDICES
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f83ce80..5b6e38b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
 	bool		relhaspkey;		/* has (or has had) PRIMARY KEY index */
 	bool		relhasrules;	/* has (or has had) any rules */
 	bool		relhastriggers; /* has (or has had) any TRIGGERs */
+	bool		relhasrowlevelsec;	/* has (or has had) row-level security */
 	bool		relhassubclass; /* has (or has had) derived classes */
 	TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
 
@@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class;
  * ----------------
  */
 
-#define Natts_pg_class					27
+#define Natts_pg_class					28
 #define Anum_pg_class_relname			1
 #define Anum_pg_class_relnamespace		2
 #define Anum_pg_class_reltype			3
@@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class;
 #define Anum_pg_class_relhaspkey		21
 #define Anum_pg_class_relhasrules		22
 #define Anum_pg_class_relhastriggers	23
-#define Anum_pg_class_relhassubclass	24
-#define Anum_pg_class_relfrozenxid		25
-#define Anum_pg_class_relacl			26
-#define Anum_pg_class_reloptions		27
+#define Anum_pg_class_relhasrowlevelsec	24
+#define Anum_pg_class_relhassubclass	25
+#define Anum_pg_class_relfrozenxid		26
+#define Anum_pg_class_relacl			27
+#define Anum_pg_class_reloptions		28
 
 /* ----------------
  *		initial contents of pg_class
@@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class;
  */
 
 /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
-DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
 
 
diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h
new file mode 100644
index 0000000..5a64d1b
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,60 @@
+/*
+ * pg_rowlevelsec.h
+ *   definition of the system catalog for row-level security policy
+ *   (pg_rowlevelsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWLEVELSEC_H
+#define PG_ROWLEVELSEC_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ *		pg_rowlevelsec definition. cpp turns this into
+ *		typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId	3838
+
+CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS
+{
+	Oid				rlsrelid;
+#ifdef CATALOG_VARLEN
+	pg_node_tree	rlsqual;
+#endif
+} FormData_pg_rowlevelsec;
+
+/* ----------------
+ *		Form_pg_rowlevelsec corresponds to a pointer to a row with
+ *		the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec;
+
+/* ----------------
+ * 		compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowlevelsec				2
+#define Anum_pg_rowlevelsec_rlsrelid		1
+#define Anum_pg_rowlevelsec_rlsqual			2
+
+typedef struct
+{
+	MemoryContext	rlscxt;
+	Expr		   *rlsqual;
+	bool			rlshassublinks;
+} RowLevelSecDesc;
+
+extern void	RelationBuildRowLevelSecurity(Relation relation);
+extern void	SetRowLevelSecurity(Relation relation, Node *clause);
+extern void	ResetRowLevelSecurity(Relation relation);
+extern void	RemoveRowLevelSecurityById(Oid relationId);
+
+#endif  /* PG_ROWLEVELSEC_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 19178b5..2f9de9f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
 	QSRC_PARSER,				/* added by parse analysis (now unused) */
 	QSRC_INSTEAD_RULE,			/* added by unconditional INSTEAD rule */
 	QSRC_QUAL_INSTEAD_RULE,		/* added by conditional INSTEAD rule */
-	QSRC_NON_INSTEAD_RULE		/* added by non-INSTEAD rule */
+	QSRC_NON_INSTEAD_RULE,		/* added by non-INSTEAD rule */
+	QSRC_ROW_LEVEL_SECURITY,	/* added by row-level security */
 } QuerySource;
 
 /* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -1231,6 +1232,8 @@ typedef enum AlterTableType
 	AT_DropInherit,				/* NO INHERIT parent */
 	AT_AddOf,					/* OF <type_name> */
 	AT_DropOf,					/* NOT OF */
+	AT_SetRowLevelSecurity,		/* SET ROW LEVEL SECURITY (...) */
+	AT_ResetRowLevelSecurity,	/* RESET ROW LEVEL SECURITY */
 	AT_GenericOptions			/* OPTIONS (...) */
 } AlterTableType;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fb9a863..6b3ea3d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
 	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
 
 	int			nParamExec;		/* number of PARAM_EXEC Params used */
+
+	Oid			planUserId;		/* user-id this plan assumed, or InvalidOid */
 } PlannedStmt;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index af9425a..73c6ff1 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
 	Index		lastRowMarkId;	/* highest PlanRowMark ID assigned */
 
 	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
+
+	Oid			planUserId;		/* User-Id to be assumed on this plan */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h
new file mode 100644
index 0000000..9afe00a
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,31 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlevelsec.h
+ *    prototypes for optimizer/rowlevelsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLEVELSEC_H
+#define ROWLEVELSEC_H
+
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(PlannerInfo *root,
+											 Relation relation);
+extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook;
+
+typedef enum {
+	RowLevelSecModeEnabled,
+	RowLevelSecModeDisabled,
+} RowLevelSecMode;
+
+extern RowLevelSecMode getRowLevelSecurityMode(void);
+extern void setRowLevelSecurityMode(RowLevelSecMode new_mode);
+
+extern void	applyRowLevelSecurity(PlannerInfo *root);
+
+#endif	/* ROWLEVELSEC_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index e3bb35f..d13ef32 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_INDEX_PREDICATE,		/* index predicate */
 	EXPR_KIND_ALTER_COL_TRANSFORM,	/* transform expr in ALTER COLUMN TYPE */
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
-	EXPR_KIND_TRIGGER_WHEN			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_TRIGGER_WHEN,			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_ROW_LEVEL_SEC,		/* policy of ROW LEVEL SECURITY */
 } ParseExprKind;
 
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e846..5f89028 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -115,6 +115,8 @@ typedef struct CachedPlan
 								 * bare utility statements) */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	Oid			planUserId;		/* is user-id that is assumed on this cached
+								   plan, or InvalidOid if portable for anybody */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4669d8a..dce8463 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_index.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "fmgr.h"
 #include "nodes/bitmapset.h"
 #include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
 	RuleLock   *rd_rules;		/* rewrite rules */
 	MemoryContext rd_rulescxt;	/* private memory cxt for rd_rules, if any */
 	TriggerDesc *trigdesc;		/* Trigger info, or NULL if rel has none */
+	RowLevelSecDesc	*rlsdesc;	/* Row-level security info, or NULL */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out
new file mode 100644
index 0000000..5673e7a
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,753 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+(16 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-C-A)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 220 |      2 | 0101      | this document is secret, category(-C-A)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+ 240 |      2 | 0111      | this document is secret, category(-CBA)
+(24 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Hash Join  (cost=30.34..57.95 rows=2 width=117)
+   Hash Cond: (rls_regress_schema.document.did = browse.did)
+   ->  Seq Scan on document  (cost=8.27..31.15 rows=343 width=49)
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=4)
+                 Index Cond: (pguser = "current_user"())
+   ->  Hash  (cost=22.06..22.06 rows=1 width=72)
+         ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+               Filter: f_leak((browse.browse)::text)
+               ->  Seq Scan on browse  (cost=0.00..22.00 rows=4 width=168)
+                     Filter: (pguser = "current_user"())
+(12 rows)
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ERROR:  must be owner of relation document
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+ERROR:  must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Nested Loop  (cost=8.27..55.90 rows=1 width=117)
+   Join Filter: (rls_regress_schema.document.did = browse.did)
+   ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+         Filter: f_leak((browse.browse)::text)
+         ->  Seq Scan on browse  (cost=0.00..22.00 rows=4 width=168)
+               Filter: (pguser = "current_user"())
+   ->  Seq Scan on document  (cost=8.27..33.72 rows=5 width=49)
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=9)
+                 Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  10 |      0 | 0000      | this document is unclassified, category(----) |                   | 
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  30 |      0 | 0010      | this document is unclassified, category(--B-) |                   | 
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+  90 |      1 | 0000      | this document is classified, category(----)   |                   | 
+ 100 |      1 | 0001      | this document is classified, category(---A)   |                   | 
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+ 120 |      1 | 0011      | this document is classified, category(--BA)   |                   | 
+ 170 |      2 | 0000      | this document is secret, category(----)       |                   | 
+ 180 |      2 | 0001      | this document is secret, category(---A)       |                   | 
+ 190 |      2 | 0010      | this document is secret, category(--B-)       |                   | 
+ 200 |      2 | 0011      | this document is secret, category(--BA)       |                   | 
+(12 rows)
+
+DELETE FROM document WHERE did = 30;			-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(30) is still referenced from table "browse".
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(90) is still referenced from table "browse".
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+SELECT * FROM t1;
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+SELECT * FROM t1 WHERE f_leak(c);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.c)
+               ->  Seq Scan on t1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.c)
+               ->  Seq Scan on t2
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+               Filter: f_leak(c)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a |  c  
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a |  c  |   t1    
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2
+                     Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  Seq Scan on t3
+(10 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  LockRows
+                     ->  Seq Scan on t1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  LockRows
+                     ->  Seq Scan on t2
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3
+(13 rows)
+
+SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.c)
+               ->  LockRows
+                     ->  Seq Scan on t1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.c)
+               ->  LockRows
+                     ->  Seq Scan on t2
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3
+                           Filter: f_leak(c)
+(16 rows)
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+                     QUERY PLAN                     
+----------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a <= 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a <= 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(c);
+NOTICE:  f_leak => aaa
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => bcd
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => def
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+           QUERY PLAN            
+---------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: f_leak(c)
+         ->  Seq Scan on t2 t1
+               Filter: f_leak(c)
+         ->  Seq Scan on t3 t1
+               Filter: f_leak(c)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a |  c  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+           QUERY PLAN           
+--------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t2 t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t3 t1
+               Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+          QUERY PLAN           
+-------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a = 2)
+         ->  Seq Scan on t2 t1
+               Filter: (a = 2)
+         ->  Seq Scan on t3 t1
+               Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+                    QUERY PLAN                     
+---------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a = 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a = 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE:  drop cascades to 7 other objects
+DETAIL:  drop cascades to table account
+drop cascades to function f_leak(text)
+drop cascades to table document
+drop cascades to table browse
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 3f04442..ffcd8d3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
  pg_proc                 | t
  pg_range                | t
  pg_rewrite              | t
+ pg_rowlevelsec          | t
  pg_seclabel             | t
  pg_shdepend             | t
  pg_shdescription        | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(155 rows)
+(156 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index ac29194..f0541c0 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: privileges security_label collate
+test: privileges rowlevelsec security_label collate
 
 test: misc
 # rules cannot run concurrently with any test that creates a view
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 8576a7f..e789395 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: privileges
+test: rowlevelsec
 test: security_label
 test: collate
 test: misc
diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql
new file mode 100644
index 0000000..a4d1ccd
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,237 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+DELETE FROM document WHERE did = 30;			-- failed
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101	1	aaa
+102	2	bbb
+103	3	ccc
+104	4	ddd
+\.
+
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201	1	abc	1.1
+202	2	bcd	2.2
+203	3	cde	3.3
+204	4	def	4.4
+\.
+
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 FROM stdin WITH (oids);
+301	1	xxx	X
+302	2	yyy	Y
+303	3	zzz	Z
+\.
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
#22Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Kohei KaiGai (#21)
1 attachment(s)
Re: [v9.3] Row-Level Security

The attached patch is a refreshed version towards the latest master branch,
to fix up patch conflicts.
Here is no other difference from the previous revision.

Thanks,

2012/9/5 Kohei KaiGai <kaigai@kaigai.gr.jp>:

2012/9/3 Kohei KaiGai <kaigai@kaigai.gr.jp>:

2012/9/2 Dean Rasheed <dean.a.rasheed@gmail.com>:

On 17 July 2012 05:02, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

2012/7/17 Robert Haas <robertmhaas@gmail.com>:

On Sun, Jul 15, 2012 at 5:52 AM, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a revised version of row-level security feature.
...
According to the Robert's comment, I revised the place to inject
applyRowLevelSecurity(). The reason why it needed to patch on
adjust_appendrel_attrs_mutator() was, we handled expansion from
regular relation to sub-query after expand_inherited_tables().
In this revision, it was moved to the head of sub-query planner.

Hi,

I had a quick look at this and spotted a problem - certain types of
query are able to bypass the RLS quals. For example:

SELECT * FROM (SELECT * FROM foo) foo;

since the RLS policy doesn't descend into subqueries, and is applied
before they are pulled up into the main query. Similarly for views on
top of tables with RLS, and SRF functions that query a table with RLS
that get inlined.

Also queries using UNION ALL are vulnerable if they end up being
flattened, for example:

SELECT * FROM foo UNION ALL SELECT * FROM foo;

Thanks for your comment.

Indeed, I missed the case of simple sub-queries and union-all being
pulled up into the main query. So, I adjusted the location to invoke
applyRowLevelSecurity() between all the pull-up stuff and expanding
inherited tables.

The attached patch is a fixed and rebased revision for CF:Sep.

Sorry! I attached incorrect revision. The attached patch is right one.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

Attachments:

pgsql-v9.3-row-level-security.ro.v4.patchapplication/octet-stream; name=pgsql-v9.3-row-level-security.ro.v4.patchDownload
 doc/src/sgml/catalogs.sgml                 |  59 +++
 doc/src/sgml/ref/alter_table.sgml          |  38 ++
 doc/src/sgml/user-manag.sgml               | 123 +++++
 src/backend/access/transam/xact.c          |  12 +
 src/backend/catalog/Makefile               |   4 +-
 src/backend/catalog/dependency.c           |  23 +
 src/backend/catalog/heap.c                 |   1 +
 src/backend/catalog/pg_rowlevelsec.c       | 241 +++++++++
 src/backend/commands/tablecmds.c           |  64 +++
 src/backend/optimizer/plan/planner.c       |  16 +
 src/backend/optimizer/util/Makefile        |   2 +-
 src/backend/optimizer/util/rowlevelsec.c   | 678 ++++++++++++++++++++++++++
 src/backend/parser/gram.y                  |  16 +
 src/backend/parser/parse_agg.c             |   6 +
 src/backend/parser/parse_expr.c            |   3 +
 src/backend/utils/adt/ri_triggers.c        |  14 +
 src/backend/utils/cache/plancache.c        |  32 ++
 src/backend/utils/cache/relcache.c         |  17 +-
 src/bin/pg_dump/pg_dump.c                  |  76 ++-
 src/bin/pg_dump/pg_dump.h                  |   1 +
 src/include/catalog/dependency.h           |   1 +
 src/include/catalog/indexing.h             |   3 +
 src/include/catalog/pg_class.h             |  20 +-
 src/include/catalog/pg_rowlevelsec.h       |  60 +++
 src/include/nodes/parsenodes.h             |   5 +-
 src/include/nodes/plannodes.h              |   2 +
 src/include/nodes/relation.h               |   2 +
 src/include/optimizer/rowlevelsec.h        |  31 ++
 src/include/parser/parse_node.h            |   3 +-
 src/include/utils/plancache.h              |   2 +
 src/include/utils/rel.h                    |   2 +
 src/test/regress/expected/rowlevelsec.out  | 753 +++++++++++++++++++++++++++++
 src/test/regress/expected/sanity_check.out |   3 +-
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/serial_schedule           |   1 +
 src/test/regress/sql/rowlevelsec.sql       | 237 +++++++++
 36 files changed, 2526 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index f999190..55898a5 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
      </row>
 
      <row>
+      <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+      <entry>row-level security policy of relation</entry>
+     </row>
+
+     <row>
       <entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
       <entry>security labels on database objects</entry>
      </row>
@@ -1807,6 +1812,16 @@
      </row>
 
      <row>
+      <entry><structfield>relhasrowlevelsec</structfield></entry>
+      <entry><type>bool</type></entry>
+      <entry></entry>
+      <entry>
+       True if table has row-level security policy; see
+       <link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link> catalog
+      </entry>
+     </row>
+
+     <row>
       <entry><structfield>relhassubclass</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
@@ -4906,6 +4921,50 @@
 
  </sect1>
 
+ <sect1 id="catalog-pg-rowlevelsec">
+  <title><structname>pg_rowlevelsec</structname></title>
+
+  <indexterm zone="catalog-pg-rowlevelsec">
+   <primary>pg_rowlevelsec</primary>
+  </indexterm>
+  <para>
+   The catalog <structname>pg_rowlevelsec</structname> expression tree of
+   row-level security policy to be performed on a particular relation.
+  </para>
+  <table>
+   <title><structname>pg_rowlevelsec</structname> Columns</title>
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>rlsrelid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+      <entry>The table this row-level security is for</entry>
+     </row>
+     <row>
+      <entry><structfield>rlsqual</structfield></entry>
+      <entry><type>text</type></entry>
+      <entry></entry>
+      <entry>An expression tree to be performed as rowl-level security policy</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+  <note>
+   <para>
+    <literal>pg_class.relhasrowlevelsec</literal>
+    must be true if a table has row-level security policy in this catalog.
+   </para>
+  </note>
+ </sect1>
 
  <sect1 id="catalog-pg-seclabel">
   <title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 356419e..9255c45 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     NOT OF
     OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
     SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+    SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)
+    RESET ROW LEVEL SECURITY
 
 <phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
 
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
    </varlistentry>
 
    <varlistentry>
+    <term><literal>SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+    <listitem>
+     <para>
+      This form set row-level security policy of the table.
+      Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+      as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+      clause, although mechanism guarantees to evaluate this condition earlier
+      than any other user given condition.
+      See also <xref linkend="ROW-LEVEL-SECURITY">.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET ROW LEVEL SECURITY</literal></term>
+    <listitem>
+     <para>
+      This form reset row-level security policy of the table, if exists.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
@@ -808,6 +833,19 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="PARAMETER">condition</replaceable></term>
+      <listitem>
+       <para>
+        An expression that returns a value of type boolean. Expect for a case
+        when queries are executed with superuser privilege, only rows for which
+        this expression returns true will be fetched, updated or deleted.
+        This expression can reference columns of the relation being configured,
+        however, unavailable to include sub-query right now.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c283e07 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,127 @@ DROP ROLE <replaceable>name</replaceable>;
   </para>
  </sect1>
 
+ <sect1 id="row-level-security">
+  <title>Row-level Security</title>
+  <para>
+   <productname>PostgreSQL</productname> v9.3 or later provides
+   row-level security feature, like several commercial database
+   management system. It allows table owner to assign a particular
+   condition that performs as a security policy of the table; only
+   rows that satisfies the condition should be visible, except for
+   a case when superuser runs queries.
+  </para>
+  <para>
+   Row-level security policy can be set using
+   <literal>SET ROW LEVEL SECURITY</literal> command of
+   <xref linkend="SQL-ALTERTABLE"> statement, as an expression
+    form that returns a value of type boolean. This expression can
+    contain references to columns of the relation, so it enables
+    to construct arbitrary rule to make access control decision
+    based on contents of each rows.
+  </para>
+  <para>
+   For example, the following <literal>customer</literal> table
+   has <literal>uname</literal> field to store user name, and
+   it assume we don't want to expose any properties of other
+   customers.
+   The following command set <literal>current_user = uname</literal>
+   as row-level security policy on the <literal>customer</literal>
+   table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+</screen>
+   <xref linkend="SQL-EXPLAIN"> command shows how row-level
+   security policy works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+                 QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+   Filter: f_leak(customer.upasswd)
+   ->  Seq Scan on customer
+         Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+   This query execution plan means the preconfigured row-level
+   security policy is implicitly added, and scan plan on the
+   target relation being wrapped up with a sub-query.
+   It ensures user given qualifiers, including functions with
+   side effects, are never executed earlier than the row-level
+   security policy regardless of its cost, except for the cases
+   when these were fully leakproof.
+   This design helps to tackle the scenario described in
+   <xref linkend="RULES-PRIVILEGES">; that introduces the order
+   to evaluate qualifiers is significant to keep confidentiality
+   of invisible rows.
+  </para>
+
+  <para>
+   On the other hand, this design allows superusers to bypass
+   checks with row-level security.
+   It ensures <application>pg_dump</application> can obtain
+   a complete set of database backup, and avoid to execute
+   Trojan horse trap, being injected as a row-level security
+   policy of user-defined table, with privileges of superuser.
+  </para>
+
+  <para>
+   In case of queries on inherited tables, row-level security
+   policy of the parent relation is not applied to child
+   relations. Scope of the row-level security policy is limited
+   to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+                QUERY PLAN
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.y)
+               ->  Seq Scan on t1
+                     Filter: ((x % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: f_leak(y)
+         ->  Subquery Scan on t3
+               Filter: f_leak(t3.y)
+               ->  Seq Scan on t3
+                     Filter: ((x % 2) = 1)
+(12 rows)
+</screen>
+    In the above example, <literal>t1</literal> has inherited
+    child table <literal>t2</literal> and <literal>t3</literal>,
+    and row-level security policy is set on only <literal>t1</literal>,
+    and <literal>t3</literal>, not <literal>t2</literal>.
+
+    The row-level security policy of <literal>t1</literal>,
+    <literal>x</literal> must be even-number, is appended only
+    <literal>t1</literal>, neither <literal>t2</literal> nor
+    <literal>t3</literal>. On the contrary, <literal>t3</literal>
+    has different row-level security policy; <literal>x</literal>
+    must be odd-number.
+  </para>
+
+  <para>
+   Right now, row-level security feature has several limitation,
+   although these shall be improved in the future version.
+
+   Row-level security policy is not applied to
+   <command>UPDATE</command> or <command>DELETE</command>
+   commands in this revision, even though it should be
+   supported soon.
+
+   Row-level security policy is not applied to rows to be inserted
+   and newer revision of updated rows, thus, it requires to
+   define before-row-insert or before-row-update trigger to check
+   whether the row's contents satisfies the policy individually.
+
+   Although it is not a specific matter in row-level security,
+   <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH">
+   allows to switch current user identifier during execution
+   of the query. Thus, it may cause unpredicated behavior
+   if and when <literal>current_user</literal> is used as
+   a part of row-level security policy.
+  </para>
+ </sect1>
 </chapter>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e7a6606..851f9db 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
 #include "executor/spi.h"
 #include "libpq/be-fsstubs.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "pgstat.h"
 #include "replication/walsender.h"
 #include "replication/syncrep.h"
@@ -144,6 +145,7 @@ typedef struct TransactionStateData
 	int			maxChildXids;	/* allocated size of childXids[] */
 	Oid			prevUser;		/* previous CurrentUserId setting */
 	int			prevSecContext; /* previous SecurityRestrictionContext */
+	RowLevelSecMode	prevRowLevelSecMode;	/* previous RLS-mode setting */
 	bool		prevXactReadOnly;		/* entry-time xact r/o state */
 	bool		startedInRecovery;		/* did we start in recovery? */
 	struct TransactionStateData *parent;		/* back link to parent */
@@ -173,6 +175,7 @@ static TransactionStateData TopTransactionStateData = {
 	0,							/* allocated size of childXids[] */
 	InvalidOid,					/* previous CurrentUserId setting */
 	0,							/* previous SecurityRestrictionContext */
+	RowLevelSecModeEnabled,		/* previous RowLevelSecMode setting */
 	false,						/* entry-time xact r/o state */
 	false,						/* startedInRecovery */
 	NULL						/* link to parent state block */
@@ -1764,6 +1767,8 @@ StartTransaction(void)
 	/* SecurityRestrictionContext should never be set outside a transaction */
 	Assert(s->prevSecContext == 0);
 
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
+
 	/*
 	 * initialize other subsystems for new transaction
 	 */
@@ -2295,6 +2300,9 @@ AbortTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Also, mode setting of row-level security should be restored. */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * do abort processing
 	 */
@@ -4188,6 +4196,9 @@ AbortSubTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Reset row-level security mode */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * We can skip all this stuff if the subxact failed before creating a
 	 * ResourceOwner...
@@ -4327,6 +4338,7 @@ PushTransaction(void)
 	s->state = TRANS_DEFAULT;
 	s->blockState = TBLOCK_SUBBEGIN;
 	GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext);
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
 	s->prevXactReadOnly = XactReadOnly;
 
 	CurrentTransactionState = s;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index df6da1f..965aa38 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
        objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \
        pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
        pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
-       pg_type.o storage.o toasting.o
+       pg_rowlevelsec.o pg_type.o storage.o toasting.o
 
 BKIFILES = postgres.bki postgres.description postgres.shdescription
 
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
-	pg_foreign_table.h \
+	pg_foreign_table.h pg_rowlevelsec.h \
 	pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
 	toasting.h indexing.h \
     )
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 87d6f02..f0736b1 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
@@ -1229,6 +1230,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveExtensionById(object->objectId);
 			break;
 
+		case OCLASS_ROWLEVELSEC:
+			RemoveRowLevelSecurityById(object->objectId);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized object class: %u",
 				 object->classId);
@@ -2280,6 +2285,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case EventTriggerRelationId:
 			return OCLASS_EVENT_TRIGGER;
+
+		case RowLevelSecurityRelationId:
+			return OCLASS_ROWLEVELSEC;
 	}
 
 	/* shouldn't get here */
@@ -2929,6 +2937,21 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_ROWLEVELSEC:
+			{
+				char	   *relname;
+
+				relname = get_rel_name(object->objectId);
+				if (!relname)
+					elog(ERROR, "cache lookup failed for relation %u",
+						 object->objectId);
+				appendStringInfo(&buffer,
+								 _("row-level security of %s"), relname);
+
+
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index c80df41..60de76d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -776,6 +776,7 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
 	values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
 	values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+	values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec);
 	values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	if (relacl != (Datum) 0)
diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c
new file mode 100644
index 0000000..e554f20
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,241 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlevelsec.c
+ *    routines to support manipulation of the pg_rowlevelsec relation
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+void
+RelationBuildRowLevelSecurity(Relation relation)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+
+	tuple = systable_getnext(sscan);
+	if (HeapTupleIsValid(tuple))
+	{
+		RowLevelSecDesc	*rlsdesc;
+		MemoryContext	rlscxt;
+		MemoryContext	oldcxt;
+		Datum	datum;
+		bool	isnull;
+		char   *temp;
+
+		/*
+		 * Make the private memory context to store RowLevelSecDesc that
+		 * includes expression tree also.
+		 */
+		rlscxt = AllocSetContextCreate(CacheMemoryContext,
+									   RelationGetRelationName(relation),
+									   ALLOCSET_SMALL_MINSIZE,
+									   ALLOCSET_SMALL_INITSIZE,
+									   ALLOCSET_SMALL_MAXSIZE);
+		PG_TRY();
+		{
+			datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual,
+								 RelationGetDescr(rlsrel), &isnull);
+			Assert(!isnull);
+			temp = TextDatumGetCString(datum);
+
+			oldcxt = MemoryContextSwitchTo(rlscxt);
+
+			rlsdesc = palloc0(sizeof(RowLevelSecDesc));
+			rlsdesc->rlscxt = rlscxt;
+			rlsdesc->rlsqual = (Expr *) stringToNode(temp);
+			Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID);
+
+			rlsdesc->rlshassublinks
+				= contain_subplans((Node *)rlsdesc->rlsqual);
+
+			MemoryContextSwitchTo(oldcxt);
+
+			pfree(temp);
+		}
+		PG_CATCH();
+		{
+			MemoryContextDelete(rlscxt);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		relation->rlsdesc = rlsdesc;
+	}
+	else
+	{
+		relation->rlsdesc = NULL;
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, AccessShareLock);
+}
+
+void
+SetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid				relationId = RelationGetRelid(relation);
+	ParseState	   *pstate;
+	RangeTblEntry  *rte;
+	Node		   *qual;
+	Relation		rlsrel;
+	ScanKeyData		skey;
+	SysScanDesc		sscan;
+	HeapTuple		oldtup;
+	HeapTuple		newtup;
+	Datum			values[Natts_pg_rowlevelsec];
+	bool			isnull[Natts_pg_rowlevelsec];
+	bool			replaces[Natts_pg_rowlevelsec];
+	ObjectAddress	target;
+	ObjectAddress	myself;
+
+	/* Parse the supplied clause */
+	pstate = make_parsestate(NULL);
+
+	rte = addRangeTableEntryForRelation(pstate, relation,
+										NULL, false, false);
+	addRTEtoQuery(pstate, rte, false, true, true);
+
+	qual = transformWhereClause(pstate, copyObject(clause),
+								EXPR_KIND_ROW_LEVEL_SEC,
+								"ROW LEVEL SECURITY");
+	/* zero-clear */
+	memset(values,   0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(isnull,   0, sizeof(isnull));
+
+	/* Update or Indert an entry to pg_rowlevelsec catalog  */
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	oldtup = systable_getnext(sscan);
+	if (HeapTupleIsValid(oldtup))
+	{
+		replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true;
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+
+		newtup = heap_modify_tuple(oldtup,
+								   RelationGetDescr(rlsrel),
+								   values, isnull, replaces);
+		simple_heap_update(rlsrel, &newtup->t_self, newtup);
+
+		deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+								   relationId, false);
+	}
+	else
+	{
+		values[Anum_pg_rowlevelsec_rlsrelid - 1]
+			= ObjectIdGetDatum(relationId);
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+		newtup = heap_form_tuple(RelationGetDescr(rlsrel),
+								 values, isnull);
+		simple_heap_insert(rlsrel, newtup);
+	}
+	CatalogUpdateIndexes(rlsrel, newtup);
+
+	heap_freetuple(newtup);
+
+	/* records dependencies of RLS-policy and relation/columns */
+	target.classId = RelationRelationId;
+	target.objectId = relationId;
+	target.objectSubId = 0;
+
+	myself.classId = RowLevelSecurityRelationId;
+	myself.objectId = relationId;
+	myself.objectSubId = 0;
+
+	recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+	recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+						   DEPENDENCY_NORMAL);
+	free_parsestate(pstate);
+
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+void
+ResetRowLevelSecurity(Relation relation)
+{
+	if (relation->rlsdesc)
+	{
+		ObjectAddress	address;
+
+		address.classId = RowLevelSecurityRelationId;
+		address.objectId = RelationGetRelid(relation);
+		address.objectSubId = 0;
+
+		performDeletion(&address, DROP_RESTRICT, 0);
+	}
+	else
+	{
+		/* Nothing to do here */
+		elog(INFO, "relation %s has no row-level security policy, skipped",
+			 RelationGetRelationName(relation));
+	}
+}
+
+/*
+ * Guts of Row-level security policy deletion.
+ */
+void
+RemoveRowLevelSecurityById(Oid relationId)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relationId));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+	{
+		simple_heap_delete(rlsrel, &tuple->t_self);
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 359d478..83ca86b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -384,6 +385,7 @@ static void ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
 static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid);
 static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode);
 static void ATExecDropOf(Relation rel, LOCKMODE lockmode);
+static void ATExecSetRowLevelSecurity(Relation relation, Node *clause);
 static void ATExecGenericOptions(Relation rel, List *options);
 
 static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
@@ -2746,6 +2748,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_SetTableSpace:		/* must rewrite heap */
 			case AT_DropNotNull:		/* may change some SQL plans */
 			case AT_SetNotNull:
+			case AT_SetRowLevelSecurity:
+			case AT_ResetRowLevelSecurity:
 			case AT_GenericOptions:
 			case AT_AlterColumnGenericOptions:
 				cmd_lockmode = AccessExclusiveLock;
@@ -3108,6 +3112,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_DropInherit:	/* NO INHERIT */
 		case AT_AddOf:			/* OF */
 		case AT_DropOf: /* NOT OF */
+		case AT_SetRowLevelSecurity:
+		case AT_ResetRowLevelSecurity:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* These commands never recurse */
 			/* No command-specific prep needed */
@@ -3383,6 +3389,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropOf:
 			ATExecDropOf(rel, lockmode);
 			break;
+		case AT_SetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, (Node *) cmd->def);
+			break;
+		case AT_ResetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, NULL);
+			break;
 		case AT_GenericOptions:
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
@@ -7533,6 +7545,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				Assert(defaultexpr);
 				break;
 
+			case OCLASS_ROWLEVELSEC:
+				/*
+				 * A row-level security policy can depend on a column in case
+				 * when the policy clause references a particular column.
+				 * Due to same reason why TRIGGER ... WHEN does not support
+				 * to change column's type being referenced in clause, row-
+				 * level security policy also does not support it.
+				 */
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used in a row-level security policy"),
+						 errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject),
+								   colName)));
+				break;
+
 			case OCLASS_PROC:
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
@@ -9631,6 +9659,42 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode)
 }
 
 /*
+ * ALTER TABLE <name> SET ROW LEVEL SECURITY (...) OR
+ *                    RESET ROW LEVEL SECURITY
+ */
+static void
+ATExecSetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid			relid = RelationGetRelid(relation);
+	Relation	class_rel;
+	HeapTuple	tuple;
+	Form_pg_class	class_form;
+
+	if (clause != NULL)
+		SetRowLevelSecurity(relation, clause);
+	else
+		ResetRowLevelSecurity(relation);
+
+	class_rel = heap_open(RelationRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	class_form = (Form_pg_class) GETSTRUCT(tuple);
+	if (clause != NULL)
+		class_form->relhasrowlevelsec = true;
+	else
+		class_form->relhasrowlevelsec = false;
+
+	simple_heap_update(class_rel, &tuple->t_self, tuple);
+	CatalogUpdateIndexes(class_rel, tuple);
+
+	heap_close(class_rel, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
+/*
  * ALTER FOREIGN TABLE <name> OPTIONS (...)
  */
 static void
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b61005f..f9d315e 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/analyze.h"
@@ -167,6 +168,13 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	glob->lastPHId = 0;
 	glob->lastRowMarkId = 0;
 	glob->transientPlan = false;
+	glob->planUserId = InvalidOid;
+	/*
+	 * XXX - a valid user-id shall be set on planUserId later, if constructed
+	 * plan assumes being executed under privilege of a particular user-id.
+	 * Elsewhere, keep InvalidOid; that means the constructed plan is portable
+	 * for any users.
+	 */
 
 	/* Determine what fraction of the plan is likely to be scanned */
 	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -244,6 +252,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->nParamExec = glob->nParamExec;
+	result->planUserId = glob->planUserId;
 
 	return result;
 }
@@ -351,6 +360,13 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		flatten_simple_union_all(root);
 
 	/*
+	 * Apply row-level security policy of appeared tables, if configured.
+	 * Note that it must be processed after all the simple subqueries or
+	 * union all queries being flatten, but prior to preprocess_rowmarks().
+	 */
+	applyRowLevelSecurity(root);
+
+	/*
 	 * Detect whether any rangetable entries are RTE_JOIN kind; if not, we can
 	 * avoid the expense of doing flatten_join_alias_vars().  Also check for
 	 * outer joins --- if none, we can skip reduce_outer_joins().  And check
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3430689 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
-       relnode.o restrictinfo.o tlist.o var.o
+       relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c
new file mode 100644
index 0000000..3a01b0c
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,678 @@
+/*
+ * optimizer/util/rowlvsec.c
+ *    Row-level security support routines
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
+#include "parser/parsetree.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* flags to pull row-level security policy */
+#define RLS_FLAG_HAS_SUBLINKS			0x0001
+
+/* hook to allow extensions to apply their own security policy */
+rowlevel_security_hook_type	rowlevel_security_hook = NULL;
+
+/* current performing mode of row-level security policy */
+static RowLevelSecMode	rowlevel_security_mode = RowLevelSecModeEnabled;
+
+/*
+ * getRowLevelSecurityMode / setRowLevelSecurityMode
+ *
+ * These functions allow to get or set current performing mode of row-
+ * level security feature. It enables to disable this feature temporarily
+ * for some cases in which row-level security prevent correct behavior
+ * such as foreign-key checks to prohibit update of PKs being referenced
+ * by others.
+ * The caller must ensure the saved previous mode shall be restored, but
+ * no need to care about cases when an error would be raised.
+ */
+RowLevelSecMode
+getRowLevelSecurityMode(void)
+{
+	return rowlevel_security_mode;
+}
+
+void
+setRowLevelSecurityMode(RowLevelSecMode new_mode)
+{
+	rowlevel_security_mode = new_mode;
+}
+
+/*
+ * pull_rowlevel_security_policy
+ *
+ * This routine tries to pull expression node of row-level security policy
+ * on the target relation, and its children if configured.
+ * If one or more relation has a security policy at least, this function
+ * returns true, or false elsewhere.
+ */
+static bool
+pull_rowlevel_security_policy(PlannerInfo *root,
+							  RangeTblEntry *rte,
+							  Index rtindex,
+							  List **rls_relids,
+							  List **rls_quals,
+							  List **rls_flags)
+{
+	Relation		rel;
+	LOCKMODE		lockmode;
+	List		   *relid_list = NIL;
+	List		   *qual_list = NIL;
+	List		   *flag_list = NIL;
+	ListCell	   *cell;
+	bool			result = false;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+	if (!rte->inh)
+	{
+		lockmode = NoLock;
+		relid_list = list_make1_oid(rte->relid);
+	}
+	else
+	{
+		/*
+		 * In case when the target relation may have inheritances,
+		 * we need to suggest an appropriate lock mode because it
+		 * is the first time to reference these tables in a series
+		 * of processes. For more details, see the comments in
+		 * expand_inherited_tables.
+		 * Also note that it does not guarantee the locks on child
+		 * tables being already acquired at expand_inherited_tables,
+		 * because row-level security routines can be bypassed if
+		 * RowLevelSecModeDisabled.
+		 */
+		if (rtindex == root->parse->resultRelation)
+			lockmode = RowExclusiveLock;
+		else
+		{
+			foreach (cell, root->parse->rowMarks)
+			{
+				Assert(IsA(lfirst(cell), RowMarkClause));
+				if (((RowMarkClause *) lfirst(cell))->rti == rtindex)
+					break;
+			}
+			if (cell)
+				lockmode = RowShareLock;
+			else
+				lockmode = AccessShareLock;
+		}
+		relid_list = find_all_inheritors(rte->relid, lockmode, NULL);
+	}
+
+	/*
+	 * Try to fetch row-level security policy of the target relation
+	 * or its children.
+	 */
+	foreach (cell, relid_list)
+	{
+		Expr   *qual = NULL;
+		int		flags = 0;
+
+		rel = heap_open(lfirst_oid(cell), lockmode);
+
+		/*
+		 * Pull out row-level security policy configured with built-in
+		 * features, if unprivileged users. Please note that superuser
+		 * can bypass it.
+		 */
+		if (rel->rlsdesc && !superuser())
+		{
+			RowLevelSecDesc	*rlsdesc = rel->rlsdesc;
+
+			qual = copyObject(rlsdesc->rlsqual);
+			if (rlsdesc->rlshassublinks)
+				flags |= RLS_FLAG_HAS_SUBLINKS;
+		}
+
+		/*
+		 * Also, ask extensions whether they want to apply their own
+		 * row-level security policy. If both built-in and extension
+		 * has their own policy, it shall be merged.
+		 */
+		if (rowlevel_security_hook)
+		{
+			List   *qual_list;
+
+			qual_list = (*rowlevel_security_hook)(root, rel);
+			if (qual_list != NIL)
+			{
+				if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+					contain_subplans((Node *)qual_list))
+					flags |= RLS_FLAG_HAS_SUBLINKS;
+
+				if (qual != NULL)
+					qual_list = lappend(qual_list, qual);
+
+				if (list_length(qual_list) == 1)
+					qual = (Expr *)list_head(qual_list);
+				else
+					qual = makeBoolExpr(AND_EXPR, qual_list, -1);
+			}
+		}
+
+		qual_list = lappend(qual_list, qual);
+		if (qual)
+			result = true;
+		flag_list = lappend_int(flag_list, flags);
+
+		heap_close(rel, NoLock);  /* close the relation, but keep locks */
+	}
+
+	/*
+	 * Inform the caller list of relation Oid, qualifier of row-level
+	 * security policy and its flag, if one or more target relations
+	 * have its row-level security policy. Elsewhere, release it.
+	 */
+	if (result)
+	{
+		*rls_relids = relid_list;
+		*rls_quals = qual_list;
+		*rls_flags = flag_list;
+	}
+	else
+	{
+		list_free(relid_list);
+		list_free(qual_list);
+		list_free(flag_list);
+	}
+	return result;
+}
+
+/*
+ * fixup_varattnos
+ *
+ * It fixes up varattno of Var node that referenced the relation with
+ * RLS policy, thus replaced to a sub-query. Here is no guarantee the
+ * varattno matches with TargetEntry's resno of the sub-query, so needs
+ * to adjust them.
+ */
+typedef struct {
+	PlannerInfo *root;
+	int		varlevelsup;
+} fixup_var_context;
+
+static bool
+fixup_var_references_walker(Node *node, fixup_var_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var            *var = (Var *)node;
+		RangeTblEntry  *rte;
+
+		/*
+		 * Does this Var node reference the Query node currently we focused
+		 * on. If not, we simply ignore it.
+		 */
+		if (var->varlevelsup != context->varlevelsup)
+			return false;
+
+		rte = rt_fetch(var->varno, context->root->parse->rtable);
+		if (!rte)
+			elog(ERROR, "invalid varno %d", var->varno);
+
+		if (rte->rtekind == RTE_SUBQUERY &&
+			rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+		{
+			List       *targetList = rte->subquery->targetList;
+			ListCell   *cell;
+
+			foreach (cell, targetList)
+			{
+				TargetEntry    *subtle = lfirst(cell);
+
+				if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+					 var->varattno == InvalidAttrNumber) ||
+					(IsA(subtle->expr, Var) &&
+					 var->varattno == ((Var *)(subtle->expr))->varattno))
+				{
+					var->varattno = subtle->resno;
+					return false;
+				}
+			}
+			elog(ERROR, "invalid varattno %d", var->varattno);
+		}
+		return false;
+	}
+	else if (IsA(node, Query))
+	{
+		bool    result;
+
+		context->varlevelsup++;
+		result = query_tree_walker((Query *) node,
+								   fixup_var_references_walker,
+								   (void *) context, 0);
+		context->varlevelsup--;
+
+		return result;
+	}
+	return expression_tree_walker(node,
+								  fixup_var_references_walker,
+								  (void *) context);
+}
+
+static void
+fixup_varattnos(PlannerInfo *root)
+{
+	fixup_var_context   context;
+
+	/*
+	 * Fixup Var->varattno that references the sub-queries originated from
+	 * regular relations with RLS policy.
+	 */
+	context.root = root;
+	context.varlevelsup = 0;
+
+	query_tree_walker(root->parse,
+					  fixup_var_references_walker,
+					  (void *) &context, 0);
+}
+
+/*
+ * make_pseudo_column
+ *
+ * make a TargetEntry object which references a particular column of
+ * the underlying table.
+ */
+static TargetEntry *
+make_pseudo_column(Oid relid_head, Oid relid, AttrNumber attnum)
+{
+	Form_pg_attribute attform;
+	HeapTuple	tuple;
+	Var		   *var;
+	char	   *resname;
+
+	if (attnum == InvalidAttrNumber)
+	{
+		ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);
+
+		r->arg = (Expr *) makeVar((Index) 1,
+								  InvalidAttrNumber,
+								  get_rel_type_id(relid),
+								  -1,
+								  InvalidOid,
+								  0);
+		r->resulttype = get_rel_type_id(relid_head);
+		r->convertformat = COERCE_IMPLICIT_CAST;
+		r->location = -1;
+
+		return makeTargetEntry((Expr *) r, -1, get_rel_name(relid), false);
+	}
+
+	tuple = SearchSysCache2(ATTNUM,
+							ObjectIdGetDatum(relid),
+							Int16GetDatum(attnum));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+			 attnum, relid);
+	attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+	var = makeVar((Index) 1,
+				  attform->attnum,
+				  attform->atttypid,
+				  attform->atttypmod,
+				  InvalidOid,
+				  0);
+	resname = pstrdup(NameStr(attform->attname));
+
+	ReleaseSysCache(tuple);
+
+	return makeTargetEntry((Expr *)var, -1, resname, false);
+}
+
+/*
+ * make_pseudo_subquery
+ *
+ * This routine makes a sub-query that references the target relation
+ * with given row-level security policy. This sub-query shall have
+ * security_barrier attribute to prevent unexpected push-down.
+ */
+static Query *
+make_pseudo_subquery(Oid relid_head,
+					 Oid relid,
+					 Node *qual,
+					 int flags,
+					 List *targetList,
+					 RangeTblEntry *rte,
+					 RowMarkClause *rclause)
+{
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+	List		   *colnameList = NIL;
+	ListCell	   *cell;
+
+	subqry = makeNode(Query);
+	subqry->commandType = CMD_SELECT;
+	subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	subrte = makeNode(RangeTblEntry);
+	subrte->rtekind = RTE_RELATION;
+	subrte->relid = relid;
+	subrte->relkind = get_rel_relkind(relid);
+	subrte->inFromCl = true;
+	subrte->requiredPerms = rte->requiredPerms;
+	subrte->selectedCols = NULL;
+	subrte->modifiedCols = NULL;
+
+	foreach (cell, targetList)
+	{
+		TargetEntry	   *oldtle = lfirst(cell);
+		TargetEntry	   *subtle;
+		AttrNumber		attnum;
+
+		if (IsA(oldtle->expr, ConvertRowtypeExpr))
+			attnum = InvalidAttrNumber;
+		else
+		{
+			Assert(IsA(oldtle->expr, Var));
+
+			attnum = get_attnum(relid, oldtle->resname);
+			if (attnum == InvalidAttrNumber)
+				ereport(ERROR,
+						(errcode(ERRCODE_UNDEFINED_COLUMN),
+					errmsg("column \"%s\" of relation \"%s\" does not exist",
+								oldtle->resname, get_rel_name(relid))));
+		}
+		subtle = make_pseudo_column(relid_head, relid, attnum);
+		subtle->resno = oldtle->resno;
+		subqry->targetList = lappend(subqry->targetList, subtle);
+
+		colnameList = lappend(colnameList,
+							  makeString(pstrdup(subtle->resname)));
+		attnum -= FirstLowInvalidHeapAttributeNumber;
+		subrte->selectedCols = bms_add_member(rte->selectedCols, attnum);
+	}
+	subrte->eref = makeAlias(get_rel_name(relid), colnameList);
+
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		subqry->hasSubLinks = true;
+
+	subqry->rtable = list_make1(subrte);
+
+	subrtr = makeNode(RangeTblRef);
+	subrtr->rtindex = 1;
+	subqry->jointree = makeFromExpr(list_make1(subrtr), qual);
+
+	if (rclause)
+	{
+		RowMarkClause  *rclause_sub;
+
+		rclause_sub = copyObject(rclause);
+		rclause_sub->rti = 1;
+
+		subqry->rowMarks = list_make1(rclause);
+		subqry->hasForUpdate = true;
+	}
+	return subqry;
+}
+
+/*
+ * expand_rtentry_with_policy
+ *
+ * This routine expand reference to the given RangeTblEntry by a sub-query
+ * which simply references the target relation with the qualifier of row-
+ * level security policy.
+ */
+static void
+expand_rtentry_with_policy(PlannerInfo *root,
+						   RangeTblEntry *rte,
+						   Index rtindex,
+						   List *rls_relids,
+						   List *rls_quals,
+						   List *rls_flags)
+{
+	Query		   *parse = root->parse;
+	Oid				relid_head;
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	List		   *targetList;
+	List		   *colnameList;
+	Bitmapset	   *attr_used;
+	AttrNumber		attnum;
+	RowMarkClause  *rclause;
+	ListCell	   *cell1;
+	ListCell	   *cell2;
+	ListCell	   *cell3;
+	ListCell	   *lc;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+	Assert(list_length(rls_relids) == list_length(rls_quals));
+	Assert(list_length(rls_relids) == list_length(rls_flags));
+	Assert(list_length(rls_relids) > 0);
+
+	/*
+	 * Construct a target-entry list
+	 */
+	targetList = NIL;
+	colnameList = NIL;
+	relid_head = linitial_oid(rls_relids);
+	attr_used = bms_union(rte->selectedCols,
+						  rte->modifiedCols);
+	while ((attnum = bms_first_member(attr_used)) >= 0)
+	{
+		attnum += FirstLowInvalidHeapAttributeNumber;
+
+		subtle = make_pseudo_column(relid_head, relid_head, attnum);
+		subtle->resno = list_length(targetList) + 1;
+
+		targetList = lappend(targetList, subtle);
+		colnameList = lappend(colnameList,
+							  makeString(pstrdup(subtle->resname)));
+	}
+	bms_free(attr_used);
+
+	/*
+	 * Push-down row-level lock of the target relation, since sub-query
+	 * does not support FOR SHARE/FOR UPDATE locks being assigned.
+	 */
+	rclause = NULL;
+	foreach (lc, parse->rowMarks)
+	{
+		if (((RowMarkClause *) lfirst(lc))->rti == rtindex)
+		{
+			rclause = lfirst(lc);
+			parse->rowMarks = list_delete(parse->rowMarks, rclause);
+			break;
+		}
+	}
+
+	/*
+	 * Construct sub-query structures
+	 */
+	forthree (cell1, rls_relids, cell2, rls_quals, cell3, rls_flags)
+	{
+		Oid			relid = lfirst_oid(cell1);
+		Node	   *qual = lfirst(cell2);
+		int			flags = lfirst_int(cell3);
+
+		subqry = make_pseudo_subquery(relid_head, relid, qual, flags,
+									  targetList, rte, rclause);
+		if (cell1 == list_head(rls_relids))
+		{
+			Assert(rte->relid == relid);
+			Assert(rte->inh == true || list_length(rls_relids) == 1);
+
+			rte->relid = InvalidOid;
+			rte->rtekind = RTE_SUBQUERY;
+			rte->subquery = subqry;
+			rte->security_barrier = true;
+
+			/* no permission checks are needed to subquery itself */
+			rte->requiredPerms = 0;
+			rte->checkAsUser = InvalidOid;
+			rte->selectedCols = NULL;
+			rte->modifiedCols = NULL;
+
+			rte->alias = NULL;
+			rte->eref = makeAlias(get_rel_name(relid),
+								  copyObject(colnameList));
+			if (list_length(rls_relids) == 1)
+				rte->inh = false;
+		}
+
+		/*
+		 * RTE's for child relations and AppendRelInfo in case when
+		 * the target relation has its children.
+		 */
+		if (list_length(rls_relids) > 1)
+		{
+			AppendRelInfo  *apinfo;
+			AttrNumber		child_rtindex;
+			ListCell	   *l;
+
+			/*
+			 * XXX - Set up RangeTblEntry for the child relation.
+			 * Note that it does not need to have security_barrier
+			 * attribute, if no row-level security policy is
+			 * configured on.
+			 */
+			subrte = makeNode(RangeTblEntry);
+			subrte->rtekind = RTE_SUBQUERY;
+			subrte->subquery = subqry;
+			if (qual)
+				subrte->security_barrier = true;
+			subrte->alias = NULL;
+			subrte->eref = makeAlias(get_rel_name(relid),
+									 copyObject(colnameList));
+			parse->rtable = lappend(parse->rtable, subrte);
+			child_rtindex = list_length(parse->rtable);
+
+			/*
+			 * reference to inherited children performs as if simple
+			 * UNION ALL operation, so add AppendRelInfo here.
+			 */
+			apinfo = makeNode(AppendRelInfo);
+			apinfo->parent_relid = rtindex;
+			apinfo->child_relid = child_rtindex;
+			foreach (l, targetList)
+			{
+				Var	   *trans_var
+					= makeVarFromTargetEntry(child_rtindex, lfirst(l));
+				apinfo->translated_vars
+					= lappend(apinfo->translated_vars, trans_var);
+			}
+			root->append_rel_list = lappend(root->append_rel_list, apinfo);
+		}
+	}
+}
+
+/*
+ * applyRowLevelSecurity
+ *
+ * It tries to apply row-level security policy of the relation.
+ * If and when a particular policy is configured on the referenced
+ * relation, it shall be replaced by a sub-query with security-barrier flag;
+ * that references the relation with row-level security policy.
+ * In the result, all users can see is rows of the relation that satisfies
+ * the condition supplied as security policy.
+ */
+void
+applyRowLevelSecurity(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	ListCell   *cell;
+	Index		rtindex;
+	bool		has_rowlevel_sec = false;
+
+	/* mode checks */
+	if (rowlevel_security_mode == RowLevelSecModeDisabled)
+		return;
+
+	/*
+	 * No need to apply row-level security on sub-query being originated
+	 * from regular relation with RLS policy any more.
+	 */
+	if (parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+		return;
+
+	rtindex = 0;
+	foreach (cell, parse->rtable)
+	{
+		RangeTblEntry  *rte = lfirst(cell);
+		List		   *rls_relids;
+		List		   *rls_quals;
+		List		   *rls_flags;
+
+		rtindex++;
+
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * XXX - In this revision, we have no support for UPDATE / DELETE
+		 * statement, so simply skip it.
+		 */
+		if (rtindex == parse->resultRelation)
+			continue;
+
+		/*
+		 * In case when a row-level security policy was configured on
+		 * the table referenced by this RangeTblEntry or its children,
+		 * it shall be rewritten to sub-query with this policy.
+		 */
+		if (pull_rowlevel_security_policy(root, rte, rtindex,
+										  &rls_relids, &rls_quals, &rls_flags))
+		{
+			expand_rtentry_with_policy(root, rte, rtindex,
+									   rls_relids, rls_quals, rls_flags);
+			has_rowlevel_sec = true;
+		}
+	}
+
+	/*
+	 * Post case handling if one or more relation was replaced to sub-query.
+	 */
+	if (has_rowlevel_sec)
+	{
+		PlanInvalItem  *pi_item;
+
+		/*
+		 * plan should be invalidated if and when userid was changed
+		 * on the executor stage, from planner stage.
+		 */
+		Assert(!OidIsValid(root->glob->planUserId) ||
+			   root->glob->planUserId == GetUserId());
+		root->glob->planUserId = GetUserId();
+
+		pi_item = makeNode(PlanInvalItem);
+		pi_item->cacheId = AUTHOID;
+		pi_item->hashValue
+			= GetSysCacheHashValue1(AUTHOID,
+									ObjectIdGetDatum(root->glob->planUserId));
+		root->glob->invalItems = lappend(root->glob->invalItems, pi_item);
+
+		/*
+		 * Since the relation with RLS policy was replaced by a sub-query,
+		 * thus resource number to reference a particular column can be
+		 * also moditifed. If we applied RLS policy on one or more relations,
+		 * varattno of Var node that has referenced the rewritten relation
+		 * needs to be fixed up.
+		 */
+		fixup_varattnos(root);
+	}
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e4ff76e..a3d445d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2127,6 +2127,22 @@ alter_table_cmd:
 					n->def = (Node *)$2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> SET ROW LEVEL SECURITY (expression) */
+			| SET ROW LEVEL SECURITY '(' a_expr ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_SetRowLevelSecurity;
+					n->def = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> RESET ROW LEVEL SECURITY */
+			| RESET ROW LEVEL SECURITY
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_ResetRowLevelSecurity;
+					n->def = NULL;
+					$$ = (Node *)n;
+				}
 			| alter_generic_options
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index d1d835b..87423d8 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("aggregate functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("aggregate functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("window functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("window functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e9267c5..94c25d1 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1443,6 +1443,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
+		case EXPR_KIND_ROW_LEVEL_SEC:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2609,6 +2610,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "EXECUTE";
 		case EXPR_KIND_TRIGGER_WHEN:
 			return "WHEN";
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			return "ROW LEVEL SECURITY";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 97e68b1..19a2255 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -43,6 +43,7 @@
 #include "parser/parse_coerce.h"
 #include "parser/parse_relation.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
@@ -2999,6 +3000,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
+	RowLevelSecMode	save_rls_mode;
 	Datum		vals[RI_MAX_NUMKEYS * 2];
 	char		nulls[RI_MAX_NUMKEYS * 2];
 
@@ -3081,6 +3083,15 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
 						   save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
 
+	/*
+	 * Disabled row-level security in case when foreign-key relation is
+	 * queried to check existence of tupls that references the tuple to
+	 * be modified on the primary-key side.
+	 */
+	save_rls_mode = getRowLevelSecurityMode();
+	if (source_is_pk)
+		setRowLevelSecurityMode(RowLevelSecModeDisabled);
+
 	/* Finally we can run the query. */
 	spi_result = SPI_execute_snapshot(qplan,
 									  vals, nulls,
@@ -3090,6 +3101,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	/* Restore UID and security context */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
 
+	/* Restore row-level security performing mode */
+	setRowLevelSecurityMode(save_rls_mode);
+
 	/* Check result */
 	if (spi_result < 0)
 		elog(ERROR, "SPI_execute_snapshot returned %d", spi_result);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 8c0391f..36a8750 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -51,6 +51,7 @@
 #include "catalog/namespace.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
@@ -665,6 +666,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
 		AcquireExecutorLocks(plan->stmt_list, true);
 
 		/*
+		 * If plan was constructed with assumption of a particular user-id,
+		 * and it is different from the current one, the cached-plan shall
+		 * be invalidated to construct suitable query plan.
+		 */
+		if (plan->is_valid &&
+			OidIsValid(plan->planUserId) &&
+			plan->planUserId == GetUserId())
+			plan->is_valid = false;
+
+		/*
 		 * If plan was transient, check to see if TransactionXmin has
 		 * advanced, and if so invalidate it.
 		 */
@@ -716,6 +727,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 {
 	CachedPlan *plan;
 	List	   *plist;
+	ListCell   *cell;
+	Oid			planUserId = InvalidOid;
 	bool		snapshot_set;
 	bool		spi_pushed;
 	MemoryContext plan_context;
@@ -794,6 +807,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	PopOverrideSearchPath();
 
 	/*
+	 * Check whether the generated plan assumes a particular user-id, or not.
+	 * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+	 * it should be kept and used to validation check of the cached plan
+	 * under the "current" user-id.
+	 */
+	foreach (cell, plist)
+	{
+		PlannedStmt	*pstmt = lfirst(cell);
+
+		if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+		{
+			Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+			planUserId = pstmt->planUserId;
+		}
+	}
+
+	/*
 	 * Make a dedicated memory context for the CachedPlan and its subsidiary
 	 * data.  It's probably not going to be large, but just in case, use the
 	 * default maxsize parameter.  It's transient for the moment.
@@ -828,6 +859,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->context = plan_context;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->planUserId = planUserId;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0cdd30d..f37586e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -896,6 +897,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	else
 		relation->trigdesc = NULL;
 
+	if (relation->rd_rel->relhasrowlevelsec)
+		RelationBuildRowLevelSecurity(relation);
+	else
+		relation->rlsdesc = NULL;
+
 	/*
 	 * if it's an index, initialize index-related information
 	 */
@@ -1785,6 +1791,8 @@ RelationDestroyRelation(Relation relation)
 		MemoryContextDelete(relation->rd_indexcxt);
 	if (relation->rd_rulescxt)
 		MemoryContextDelete(relation->rd_rulescxt);
+	if (relation->rlsdesc)
+		MemoryContextDelete(relation->rlsdesc->rlscxt);
 	pfree(relation);
 }
 
@@ -3024,7 +3032,13 @@ RelationCacheInitializePhase3(void)
 				relation->rd_rel->relhastriggers = false;
 			restart = true;
 		}
-
+		if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL)
+		{
+			RelationBuildRowLevelSecurity(relation);
+			if (relation->rlsdesc == NULL)
+				relation->rd_rel->relhasrowlevelsec = false;
+			restart = true;
+		}
 		/* Release hold on the relation */
 		RelationDecrementReferenceCount(relation);
 
@@ -4174,6 +4188,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_rules = NULL;
 		rel->rd_rulescxt = NULL;
 		rel->trigdesc = NULL;
+		rel->rlsdesc = NULL;
 		rel->rd_indexprs = NIL;
 		rel->rd_indpred = NIL;
 		rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9920d96..c23a324 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3867,6 +3867,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reloptions;
 	int			i_toastreloptions;
 	int			i_reloftype;
+	int			i_rlsqual;
 
 	/* Make sure we are in proper schema */
 	selectSourceSchema(fout, "pg_catalog");
@@ -3891,7 +3892,45 @@ getTables(Archive *fout, int *numTables)
 	 * we cannot correctly identify inherited columns, owned sequences, etc.
 	 */
 
-	if (fout->remoteVersion >= 90100)
+	if (fout->remoteVersion >= 90300)
+	{
+		/*
+		 * Left join to pick up dependency info linking sequences to their
+		 * owning column, if any (note this dependency is AUTO as of 8.2)
+		 */
+		appendPQExpBuffer(query,
+						  "SELECT c.tableoid, c.oid, c.relname, "
+						  "c.relacl, c.relkind, c.relnamespace, "
+						  "(%s c.relowner) AS rolname, "
+						  "c.relchecks, c.relhastriggers, "
+						  "c.relhasindex, c.relhasrules, c.relhasoids, "
+						  "c.relfrozenxid, tc.oid AS toid, "
+						  "tc.relfrozenxid AS tfrozenxid, "
+						  "c.relpersistence, "
+						  "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+						  "d.refobjid AS owning_tab, "
+						  "d.refobjsubid AS owning_col, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+						"array_to_string(c.reloptions, ', ') AS reloptions, "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual "
+						  "FROM pg_class c "
+						  "LEFT JOIN pg_depend d ON "
+						  "(c.relkind = '%c' AND "
+						  "d.classid = c.tableoid AND d.objid = c.oid AND "
+						  "d.objsubid = 0 AND "
+						  "d.refclassid = c.tableoid AND d.deptype = 'a') "
+					   "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+					   "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) "
+						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+						  "ORDER BY c.oid",
+						  username_subquery,
+						  RELKIND_SEQUENCE,
+						  RELKIND_RELATION, RELKIND_SEQUENCE,
+						  RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+						  RELKIND_FOREIGN_TABLE);
+	}
+	else if (fout->remoteVersion >= 90100)
 	{
 		/*
 		 * Left join to pick up dependency info linking sequences to their
@@ -3911,7 +3950,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL as rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3947,7 +3987,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3982,7 +4023,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4017,7 +4059,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4053,7 +4096,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4088,7 +4132,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4119,7 +4164,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4145,7 +4191,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4181,7 +4228,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "WHERE relkind IN ('%c', '%c') "
 						  "ORDER BY oid",
@@ -4229,6 +4277,7 @@ getTables(Archive *fout, int *numTables)
 	i_reloptions = PQfnumber(res, "reloptions");
 	i_toastreloptions = PQfnumber(res, "toast_reloptions");
 	i_reloftype = PQfnumber(res, "reloftype");
+	i_rlsqual = PQfnumber(res, "rlsqual");
 
 	if (lockWaitTimeout && fout->remoteVersion >= 70300)
 	{
@@ -4271,6 +4320,10 @@ getTables(Archive *fout, int *numTables)
 			tblinfo[i].reloftype = NULL;
 		else
 			tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype));
+		if (PQgetisnull(res, i, i_rlsqual))
+			tblinfo[i].rlsqual = NULL;
+		else
+			tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual));
 		tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
 		if (PQgetisnull(res, i, i_owning_tab))
 		{
@@ -12803,6 +12856,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 			}
 		}
 	}
+	if (tbinfo->rlsqual)
+		appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n",
+						  fmtId(tbinfo->dobj.name), tbinfo->rlsqual);
 
 	if (binary_upgrade)
 		binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2aa2060..5485101 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -256,6 +256,7 @@ typedef struct _tableInfo
 	uint32		toast_frozenxid;	/* for restore toast frozen xid */
 	int			ncheck;			/* # of CHECK expressions */
 	char	   *reloftype;		/* underlying type for typed table */
+	char	   *rlsqual;		/* row-level security policy */
 	/* these two are set only if table is a sequence owned by a column: */
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8499768..49baa0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
 	OCLASS_EVENT_TRIGGER,		/* pg_event_trigger */
+	OCLASS_ROWLEVELSEC,			/* pg_rowlevelsec */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 238fe58..a3b07e2 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
 DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
 #define RangeTypidIndexId					3542
 
+DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId				3839
+
 /* last step of initialization script: build the indexes declared above */
 BUILD_INDICES
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f83ce80..5b6e38b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
 	bool		relhaspkey;		/* has (or has had) PRIMARY KEY index */
 	bool		relhasrules;	/* has (or has had) any rules */
 	bool		relhastriggers; /* has (or has had) any TRIGGERs */
+	bool		relhasrowlevelsec;	/* has (or has had) row-level security */
 	bool		relhassubclass; /* has (or has had) derived classes */
 	TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
 
@@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class;
  * ----------------
  */
 
-#define Natts_pg_class					27
+#define Natts_pg_class					28
 #define Anum_pg_class_relname			1
 #define Anum_pg_class_relnamespace		2
 #define Anum_pg_class_reltype			3
@@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class;
 #define Anum_pg_class_relhaspkey		21
 #define Anum_pg_class_relhasrules		22
 #define Anum_pg_class_relhastriggers	23
-#define Anum_pg_class_relhassubclass	24
-#define Anum_pg_class_relfrozenxid		25
-#define Anum_pg_class_relacl			26
-#define Anum_pg_class_reloptions		27
+#define Anum_pg_class_relhasrowlevelsec	24
+#define Anum_pg_class_relhassubclass	25
+#define Anum_pg_class_relfrozenxid		26
+#define Anum_pg_class_relacl			27
+#define Anum_pg_class_reloptions		28
 
 /* ----------------
  *		initial contents of pg_class
@@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class;
  */
 
 /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
-DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
 
 
diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h
new file mode 100644
index 0000000..5a64d1b
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,60 @@
+/*
+ * pg_rowlevelsec.h
+ *   definition of the system catalog for row-level security policy
+ *   (pg_rowlevelsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWLEVELSEC_H
+#define PG_ROWLEVELSEC_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ *		pg_rowlevelsec definition. cpp turns this into
+ *		typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId	3838
+
+CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS
+{
+	Oid				rlsrelid;
+#ifdef CATALOG_VARLEN
+	pg_node_tree	rlsqual;
+#endif
+} FormData_pg_rowlevelsec;
+
+/* ----------------
+ *		Form_pg_rowlevelsec corresponds to a pointer to a row with
+ *		the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec;
+
+/* ----------------
+ * 		compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowlevelsec				2
+#define Anum_pg_rowlevelsec_rlsrelid		1
+#define Anum_pg_rowlevelsec_rlsqual			2
+
+typedef struct
+{
+	MemoryContext	rlscxt;
+	Expr		   *rlsqual;
+	bool			rlshassublinks;
+} RowLevelSecDesc;
+
+extern void	RelationBuildRowLevelSecurity(Relation relation);
+extern void	SetRowLevelSecurity(Relation relation, Node *clause);
+extern void	ResetRowLevelSecurity(Relation relation);
+extern void	RemoveRowLevelSecurityById(Oid relationId);
+
+#endif  /* PG_ROWLEVELSEC_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 09b15e7..4118794 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
 	QSRC_PARSER,				/* added by parse analysis (now unused) */
 	QSRC_INSTEAD_RULE,			/* added by unconditional INSTEAD rule */
 	QSRC_QUAL_INSTEAD_RULE,		/* added by conditional INSTEAD rule */
-	QSRC_NON_INSTEAD_RULE		/* added by non-INSTEAD rule */
+	QSRC_NON_INSTEAD_RULE,		/* added by non-INSTEAD rule */
+	QSRC_ROW_LEVEL_SECURITY,	/* added by row-level security */
 } QuerySource;
 
 /* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -1232,6 +1233,8 @@ typedef enum AlterTableType
 	AT_DropInherit,				/* NO INHERIT parent */
 	AT_AddOf,					/* OF <type_name> */
 	AT_DropOf,					/* NOT OF */
+	AT_SetRowLevelSecurity,		/* SET ROW LEVEL SECURITY (...) */
+	AT_ResetRowLevelSecurity,	/* RESET ROW LEVEL SECURITY */
 	AT_GenericOptions			/* OPTIONS (...) */
 } AlterTableType;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fb9a863..6b3ea3d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
 	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
 
 	int			nParamExec;		/* number of PARAM_EXEC Params used */
+
+	Oid			planUserId;		/* user-id this plan assumed, or InvalidOid */
 } PlannedStmt;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 2b2742d..f20923a 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
 	Index		lastRowMarkId;	/* highest PlanRowMark ID assigned */
 
 	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
+
+	Oid			planUserId;		/* User-Id to be assumed on this plan */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h
new file mode 100644
index 0000000..9afe00a
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,31 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlevelsec.h
+ *    prototypes for optimizer/rowlevelsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLEVELSEC_H
+#define ROWLEVELSEC_H
+
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(PlannerInfo *root,
+											 Relation relation);
+extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook;
+
+typedef enum {
+	RowLevelSecModeEnabled,
+	RowLevelSecModeDisabled,
+} RowLevelSecMode;
+
+extern RowLevelSecMode getRowLevelSecurityMode(void);
+extern void setRowLevelSecurityMode(RowLevelSecMode new_mode);
+
+extern void	applyRowLevelSecurity(PlannerInfo *root);
+
+#endif	/* ROWLEVELSEC_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index e3bb35f..d13ef32 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_INDEX_PREDICATE,		/* index predicate */
 	EXPR_KIND_ALTER_COL_TRANSFORM,	/* transform expr in ALTER COLUMN TYPE */
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
-	EXPR_KIND_TRIGGER_WHEN			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_TRIGGER_WHEN,			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_ROW_LEVEL_SEC,		/* policy of ROW LEVEL SECURITY */
 } ParseExprKind;
 
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e846..5f89028 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -115,6 +115,8 @@ typedef struct CachedPlan
 								 * bare utility statements) */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	Oid			planUserId;		/* is user-id that is assumed on this cached
+								   plan, or InvalidOid if portable for anybody */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4669d8a..dce8463 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_index.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "fmgr.h"
 #include "nodes/bitmapset.h"
 #include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
 	RuleLock   *rd_rules;		/* rewrite rules */
 	MemoryContext rd_rulescxt;	/* private memory cxt for rd_rules, if any */
 	TriggerDesc *trigdesc;		/* Trigger info, or NULL if rel has none */
+	RowLevelSecDesc	*rlsdesc;	/* Row-level security info, or NULL */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out
new file mode 100644
index 0000000..1658806
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,753 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+(16 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-C-A)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 220 |      2 | 0101      | this document is secret, category(-C-A)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+ 240 |      2 | 0111      | this document is secret, category(-CBA)
+(24 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document document_1
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Hash Join  (cost=30.34..57.95 rows=2 width=117)
+   Hash Cond: (document.did = browse.did)
+   ->  Seq Scan on document  (cost=8.27..31.15 rows=343 width=49)
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=4)
+                 Index Cond: (pguser = "current_user"())
+   ->  Hash  (cost=22.06..22.06 rows=1 width=72)
+         ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+               Filter: f_leak((browse.browse)::text)
+               ->  Seq Scan on browse browse_1  (cost=0.00..22.00 rows=4 width=168)
+                     Filter: (pguser = "current_user"())
+(12 rows)
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ERROR:  must be owner of relation document
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+ERROR:  must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document document_1
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Nested Loop  (cost=8.27..55.90 rows=1 width=117)
+   Join Filter: (document.did = browse.did)
+   ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+         Filter: f_leak((browse.browse)::text)
+         ->  Seq Scan on browse browse_1  (cost=0.00..22.00 rows=4 width=168)
+               Filter: (pguser = "current_user"())
+   ->  Seq Scan on document  (cost=8.27..33.72 rows=5 width=49)
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=9)
+                 Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  10 |      0 | 0000      | this document is unclassified, category(----) |                   | 
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  30 |      0 | 0010      | this document is unclassified, category(--B-) |                   | 
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+  90 |      1 | 0000      | this document is classified, category(----)   |                   | 
+ 100 |      1 | 0001      | this document is classified, category(---A)   |                   | 
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+ 120 |      1 | 0011      | this document is classified, category(--BA)   |                   | 
+ 170 |      2 | 0000      | this document is secret, category(----)       |                   | 
+ 180 |      2 | 0001      | this document is secret, category(---A)       |                   | 
+ 190 |      2 | 0010      | this document is secret, category(--B-)       |                   | 
+ 200 |      2 | 0011      | this document is secret, category(--BA)       |                   | 
+(12 rows)
+
+DELETE FROM document WHERE did = 30;			-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(30) is still referenced from table "browse".
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(90) is still referenced from table "browse".
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+SELECT * FROM t1;
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+SELECT * FROM t1 WHERE f_leak(c);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.c)
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.c)
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+               Filter: f_leak(c)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a |  c  
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a |  c  |   t1    
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  Seq Scan on t3 t3_1
+(10 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  LockRows
+                     ->  Seq Scan on t1 t1_1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  LockRows
+                     ->  Seq Scan on t2 t2_1
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3 t3_1
+(13 rows)
+
+SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.c)
+               ->  LockRows
+                     ->  Seq Scan on t1 t1_1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.c)
+               ->  LockRows
+                     ->  Seq Scan on t2 t2_1
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3 t3_1
+                           Filter: f_leak(c)
+(16 rows)
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+                     QUERY PLAN                     
+----------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a <= 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a <= 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(c);
+NOTICE:  f_leak => aaa
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => bcd
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => def
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  c  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+           QUERY PLAN            
+---------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: f_leak(c)
+         ->  Seq Scan on t2
+               Filter: f_leak(c)
+         ->  Seq Scan on t3
+               Filter: f_leak(c)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a |  c  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+           QUERY PLAN           
+--------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t2
+               Filter: (a <= 2)
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+          QUERY PLAN           
+-------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a = 2)
+         ->  Seq Scan on t2
+               Filter: (a = 2)
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a |  c  
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+                    QUERY PLAN                     
+---------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a = 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a = 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE:  drop cascades to 7 other objects
+DETAIL:  drop cascades to table account
+drop cascades to function f_leak(text)
+drop cascades to table document
+drop cascades to table browse
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 3f04442..ffcd8d3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
  pg_proc                 | t
  pg_range                | t
  pg_rewrite              | t
+ pg_rowlevelsec          | t
  pg_seclabel             | t
  pg_shdepend             | t
  pg_shdescription        | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(155 rows)
+(156 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f1a72f7..e0cef62 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: privileges security_label collate alter_generic
+test: privileges rowlevelsec security_label collate alter_generic
 
 test: misc
 # rules cannot run concurrently with any test that creates a view
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index f2813fb..1317015 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: privileges
+test: rowlevelsec
 test: security_label
 test: collate
 test: misc
diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql
new file mode 100644
index 0000000..a4d1ccd
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,237 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+DELETE FROM document WHERE did = 30;			-- failed
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, b text, c text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN b;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101	1	aaa
+102	2	bbb
+103	3	ccc
+104	4	ddd
+\.
+
+CREATE TABLE t2 (d float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201	1	abc	1.1
+202	2	bcd	2.2
+203	3	cde	3.3
+204	4	def	4.4
+\.
+
+CREATE TABLE t3 (e text) INHERITS (t1);
+COPY t3 FROM stdin WITH (oids);
+301	1	xxx	X
+302	2	yyy	Y
+303	3	zzz	Z
+\.
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c) FOR SHARE;
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(c);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(c);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
#23Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Kohei KaiGai (#22)
1 attachment(s)
Re: [v9.3] Row-Level Security

On 8 October 2012 15:57, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a refreshed version towards the latest master branch,
to fix up patch conflicts.
Here is no other difference from the previous revision.

Thanks,

I had another look at this over the weekend and I found couple of
additional problems (test cases attached):

1). It is possible to define a RLS qual that refers back to the table
that it's defined on, in such a way that causes infinite recursion in
the planner, giving "ERROR: stack depth limit exceeded". I think it
would be preferable to trap this and report a more meaningful error
back to the user, along similar lines to a self-referencing view.

2). In other cases it is possible to define a RLS qual that refers to
another table with a RLS qual in such a way that the second table's
RLS qual is not checked, thus allowing a user to bypass the security
check.

3). If a RLS qual refers to a view it errors, since the RLS quals are
added after rule expansion, and so the view is not rewritten.

To me this suggests that perhaps the expansion of RLS quals should be
done in the rewriter. I've not thought that through in any detail, but
ISTM that a RIR rule could add a table with a RLS qual, and a RLS qual
could add a relation with a RIR rule that needs expanding, and so the
2 need to be processed together. This could also make use of the
existing recursion-checking code in the rewriter.

Regards,
Dean

Attachments:

test.sqlapplication/octet-stream; name=test.sqlDownload
#24Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Dean Rasheed (#23)
Re: [v9.3] Row-Level Security

2012/10/8 Dean Rasheed <dean.a.rasheed@gmail.com>:

On 8 October 2012 15:57, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a refreshed version towards the latest master branch,
to fix up patch conflicts.
Here is no other difference from the previous revision.

Thanks,

I had another look at this over the weekend and I found couple of
additional problems (test cases attached):

1). It is possible to define a RLS qual that refers back to the table
that it's defined on, in such a way that causes infinite recursion in
the planner, giving "ERROR: stack depth limit exceeded". I think it
would be preferable to trap this and report a more meaningful error
back to the user, along similar lines to a self-referencing view.

2). In other cases it is possible to define a RLS qual that refers to
another table with a RLS qual in such a way that the second table's
RLS qual is not checked, thus allowing a user to bypass the security
check.

3). If a RLS qual refers to a view it errors, since the RLS quals are
added after rule expansion, and so the view is not rewritten.

To me this suggests that perhaps the expansion of RLS quals should be
done in the rewriter. I've not thought that through in any detail, but
ISTM that a RIR rule could add a table with a RLS qual, and a RLS qual
could add a relation with a RIR rule that needs expanding, and so the
2 need to be processed together. This could also make use of the
existing recursion-checking code in the rewriter.

Thanks for your checks. I missed some cases that you suggested.

The reason why we need to put RLS expansion at planner stage is
requirement towards plan cache invalidation. Due to special case
handling for superuser, plan cache has to be invalidated if user-id
to run executor was switched since planner stage. The planner shall
be invoked again, but not rewritter, on its invalidation.

Probably, it make sense to invoke rewriter's logic to solve RLS
policy from planner stage (that allows plan-cache invalidation).
Let me investigate the code of rewriter.

Best regards,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#25Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Kohei KaiGai (#24)
1 attachment(s)
Re: [v9.3] Row-Level Security

The revised patch fixes the problem that Daen pointed out.

In case when row-level security policy contains SubLink node,
it become to call the rewriter to expand views being contained
within SubLink, and also append qualifier of security policy
onto underlying tables. It enables to apply configured policy
on nested relations also, as if top-level ones.
In addition, I added a check for infinite recursion when two
different tables have row-level policy that references each
other.

So, for example, self-recursion RLS shall be prevented.
postgres=> ALTER TABLE foo SET ROW LEVEL SECURITY (a < (SELECT
max(a) FROM foo));
ALTER TABLE
postgres=> SELECT * FROM foo;
ERROR: infinite recursion detected for relation "foo"

It shows RLS policy is recursively applied.
postgres=> ALTER TABLE foo SET ROW LEVEL SECURITY (a > 0);
ALTER TABLE
postgres=> ALTER TABLE bar SET ROW LEVEL SECURITY
(EXISTS(SELECT 1 FROM foo WHERE foo.a = bar.a));
ALTER TABLE
postgres=> EXPLAIN SELECT * FROM bar;
QUERY PLAN
--------------------------------------------------------------------------------------------
Hash Join (cost=44.95..111.95 rows=1200 width=4)
Hash Cond: (bar.a = foo.a)
-> Seq Scan on bar (cost=0.00..34.00 rows=2400 width=4)
-> Hash (cost=42.45..42.45 rows=200 width=4)
-> HashAggregate (cost=40.45..42.45 rows=200 width=4)
-> Bitmap Heap Scan on foo (cost=10.45..30.45
rows=800 width=4)
Recheck Cond: (a > 0)
-> Bitmap Index Scan on foo_pkey
(cost=0.00..10.25 rows=800 width=0)
Index Cond: (a > 0)
(9 rows)

Even if RLS policy contains a view, it works fine.
postgres=> CREATE VIEW foo_v AS SELECT * FROM foo;
CREATE VIEW
postgres=> ALTER TABLE bar SET ROW LEVEL SECURITY
(a IN (SELECT * FROM foo_v));
ALTER TABLE
postgres=> EXPLAIN SELECT * FROM bar;
QUERY PLAN
--------------------------------------------------------------------------------------------
Hash Join (cost=44.95..111.95 rows=1200 width=4)
Hash Cond: (bar.a = foo.a)
-> Seq Scan on bar (cost=0.00..34.00 rows=2400 width=4)
-> Hash (cost=42.45..42.45 rows=200 width=4)
-> HashAggregate (cost=40.45..42.45 rows=200 width=4)
-> Bitmap Heap Scan on foo (cost=10.45..30.45
rows=800 width=4)
Recheck Cond: (a > 0)
-> Bitmap Index Scan on foo_pkey
(cost=0.00..10.25 rows=800 width=0)
Index Cond: (a > 0)
(9 rows)

Thanks,

2012/10/8 Kohei KaiGai <kaigai@kaigai.gr.jp>:

2012/10/8 Dean Rasheed <dean.a.rasheed@gmail.com>:

On 8 October 2012 15:57, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a refreshed version towards the latest master branch,
to fix up patch conflicts.
Here is no other difference from the previous revision.

Thanks,

I had another look at this over the weekend and I found couple of
additional problems (test cases attached):

1). It is possible to define a RLS qual that refers back to the table
that it's defined on, in such a way that causes infinite recursion in
the planner, giving "ERROR: stack depth limit exceeded". I think it
would be preferable to trap this and report a more meaningful error
back to the user, along similar lines to a self-referencing view.

2). In other cases it is possible to define a RLS qual that refers to
another table with a RLS qual in such a way that the second table's
RLS qual is not checked, thus allowing a user to bypass the security
check.

3). If a RLS qual refers to a view it errors, since the RLS quals are
added after rule expansion, and so the view is not rewritten.

To me this suggests that perhaps the expansion of RLS quals should be
done in the rewriter. I've not thought that through in any detail, but
ISTM that a RIR rule could add a table with a RLS qual, and a RLS qual
could add a relation with a RIR rule that needs expanding, and so the
2 need to be processed together. This could also make use of the
existing recursion-checking code in the rewriter.

Thanks for your checks. I missed some cases that you suggested.

The reason why we need to put RLS expansion at planner stage is
requirement towards plan cache invalidation. Due to special case
handling for superuser, plan cache has to be invalidated if user-id
to run executor was switched since planner stage. The planner shall
be invoked again, but not rewritter, on its invalidation.

Probably, it make sense to invoke rewriter's logic to solve RLS
policy from planner stage (that allows plan-cache invalidation).
Let me investigate the code of rewriter.

Best regards,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

Attachments:

pgsql-v9.3-row-level-security.ro.v5.patchapplication/octet-stream; name=pgsql-v9.3-row-level-security.ro.v5.patchDownload
 doc/src/sgml/catalogs.sgml                 |  59 ++
 doc/src/sgml/ref/alter_table.sgml          |  38 ++
 doc/src/sgml/user-manag.sgml               | 123 ++++
 src/backend/access/transam/xact.c          |  12 +
 src/backend/catalog/Makefile               |   4 +-
 src/backend/catalog/dependency.c           |  23 +
 src/backend/catalog/heap.c                 |   1 +
 src/backend/catalog/pg_rowlevelsec.c       | 241 +++++++
 src/backend/commands/tablecmds.c           |  64 ++
 src/backend/optimizer/plan/planner.c       |  12 +
 src/backend/optimizer/util/Makefile        |   2 +-
 src/backend/optimizer/util/rowlevelsec.c   | 977 +++++++++++++++++++++++++++++
 src/backend/parser/gram.y                  |  16 +
 src/backend/parser/parse_agg.c             |   6 +
 src/backend/parser/parse_expr.c            |   3 +
 src/backend/rewrite/rewriteHandler.c       |  16 +
 src/backend/utils/adt/ri_triggers.c        |  14 +
 src/backend/utils/cache/plancache.c        |  32 +
 src/backend/utils/cache/relcache.c         |  17 +-
 src/bin/pg_dump/pg_dump.c                  |  76 ++-
 src/bin/pg_dump/pg_dump.h                  |   1 +
 src/include/catalog/dependency.h           |   1 +
 src/include/catalog/indexing.h             |   3 +
 src/include/catalog/pg_class.h             |  20 +-
 src/include/catalog/pg_rowlevelsec.h       |  60 ++
 src/include/nodes/parsenodes.h             |   5 +-
 src/include/nodes/plannodes.h              |   2 +
 src/include/nodes/relation.h               |   2 +
 src/include/optimizer/rowlevelsec.h        |  31 +
 src/include/parser/parse_node.h            |   3 +-
 src/include/rewrite/rewriteHandler.h       |   1 +
 src/include/utils/plancache.h              |   2 +
 src/include/utils/rel.h                    |   2 +
 src/test/regress/expected/rowlevelsec.out  | 853 +++++++++++++++++++++++++
 src/test/regress/expected/sanity_check.out |   3 +-
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/serial_schedule           |   1 +
 src/test/regress/sql/rowlevelsec.sql       | 273 ++++++++
 38 files changed, 2974 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index f999190..55898a5 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
      </row>
 
      <row>
+      <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+      <entry>row-level security policy of relation</entry>
+     </row>
+
+     <row>
       <entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
       <entry>security labels on database objects</entry>
      </row>
@@ -1807,6 +1812,16 @@
      </row>
 
      <row>
+      <entry><structfield>relhasrowlevelsec</structfield></entry>
+      <entry><type>bool</type></entry>
+      <entry></entry>
+      <entry>
+       True if table has row-level security policy; see
+       <link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link> catalog
+      </entry>
+     </row>
+
+     <row>
       <entry><structfield>relhassubclass</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
@@ -4906,6 +4921,50 @@
 
  </sect1>
 
+ <sect1 id="catalog-pg-rowlevelsec">
+  <title><structname>pg_rowlevelsec</structname></title>
+
+  <indexterm zone="catalog-pg-rowlevelsec">
+   <primary>pg_rowlevelsec</primary>
+  </indexterm>
+  <para>
+   The catalog <structname>pg_rowlevelsec</structname> expression tree of
+   row-level security policy to be performed on a particular relation.
+  </para>
+  <table>
+   <title><structname>pg_rowlevelsec</structname> Columns</title>
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>rlsrelid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+      <entry>The table this row-level security is for</entry>
+     </row>
+     <row>
+      <entry><structfield>rlsqual</structfield></entry>
+      <entry><type>text</type></entry>
+      <entry></entry>
+      <entry>An expression tree to be performed as rowl-level security policy</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+  <note>
+   <para>
+    <literal>pg_class.relhasrowlevelsec</literal>
+    must be true if a table has row-level security policy in this catalog.
+   </para>
+  </note>
+ </sect1>
 
  <sect1 id="catalog-pg-seclabel">
   <title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 356419e..9255c45 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     NOT OF
     OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
     SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+    SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)
+    RESET ROW LEVEL SECURITY
 
 <phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
 
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
    </varlistentry>
 
    <varlistentry>
+    <term><literal>SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+    <listitem>
+     <para>
+      This form set row-level security policy of the table.
+      Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+      as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+      clause, although mechanism guarantees to evaluate this condition earlier
+      than any other user given condition.
+      See also <xref linkend="ROW-LEVEL-SECURITY">.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET ROW LEVEL SECURITY</literal></term>
+    <listitem>
+     <para>
+      This form reset row-level security policy of the table, if exists.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
@@ -808,6 +833,19 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="PARAMETER">condition</replaceable></term>
+      <listitem>
+       <para>
+        An expression that returns a value of type boolean. Expect for a case
+        when queries are executed with superuser privilege, only rows for which
+        this expression returns true will be fetched, updated or deleted.
+        This expression can reference columns of the relation being configured,
+        however, unavailable to include sub-query right now.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..c283e07 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,127 @@ DROP ROLE <replaceable>name</replaceable>;
   </para>
  </sect1>
 
+ <sect1 id="row-level-security">
+  <title>Row-level Security</title>
+  <para>
+   <productname>PostgreSQL</productname> v9.3 or later provides
+   row-level security feature, like several commercial database
+   management system. It allows table owner to assign a particular
+   condition that performs as a security policy of the table; only
+   rows that satisfies the condition should be visible, except for
+   a case when superuser runs queries.
+  </para>
+  <para>
+   Row-level security policy can be set using
+   <literal>SET ROW LEVEL SECURITY</literal> command of
+   <xref linkend="SQL-ALTERTABLE"> statement, as an expression
+    form that returns a value of type boolean. This expression can
+    contain references to columns of the relation, so it enables
+    to construct arbitrary rule to make access control decision
+    based on contents of each rows.
+  </para>
+  <para>
+   For example, the following <literal>customer</literal> table
+   has <literal>uname</literal> field to store user name, and
+   it assume we don't want to expose any properties of other
+   customers.
+   The following command set <literal>current_user = uname</literal>
+   as row-level security policy on the <literal>customer</literal>
+   table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+</screen>
+   <xref linkend="SQL-EXPLAIN"> command shows how row-level
+   security policy works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+                 QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+   Filter: f_leak(customer.upasswd)
+   ->  Seq Scan on customer
+         Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+   This query execution plan means the preconfigured row-level
+   security policy is implicitly added, and scan plan on the
+   target relation being wrapped up with a sub-query.
+   It ensures user given qualifiers, including functions with
+   side effects, are never executed earlier than the row-level
+   security policy regardless of its cost, except for the cases
+   when these were fully leakproof.
+   This design helps to tackle the scenario described in
+   <xref linkend="RULES-PRIVILEGES">; that introduces the order
+   to evaluate qualifiers is significant to keep confidentiality
+   of invisible rows.
+  </para>
+
+  <para>
+   On the other hand, this design allows superusers to bypass
+   checks with row-level security.
+   It ensures <application>pg_dump</application> can obtain
+   a complete set of database backup, and avoid to execute
+   Trojan horse trap, being injected as a row-level security
+   policy of user-defined table, with privileges of superuser.
+  </para>
+
+  <para>
+   In case of queries on inherited tables, row-level security
+   policy of the parent relation is not applied to child
+   relations. Scope of the row-level security policy is limited
+   to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+                QUERY PLAN
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.y)
+               ->  Seq Scan on t1
+                     Filter: ((x % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: f_leak(y)
+         ->  Subquery Scan on t3
+               Filter: f_leak(t3.y)
+               ->  Seq Scan on t3
+                     Filter: ((x % 2) = 1)
+(12 rows)
+</screen>
+    In the above example, <literal>t1</literal> has inherited
+    child table <literal>t2</literal> and <literal>t3</literal>,
+    and row-level security policy is set on only <literal>t1</literal>,
+    and <literal>t3</literal>, not <literal>t2</literal>.
+
+    The row-level security policy of <literal>t1</literal>,
+    <literal>x</literal> must be even-number, is appended only
+    <literal>t1</literal>, neither <literal>t2</literal> nor
+    <literal>t3</literal>. On the contrary, <literal>t3</literal>
+    has different row-level security policy; <literal>x</literal>
+    must be odd-number.
+  </para>
+
+  <para>
+   Right now, row-level security feature has several limitation,
+   although these shall be improved in the future version.
+
+   Row-level security policy is not applied to
+   <command>UPDATE</command> or <command>DELETE</command>
+   commands in this revision, even though it should be
+   supported soon.
+
+   Row-level security policy is not applied to rows to be inserted
+   and newer revision of updated rows, thus, it requires to
+   define before-row-insert or before-row-update trigger to check
+   whether the row's contents satisfies the policy individually.
+
+   Although it is not a specific matter in row-level security,
+   <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH">
+   allows to switch current user identifier during execution
+   of the query. Thus, it may cause unpredicated behavior
+   if and when <literal>current_user</literal> is used as
+   a part of row-level security policy.
+  </para>
+ </sect1>
 </chapter>
diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index e7a6606..851f9db 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -35,6 +35,7 @@
 #include "executor/spi.h"
 #include "libpq/be-fsstubs.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "pgstat.h"
 #include "replication/walsender.h"
 #include "replication/syncrep.h"
@@ -144,6 +145,7 @@ typedef struct TransactionStateData
 	int			maxChildXids;	/* allocated size of childXids[] */
 	Oid			prevUser;		/* previous CurrentUserId setting */
 	int			prevSecContext; /* previous SecurityRestrictionContext */
+	RowLevelSecMode	prevRowLevelSecMode;	/* previous RLS-mode setting */
 	bool		prevXactReadOnly;		/* entry-time xact r/o state */
 	bool		startedInRecovery;		/* did we start in recovery? */
 	struct TransactionStateData *parent;		/* back link to parent */
@@ -173,6 +175,7 @@ static TransactionStateData TopTransactionStateData = {
 	0,							/* allocated size of childXids[] */
 	InvalidOid,					/* previous CurrentUserId setting */
 	0,							/* previous SecurityRestrictionContext */
+	RowLevelSecModeEnabled,		/* previous RowLevelSecMode setting */
 	false,						/* entry-time xact r/o state */
 	false,						/* startedInRecovery */
 	NULL						/* link to parent state block */
@@ -1764,6 +1767,8 @@ StartTransaction(void)
 	/* SecurityRestrictionContext should never be set outside a transaction */
 	Assert(s->prevSecContext == 0);
 
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
+
 	/*
 	 * initialize other subsystems for new transaction
 	 */
@@ -2295,6 +2300,9 @@ AbortTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Also, mode setting of row-level security should be restored. */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * do abort processing
 	 */
@@ -4188,6 +4196,9 @@ AbortSubTransaction(void)
 	 */
 	SetUserIdAndSecContext(s->prevUser, s->prevSecContext);
 
+	/* Reset row-level security mode */
+	setRowLevelSecurityMode(s->prevRowLevelSecMode);
+
 	/*
 	 * We can skip all this stuff if the subxact failed before creating a
 	 * ResourceOwner...
@@ -4327,6 +4338,7 @@ PushTransaction(void)
 	s->state = TRANS_DEFAULT;
 	s->blockState = TBLOCK_SUBBEGIN;
 	GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext);
+	s->prevRowLevelSecMode = getRowLevelSecurityMode();
 	s->prevXactReadOnly = XactReadOnly;
 
 	CurrentTransactionState = s;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index df6da1f..965aa38 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
        objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \
        pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
        pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
-       pg_type.o storage.o toasting.o
+       pg_rowlevelsec.o pg_type.o storage.o toasting.o
 
 BKIFILES = postgres.bki postgres.description postgres.shdescription
 
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
-	pg_foreign_table.h \
+	pg_foreign_table.h pg_rowlevelsec.h \
 	pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
 	toasting.h indexing.h \
     )
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 87d6f02..f0736b1 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
@@ -1229,6 +1230,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveExtensionById(object->objectId);
 			break;
 
+		case OCLASS_ROWLEVELSEC:
+			RemoveRowLevelSecurityById(object->objectId);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized object class: %u",
 				 object->classId);
@@ -2280,6 +2285,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case EventTriggerRelationId:
 			return OCLASS_EVENT_TRIGGER;
+
+		case RowLevelSecurityRelationId:
+			return OCLASS_ROWLEVELSEC;
 	}
 
 	/* shouldn't get here */
@@ -2929,6 +2937,21 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_ROWLEVELSEC:
+			{
+				char	   *relname;
+
+				relname = get_rel_name(object->objectId);
+				if (!relname)
+					elog(ERROR, "cache lookup failed for relation %u",
+						 object->objectId);
+				appendStringInfo(&buffer,
+								 _("row-level security of %s"), relname);
+
+
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index c80df41..60de76d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -776,6 +776,7 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
 	values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
 	values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+	values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec);
 	values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	if (relacl != (Datum) 0)
diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c
new file mode 100644
index 0000000..e554f20
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,241 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlevelsec.c
+ *    routines to support manipulation of the pg_rowlevelsec relation
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+void
+RelationBuildRowLevelSecurity(Relation relation)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+
+	tuple = systable_getnext(sscan);
+	if (HeapTupleIsValid(tuple))
+	{
+		RowLevelSecDesc	*rlsdesc;
+		MemoryContext	rlscxt;
+		MemoryContext	oldcxt;
+		Datum	datum;
+		bool	isnull;
+		char   *temp;
+
+		/*
+		 * Make the private memory context to store RowLevelSecDesc that
+		 * includes expression tree also.
+		 */
+		rlscxt = AllocSetContextCreate(CacheMemoryContext,
+									   RelationGetRelationName(relation),
+									   ALLOCSET_SMALL_MINSIZE,
+									   ALLOCSET_SMALL_INITSIZE,
+									   ALLOCSET_SMALL_MAXSIZE);
+		PG_TRY();
+		{
+			datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual,
+								 RelationGetDescr(rlsrel), &isnull);
+			Assert(!isnull);
+			temp = TextDatumGetCString(datum);
+
+			oldcxt = MemoryContextSwitchTo(rlscxt);
+
+			rlsdesc = palloc0(sizeof(RowLevelSecDesc));
+			rlsdesc->rlscxt = rlscxt;
+			rlsdesc->rlsqual = (Expr *) stringToNode(temp);
+			Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID);
+
+			rlsdesc->rlshassublinks
+				= contain_subplans((Node *)rlsdesc->rlsqual);
+
+			MemoryContextSwitchTo(oldcxt);
+
+			pfree(temp);
+		}
+		PG_CATCH();
+		{
+			MemoryContextDelete(rlscxt);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		relation->rlsdesc = rlsdesc;
+	}
+	else
+	{
+		relation->rlsdesc = NULL;
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, AccessShareLock);
+}
+
+void
+SetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid				relationId = RelationGetRelid(relation);
+	ParseState	   *pstate;
+	RangeTblEntry  *rte;
+	Node		   *qual;
+	Relation		rlsrel;
+	ScanKeyData		skey;
+	SysScanDesc		sscan;
+	HeapTuple		oldtup;
+	HeapTuple		newtup;
+	Datum			values[Natts_pg_rowlevelsec];
+	bool			isnull[Natts_pg_rowlevelsec];
+	bool			replaces[Natts_pg_rowlevelsec];
+	ObjectAddress	target;
+	ObjectAddress	myself;
+
+	/* Parse the supplied clause */
+	pstate = make_parsestate(NULL);
+
+	rte = addRangeTableEntryForRelation(pstate, relation,
+										NULL, false, false);
+	addRTEtoQuery(pstate, rte, false, true, true);
+
+	qual = transformWhereClause(pstate, copyObject(clause),
+								EXPR_KIND_ROW_LEVEL_SEC,
+								"ROW LEVEL SECURITY");
+	/* zero-clear */
+	memset(values,   0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(isnull,   0, sizeof(isnull));
+
+	/* Update or Indert an entry to pg_rowlevelsec catalog  */
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	oldtup = systable_getnext(sscan);
+	if (HeapTupleIsValid(oldtup))
+	{
+		replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true;
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+
+		newtup = heap_modify_tuple(oldtup,
+								   RelationGetDescr(rlsrel),
+								   values, isnull, replaces);
+		simple_heap_update(rlsrel, &newtup->t_self, newtup);
+
+		deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+								   relationId, false);
+	}
+	else
+	{
+		values[Anum_pg_rowlevelsec_rlsrelid - 1]
+			= ObjectIdGetDatum(relationId);
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+		newtup = heap_form_tuple(RelationGetDescr(rlsrel),
+								 values, isnull);
+		simple_heap_insert(rlsrel, newtup);
+	}
+	CatalogUpdateIndexes(rlsrel, newtup);
+
+	heap_freetuple(newtup);
+
+	/* records dependencies of RLS-policy and relation/columns */
+	target.classId = RelationRelationId;
+	target.objectId = relationId;
+	target.objectSubId = 0;
+
+	myself.classId = RowLevelSecurityRelationId;
+	myself.objectId = relationId;
+	myself.objectSubId = 0;
+
+	recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+	recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+						   DEPENDENCY_NORMAL);
+	free_parsestate(pstate);
+
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+void
+ResetRowLevelSecurity(Relation relation)
+{
+	if (relation->rlsdesc)
+	{
+		ObjectAddress	address;
+
+		address.classId = RowLevelSecurityRelationId;
+		address.objectId = RelationGetRelid(relation);
+		address.objectSubId = 0;
+
+		performDeletion(&address, DROP_RESTRICT, 0);
+	}
+	else
+	{
+		/* Nothing to do here */
+		elog(INFO, "relation %s has no row-level security policy, skipped",
+			 RelationGetRelationName(relation));
+	}
+}
+
+/*
+ * Guts of Row-level security policy deletion.
+ */
+void
+RemoveRowLevelSecurityById(Oid relationId)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relationId));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+	{
+		simple_heap_delete(rlsrel, &tuple->t_self);
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 359d478..83ca86b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -384,6 +385,7 @@ static void ATExecDropInherit(Relation rel, RangeVar *parent, LOCKMODE lockmode)
 static void drop_parent_dependency(Oid relid, Oid refclassid, Oid refobjid);
 static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockmode);
 static void ATExecDropOf(Relation rel, LOCKMODE lockmode);
+static void ATExecSetRowLevelSecurity(Relation relation, Node *clause);
 static void ATExecGenericOptions(Relation rel, List *options);
 
 static void copy_relation_data(SMgrRelation rel, SMgrRelation dst,
@@ -2746,6 +2748,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_SetTableSpace:		/* must rewrite heap */
 			case AT_DropNotNull:		/* may change some SQL plans */
 			case AT_SetNotNull:
+			case AT_SetRowLevelSecurity:
+			case AT_ResetRowLevelSecurity:
 			case AT_GenericOptions:
 			case AT_AlterColumnGenericOptions:
 				cmd_lockmode = AccessExclusiveLock;
@@ -3108,6 +3112,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_DropInherit:	/* NO INHERIT */
 		case AT_AddOf:			/* OF */
 		case AT_DropOf: /* NOT OF */
+		case AT_SetRowLevelSecurity:
+		case AT_ResetRowLevelSecurity:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* These commands never recurse */
 			/* No command-specific prep needed */
@@ -3383,6 +3389,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropOf:
 			ATExecDropOf(rel, lockmode);
 			break;
+		case AT_SetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, (Node *) cmd->def);
+			break;
+		case AT_ResetRowLevelSecurity:
+			ATExecSetRowLevelSecurity(rel, NULL);
+			break;
 		case AT_GenericOptions:
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
@@ -7533,6 +7545,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				Assert(defaultexpr);
 				break;
 
+			case OCLASS_ROWLEVELSEC:
+				/*
+				 * A row-level security policy can depend on a column in case
+				 * when the policy clause references a particular column.
+				 * Due to same reason why TRIGGER ... WHEN does not support
+				 * to change column's type being referenced in clause, row-
+				 * level security policy also does not support it.
+				 */
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used in a row-level security policy"),
+						 errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject),
+								   colName)));
+				break;
+
 			case OCLASS_PROC:
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
@@ -9631,6 +9659,42 @@ ATExecDropOf(Relation rel, LOCKMODE lockmode)
 }
 
 /*
+ * ALTER TABLE <name> SET ROW LEVEL SECURITY (...) OR
+ *                    RESET ROW LEVEL SECURITY
+ */
+static void
+ATExecSetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid			relid = RelationGetRelid(relation);
+	Relation	class_rel;
+	HeapTuple	tuple;
+	Form_pg_class	class_form;
+
+	if (clause != NULL)
+		SetRowLevelSecurity(relation, clause);
+	else
+		ResetRowLevelSecurity(relation);
+
+	class_rel = heap_open(RelationRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	class_form = (Form_pg_class) GETSTRUCT(tuple);
+	if (clause != NULL)
+		class_form->relhasrowlevelsec = true;
+	else
+		class_form->relhasrowlevelsec = false;
+
+	simple_heap_update(class_rel, &tuple->t_self, tuple);
+	CatalogUpdateIndexes(class_rel, tuple);
+
+	heap_close(class_rel, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
+/*
  * ALTER FOREIGN TABLE <name> OPTIONS (...)
  */
 static void
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b61005f..ad8b9ad 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/analyze.h"
@@ -167,6 +168,16 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	glob->lastPHId = 0;
 	glob->lastRowMarkId = 0;
 	glob->transientPlan = false;
+	glob->planUserId = InvalidOid;
+	/*
+	 * XXX - a valid user-id shall be set on planUserId later, if constructed
+	 * plan assumes being executed under privilege of a particular user-id.
+	 * Elsewhere, keep InvalidOid; that means the constructed plan is portable
+	 * for any users.
+	 * Please also note that it is also the reason why row-level security
+	 * needs to be placed on top of planner stage, instead of rewriter.
+	 */
+	applyRowLevelSecurity(glob, parse);
 
 	/* Determine what fraction of the plan is likely to be scanned */
 	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -244,6 +255,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->nParamExec = glob->nParamExec;
+	result->planUserId = glob->planUserId;
 
 	return result;
 }
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3430689 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
-       relnode.o restrictinfo.o tlist.o var.o
+       relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c
new file mode 100644
index 0000000..664c993
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,977 @@
+/*
+ * optimizer/util/rowlvsec.c
+ *    Row-level security support routines
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* flags to pull row-level security policy */
+#define RLS_FLAG_HAS_SUBLINKS			0x0001
+
+/* hook to allow extensions to apply their own security policy */
+rowlevel_security_hook_type	rowlevel_security_hook = NULL;
+
+/* current performing mode of row-level security policy */
+static RowLevelSecMode	rowlevel_security_mode = RowLevelSecModeEnabled;
+
+static bool	apply_subquery_rowlevelsec(Query *parse, List *active_relids);
+
+/*
+ * getRowLevelSecurityMode / setRowLevelSecurityMode
+ *
+ * These functions allow to get or set current performing mode of row-
+ * level security feature. It enables to disable this feature temporarily
+ * for some cases in which row-level security prevent correct behavior
+ * such as foreign-key checks to prohibit update of PKs being referenced
+ * by others.
+ * The caller must ensure the saved previous mode shall be restored, but
+ * no need to care about cases when an error would be raised.
+ */
+RowLevelSecMode
+getRowLevelSecurityMode(void)
+{
+	return rowlevel_security_mode;
+}
+
+void
+setRowLevelSecurityMode(RowLevelSecMode new_mode)
+{
+	rowlevel_security_mode = new_mode;
+}
+
+/*
+ * check_and_fixup_sublink
+ *
+ * This routine calls query rewriter to handle sub-query of sub-link
+ * within row-level security policy, and ensure it does not contain
+ * any infinite reference loop.
+ */
+typedef struct {
+	bool	has_rowlevel_sec;
+	bool	exec_query_rewriter;
+	List   *active_relids;
+} fixup_sublink_context;
+
+static bool
+fixup_sublink_walker(Node *node, fixup_sublink_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, SubLink))
+	{
+		SubLink	   *sl = (SubLink *) node;
+
+		Assert(IsA(sl->subselect, Query));
+
+		if (context->exec_query_rewriter)
+			QueryRewriteExpr((Node *) sl, context->active_relids);
+
+		if (apply_subquery_rowlevelsec((Query *)sl->subselect,
+									   context->active_relids))
+			context->has_rowlevel_sec = true;
+	}
+	return expression_tree_walker(node, fixup_sublink_walker,
+								  (void *) context);
+}
+
+/*
+ * make_pseudo_subquery
+ *
+ * This routine makes a sub-query that references the target relation
+ * with given row-level security policy. This sub-query shall have
+ * security_barrier attribute to prevent unexpected push-down.
+ */
+static Query *
+make_pseudo_subquery(RangeTblEntry *rte,
+					 Oid relid,
+					 Node *qual,
+					 int flags,
+					 RowMarkClause *rclause)
+{
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+
+	subqry = makeNode(Query);
+	subqry->commandType = CMD_SELECT;
+	subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	subrte = makeNode(RangeTblEntry);
+	subrte->rtekind = RTE_RELATION;
+	subrte->relid = relid;
+	subrte->relkind = get_rel_relkind(relid);
+	subrte->inFromCl = true;
+	subrte->requiredPerms = 0;
+	if (rte->relid == relid)
+	{
+		subrte->requiredPerms = rte->requiredPerms;
+		subrte->selectedCols = bms_copy(rte->selectedCols);
+		subrte->modifiedCols = bms_copy(rte->modifiedCols);
+	}
+	else
+	{
+		/*
+		 * selectedCols and modifiedCols should be adjusted
+		 * according to child table definition, because it
+		 * may have original attribute-number in comparison
+		 * with the parent table.
+		 */
+		AttrNumber	attidx;
+		AttrNumber	attnum;
+		char	   *attname;
+		Bitmapset  *attrs_used = bms_union(rte->selectedCols,
+										   rte->modifiedCols);
+		while ((attidx = bms_first_member(attrs_used)) >= 0)
+		{
+			attnum = attidx + FirstLowInvalidHeapAttributeNumber;
+			if (attnum != InvalidAttrNumber)
+			{
+				attname = get_relid_attribute_name(rte->relid, attnum);
+				attnum = get_attnum(relid, attname);
+				if (attnum == InvalidAttrNumber)
+					elog(ERROR, "Not exist column \"%s\" of relation \"%s\"",
+						 attname, get_rel_name(relid));
+				pfree(attname);
+			}
+			attnum -= FirstLowInvalidHeapAttributeNumber;
+			if (bms_is_member(attidx, rte->selectedCols))
+				bms_add_member(subrte->selectedCols, attnum);
+			if (bms_is_member(attidx, rte->modifiedCols))
+				bms_add_member(subrte->modifiedCols, attnum);
+		}
+	}
+	subrte->alias = makeAlias(get_rel_name(relid), NIL);
+	subrte->eref = makeAlias(get_rel_name(relid), NIL);
+
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		subqry->hasSubLinks = true;
+
+	subqry->rtable = list_make1(subrte);
+
+	subrtr = makeNode(RangeTblRef);
+	subrtr->rtindex = 1;
+	subqry->jointree = makeFromExpr(list_make1(subrtr), qual);
+
+	if (rclause)
+	{
+		RowMarkClause  *rclause_sub;
+
+		rclause_sub = copyObject(rclause);
+		rclause_sub->rti = 1;
+
+		subqry->rowMarks = list_make1(rclause_sub);
+		subqry->hasForUpdate = true;
+	}
+	return subqry;
+}
+
+/*
+ * append_pseudo_column
+ *
+ * this routine construct a TargetEntry object that reference a certain
+ * column of the underlying table, then append it on targetList of the
+ * given RangeTblEntry (should be RTE_SUBQUERY).
+ */
+static TargetEntry *
+append_pseudo_column(RangeTblEntry *rte, Oid base_relid, AttrNumber attnum)
+{
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	Value		   *colname;
+
+	Assert(rte->rtekind == RTE_SUBQUERY);
+	Assert(list_length(rte->subquery->rtable) == 1);
+	Assert(rt_fetch(1, rte->subquery->rtable)->rtekind == RTE_RELATION);
+
+	subqry = rte->subquery;
+	subrte = rt_fetch(1, subqry->rtable);
+
+	/* Special case for whole-row-reference is required */
+	if (attnum == InvalidAttrNumber)
+	{
+		ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);
+
+		r->arg = (Expr *) makeVar((Index) 1,
+								  InvalidAttrNumber,
+								  get_rel_type_id(subrte->relid),
+								  -1,
+								  InvalidOid,
+								  0);
+		r->resulttype = get_rel_type_id(base_relid);
+		r->convertformat = COERCE_IMPLICIT_CAST;
+		r->location = -1;
+
+		subtle = makeTargetEntry((Expr *) r,
+								 list_length(subqry->targetList) + 1,
+								 get_rel_name(subrte->relid),
+								 false);
+	}
+	else
+	{
+		Form_pg_attribute attform;
+		HeapTuple	tuple;
+		Var		   *var;
+
+		/*
+		 * If the TargetEntry is not for base relaion, we need to
+		 * adjust attribute number to be used.
+		 */
+		if (attnum > 0 && subrte->relid != base_relid)
+		{
+			char   *attname = get_relid_attribute_name(base_relid, attnum);
+
+			attnum = get_attnum(subrte->relid, attname);
+			if (attnum == InvalidAttrNumber)
+				elog(ERROR, "column \"%s\" of relation \"%s\" does not exist",
+					 attname, get_rel_name(subrte->relid));
+			pfree(attname);
+		}
+
+		tuple = SearchSysCache2(ATTNUM,
+								ObjectIdGetDatum(subrte->relid),
+								Int16GetDatum(attnum));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for column %d of relation %u",
+				 attnum, subrte->relid);
+		attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+		var = makeVar((Index) 1,
+					  attform->attnum,
+					  attform->atttypid,
+					  attform->atttypmod,
+					  attform->attcollation,
+					  0);
+		subtle = makeTargetEntry((Expr *)var,
+								 list_length(subqry->targetList) + 1,
+								 pstrdup(NameStr(attform->attname)),
+								 false);
+		ReleaseSysCache(tuple);
+	}
+	subqry->targetList = lappend(subqry->targetList, subtle);
+
+	colname = makeString(pstrdup(subtle->resname));
+	rte->eref->colnames = lappend(rte->eref->colnames, colname);
+
+	return subtle;
+}
+
+/*
+ * lookup_pseudo_column
+ *
+ * This routine looks up attribute number of pseudo column being associated
+ * with the given attribute number of the underlying table.
+ * If not in target-list of the sub-query, it append a new target-entry.
+ */
+static AttrNumber
+lookup_pseudo_column(RangeTblEntry *rte, AttrNumber attnum)
+{
+	Query		   *subqry = rte->subquery;
+	ListCell	   *cell;
+	TargetEntry	   *subtle;
+	Oid				base_relid;
+
+	Assert(rte->rtekind == RTE_SUBQUERY);
+	Assert(list_length(subqry->rtable) == 1);
+	Assert(rt_fetch(1, subqry->rtable)->rtekind == RTE_RELATION);
+	base_relid = rt_fetch(1, subqry->rtable)->relid;
+
+	foreach (cell, subqry->targetList)
+	{
+		subtle = lfirst(cell);
+
+		if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+			 attnum == InvalidAttrNumber) ||
+			(IsA(subtle->expr, Var) &&
+			 attnum == ((Var *)subtle->expr)->varattno))
+			return subtle->resno;
+	}
+	subtle = append_pseudo_column(rte, base_relid, attnum);
+
+	return subtle->resno;
+}
+
+/*
+ * fixup_setop_walker
+ *
+ * it appends information about newly added pseudo column on
+ * SetOperationStmt tree of the pseudo sub-query.
+ */
+static void
+fixup_setop_walker(Node *setop, Oid coltypeid, int coltypemod)
+{
+	if (IsA(setop, RangeTblRef))
+		return;
+	else if (IsA(setop, SetOperationStmt))
+	{
+		SetOperationStmt *s = (SetOperationStmt *) setop;
+
+		s->colTypes = lappend_oid(s->colTypes, coltypeid);
+		s->colTypmods = lappend_oid(s->colTypmods, coltypemod);
+		s->colCollations = lappend_oid(s->colCollations, InvalidOid);
+	}
+	else
+		elog(ERROR, "unexpected node type: %d", (int) nodeTag(setop));
+}
+
+/*
+ * lookup_pseudo_column_multi
+ *
+ * This routine looks up attribute number of pseudo column being associated
+ * with the given attribute number of the underlying table.
+ * If not in target-list of the sub-query, it append a new target-entry.
+ * Unlike lookup_pseudo_column, it assumes the given RangeTblEntry has
+ * multiple sub-queries underlying simple table scan.
+ */
+static AttrNumber
+lookup_pseudo_column_multi(RangeTblEntry *rte, AttrNumber attnum)
+{
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	Var			   *subvar;
+	ListCell	   *cell;
+	Value		   *colname;
+	Oid				base_relid;
+	Oid				coltypeid = InvalidOid;
+	int				coltypemod = -1;
+	AttrNumber		resno = -1;
+	char		   *resname = NULL;
+
+	Assert(rte->rtekind == RTE_SUBQUERY);
+	Assert(list_length(rte->subquery->rtable) > 1);
+
+	/*
+	 * First, it looks up target-list of the given sub-query.
+	 */
+	subrte = rt_fetch(1, rte->subquery->rtable);
+	Assert(subrte->rtekind == RTE_SUBQUERY);
+
+	subqry = subrte->subquery;
+	Assert(list_length(subqry->rtable) == 1);
+	Assert(rt_fetch(1, subqry->rtable)->rtekind == RTE_RELATION);
+	base_relid = rt_fetch(1, subqry->rtable)->relid;
+
+	foreach (cell, rte->subquery->targetList)
+	{
+		TargetEntry	*tle = lfirst(cell);
+		Var *var = (Var *)tle->expr;
+
+		Assert(IsA(var, Var));
+		Assert(var->varattno <= list_length(subqry->targetList));
+
+		subtle = list_nth(subqry->targetList, var->varattno - 1);
+		if ((IsA(subtle->expr, ConvertRowtypeExpr) &&
+			 attnum == InvalidAttrNumber) ||
+			(IsA(subtle->expr, Var) &&
+			 attnum == ((Var *)subtle->expr)->varattno))
+			return tle->resno;
+	}
+
+	/*
+	 * Second, append a TargetEntry to reference the required attribute
+	 * of the underlying tables. Note that the underlying tables are
+	 * connected with UNION ALL, target entries must have exactly same
+	 * resource number and data type.
+	 */
+	foreach (cell, rte->subquery->rtable)
+	{
+		subrte = (RangeTblEntry *) lfirst(cell);
+		subtle = append_pseudo_column(subrte, base_relid, attnum);
+
+		if (cell == list_head(rte->subquery->rtable))
+		{
+			resno = subtle->resno;
+			resname = pstrdup(subtle->resname);
+
+			if (IsA(subtle->expr, ConvertRowtypeExpr))
+			{
+				ConvertRowtypeExpr *r = (ConvertRowtypeExpr *) subtle->expr;
+
+				coltypeid = r->resulttype;
+				coltypemod = -1;
+			}
+			else if (IsA(subtle->expr, Var))
+			{
+				Var	*v = (Var *) subtle->expr;
+
+				coltypeid = v->vartype;
+				coltypemod = v->vartypmod;
+			}
+			else
+				elog(ERROR, "unexpected node type: %d",
+					 (int)nodeTag(subtle->expr));
+		}
+		else
+			Assert(subtle->resno == resno);
+	}
+	/* fixup SetOperationStmt */
+	fixup_setop_walker(rte->subquery->setOperations, coltypeid, coltypemod);
+
+	/*
+	 * Not only underlying sub-queries, the base sub-qury also need to
+	 * have Var entry within the target list.
+	 */
+	subvar = makeVar((Index) 1,
+					 resno,
+					 coltypeid,
+					 coltypemod,
+					 InvalidOid,
+					 0);
+	subtle = makeTargetEntry((Expr *)subvar, resno, resname, false);
+	rte->subquery->targetList = lappend(rte->subquery->targetList, subtle);
+	colname = makeString(pstrdup(resname));
+	rte->eref->colnames = lappend(rte->eref->colnames, colname);
+
+	Assert(list_length(rte->subquery->targetList) == resno);
+
+	return resno;
+}
+
+/*
+ * fixup_varattnos
+ *
+ * It fixes up varattno of Var node that referenced the relation with
+ * RLS policy, thus replaced to a sub-query. Here is no guarantee the
+ * varattno matches with TargetEntry's resno of the sub-query, so needs
+ * to adjust them.
+ */
+typedef struct {
+	Query  *parse;
+	Index	varno;
+	int		varlevelsup;
+} fixup_varattnos_context;
+
+static bool
+fixup_varattnos_walker(Node *node, fixup_varattnos_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var            *var = (Var *)node;
+		RangeTblEntry  *rte;
+
+		/*
+		 * Does this Var node reference the Query node currently we focused
+		 * on. If not, we simply ignore it.
+		 */
+		if (var->varlevelsup != context->varlevelsup ||
+			var->varno != context->varno)
+			return false;
+
+		rte = rt_fetch(var->varno, context->parse->rtable);
+		if (!rte)
+			elog(ERROR, "invalid varno %d", var->varno);
+
+		Assert(rte->rtekind == RTE_SUBQUERY);
+		Assert(rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY);
+
+		if (list_length(rte->subquery->rtable) == 1)
+			var->varattno = lookup_pseudo_column(rte, var->varattno);
+		else
+			var->varattno = lookup_pseudo_column_multi(rte, var->varattno);
+
+		return false;
+	}
+	else if (IsA(node, Query))
+	{
+		bool    result;
+
+		context->varlevelsup++;
+		result = query_tree_walker((Query *) node,
+								   fixup_varattnos_walker,
+								   (void *) context, 0);
+		context->varlevelsup--;
+
+		return result;
+	}
+	return expression_tree_walker(node,
+								  fixup_varattnos_walker,
+								  (void *) context);
+}
+
+/*
+ * expand_range_table
+ *
+ * It replaces the given table reference by a simple sub-query.
+ */
+static void
+expand_range_table(RangeTblEntry *rte, RowMarkClause *rclause,
+				   Node *qual, int flags)
+{
+	Query		   *subqry;
+
+	subqry = make_pseudo_subquery(rte, rte->relid, qual, flags, rclause);
+
+	/* no permission checks are needed to subquery itself */
+	rte->requiredPerms = 0;
+	rte->checkAsUser = InvalidOid;
+	rte->selectedCols = NULL;
+	rte->modifiedCols = NULL;
+
+	rte->eref = makeAlias(get_rel_name(rte->relid), NIL);
+	rte->alias = makeAlias(get_rel_name(rte->relid), NIL);
+
+	rte->relid = InvalidOid;
+	rte->rtekind = RTE_SUBQUERY;
+	rte->subquery = subqry;
+	rte->security_barrier = (!qual ? false : true);
+	rte->inh = false;
+}
+
+/*
+ * expand_range_table_multi
+ *
+ * It replaces the given table references by an alternative sub-query that
+ * also contains multiple simple sub-queries.
+ */
+static void
+expand_range_table_multi(RangeTblEntry *rte, RowMarkClause *rclause,
+						 List *rls_relids, List *rls_quals, List *rls_flags)
+{
+	Query	   *baseqry;
+	SetOperationStmt *setop = NULL;
+	RangeTblEntry *subrte;
+	RangeTblRef	*subrtr;
+	ListCell   *cell1;
+	ListCell   *cell2;
+	ListCell   *cell3;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+	Assert(rte->relid == linitial_oid(rls_relids));
+	Assert(list_length(rls_relids) == list_length(rls_quals));
+	Assert(list_length(rls_relids) == list_length(rls_flags));
+	Assert(list_length(rls_relids) > 1);
+
+	/*
+	 * baseqry is the sub-query that should replaces reference to
+	 * the target relation, and will also have multiple sub-queries
+	 * to scan the relations with row-level security policy.
+	 */
+	baseqry = makeNode(Query);
+	baseqry->commandType = CMD_SELECT;
+	baseqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	forthree (cell1, rls_relids, cell2, rls_quals, cell3, rls_flags)
+	{
+		Oid		relid = lfirst_oid(cell1);
+		Node   *qual  = lfirst(cell2);
+		int		flags = lfirst_int(cell3);
+
+		/*
+		 * Construct a pseudo sub-query corresponding to the table
+		 * to be applied, then chain it with base-query.
+		 * If no security policy is configured, here is no reason
+		 * whey the sub-query should be security-barrier.
+		 */
+		subrte = makeNode(RangeTblEntry);
+		subrte->rtekind = RTE_SUBQUERY;
+		subrte->subquery
+			= make_pseudo_subquery(rte, relid, qual, flags, rclause);
+		subrte->security_barrier = (!qual ? false : true);
+		subrte->alias = makeAlias(get_rel_name(relid), NIL);
+		subrte->eref = makeAlias(get_rel_name(relid), NIL);
+
+		baseqry->rtable = lappend(baseqry->rtable, subrte);
+
+		/*
+		 * Also, we need to set up SetOperationStmt to connect pseudo
+		 * sub-queries according to simple UNION ALL.
+		 */
+		subrtr = makeNode(RangeTblRef);
+		subrtr->rtindex = list_length(baseqry->rtable);
+		if (!setop)
+		{
+			setop = makeNode(SetOperationStmt);
+			setop->op = SETOP_UNION;
+			setop->all = true;
+			setop->larg = (Node *)subrtr;
+			setop->rarg = NULL;
+		}
+		else if (!setop->rarg)
+		{
+			Assert(setop->larg != NULL);
+
+			setop->rarg = (Node *)subrtr;
+		}
+		else
+		{
+			SetOperationStmt   *setop_next;
+
+			Assert(setop->larg != NULL && setop->rarg != NULL);
+
+			setop_next = makeNode(SetOperationStmt);
+			setop_next->op = SETOP_UNION;
+			setop_next->all = true;
+			setop_next->larg = (Node *) setop;
+			setop_next->rarg = (Node *) subrtr;
+
+			setop = setop_next;
+		}
+	}
+	baseqry->setOperations = (Node *) setop;
+
+	/* no permission checks are needed to subquery itself */
+	rte->requiredPerms = 0;
+	rte->checkAsUser = InvalidOid;
+	rte->selectedCols = NULL;
+	rte->modifiedCols = NULL;
+	rte->eref = makeAlias(get_rel_name(rte->relid), NIL);
+	rte->alias = makeAlias(get_rel_name(rte->relid), NIL);
+
+	rte->relid = InvalidOid;
+	rte->rtekind = RTE_SUBQUERY;
+	rte->subquery = baseqry;
+	rte->security_barrier = false;
+}
+
+/*
+ * apply_relation_rowlevelsec
+ *
+ * This routine pulls row-level security policy of the target relaion
+ * given as range table entry.
+ */
+static bool
+apply_relation_rowlevelsec(Query *parse, RangeTblEntry *rte, Index rtindex,
+						   List *active_relids)
+{
+	Relation		rel;
+	LOCKMODE		lockmode;
+	List		   *relid_list = NIL;
+	List		   *quals_list = NIL;
+	List		   *flags_list = NIL;
+	ListCell	   *cell;
+	bool			result = false;
+
+	Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+	/*
+	 * XXX - Right now, we don't care about result relation of
+	 * UPDATE or DELETE statement. So, ignore it simply.
+	 */
+	if (parse->resultRelation == rtindex)
+		return false;
+
+	if (!rte->inh)
+	{
+		lockmode = NoLock;
+		relid_list = list_make1_oid(rte->relid);
+	}
+	else
+	{
+		/*
+		 * In case when the target relation may have inheritances,
+		 * we need to suggest an appropriate lock mode because it
+		 * is the first time to reference these tables in a series
+		 * of processes. For more details, see the comments in
+		 * expand_inherited_tables.
+		 * Also note that it does not guarantee the locks on child
+		 * tables being already acquired at expand_inherited_tables,
+		 * because row-level security routines can be bypassed if
+		 * RowLevelSecModeDisabled.
+		 */
+		if (rtindex == parse->resultRelation)
+			lockmode = RowExclusiveLock;
+		else
+		{
+			foreach (cell, parse->rowMarks)
+			{
+				Assert(IsA(lfirst(cell), RowMarkClause));
+				if (((RowMarkClause *) lfirst(cell))->rti == rtindex)
+					break;
+			}
+			if (cell)
+				lockmode = RowShareLock;
+			else
+				lockmode = AccessShareLock;
+		}
+		relid_list = find_all_inheritors(rte->relid, lockmode, NULL);
+	}
+
+	/*
+	 * Try to fetch row-level security policy of the target relation
+	 * or its children.
+	 */
+	foreach (cell, relid_list)
+	{
+		Expr	   *qual = NULL;
+		int			flags = 0;
+
+		rel = heap_open(lfirst_oid(cell), lockmode);
+
+		/*
+		 * Recursion check to prevent infinite recursion.
+		 */
+		if (list_member_oid(active_relids, RelationGetRelid(rel)))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("infinite recursion detected for relation \"%s\"",
+							RelationGetRelationName(rel))));
+
+		/*
+		 * Pull out row-level security policy configured with built-in
+		 * features, if unprivileged users. Please note that superuser
+		 * can bypass it.
+		 */
+		if (rel->rlsdesc && !superuser())
+		{
+			RowLevelSecDesc	*rlsdesc = rel->rlsdesc;
+
+			qual = copyObject(rlsdesc->rlsqual);
+			if (rlsdesc->rlshassublinks)
+				flags |= RLS_FLAG_HAS_SUBLINKS;
+		}
+
+		/*
+		 * Also, ask extensions whether they want to apply their own
+		 * row-level security policy. If both built-in and extension
+		 * has their own policy, it shall be merged.
+		 */
+		if (rowlevel_security_hook)
+		{
+			List   *temp;
+
+			temp = (*rowlevel_security_hook)(rel);
+			if (temp != NIL)
+			{
+				if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+					contain_subplans((Node *) temp))
+					flags |= RLS_FLAG_HAS_SUBLINKS;
+
+				if (qual != NULL)
+					temp = lappend(temp, qual);
+
+				if (list_length(temp) == 1)
+					qual = (Expr *)list_head(temp);
+				else
+					qual = makeBoolExpr(AND_EXPR, temp, -1);
+			}
+		}
+
+		/*
+		 * If either built-in or extension row-level security policy
+		 * is configured at least, we determine this table reference
+		 * should be replaced with an alternative sub-query.
+		 */
+		if (qual)
+		{
+			/* SubLink should be also extracted */
+			if (flags & RLS_FLAG_HAS_SUBLINKS)
+			{
+				fixup_sublink_context	context;
+
+				memset(&context, 0, sizeof(fixup_sublink_context));
+
+				context.exec_query_rewriter = true;
+				context.active_relids = lappend_oid(list_copy(active_relids),
+													RelationGetRelid(rel));
+				fixup_sublink_walker((Node *)qual, (void *) &context);
+
+				list_free(context.active_relids);
+			}
+			result = true;
+		}
+		quals_list = lappend(quals_list, qual);
+		flags_list = lappend_int(flags_list, flags);
+
+		heap_close(rel, NoLock);  /* close the relation, but keep locks */
+	}
+
+	if (result)
+	{
+		RowMarkClause *rclause = NULL;
+
+		/*
+		 * Push-down row-level lock of the target relation, since sub-query
+		 * does not support FOR SHARE/FOR UPDATE locks being assigned.
+		 */
+		foreach (cell, parse->rowMarks)
+		{
+			if (((RowMarkClause *) lfirst(cell))->rti == rtindex)
+			{
+				rclause = lfirst(cell);
+				parse->rowMarks = list_delete(parse->rowMarks, rclause);
+				break;
+			}
+		}
+
+		/*
+		 * If no children, a range table entry of relation is replaced
+		 * with a simple sub-query. On the other hand, it is replaced
+		 * with a sub-query that also contains simple but multiple sub-
+		 * queries being connected with UNION ALL.
+		 */
+		if (list_length(relid_list) == 1)
+			expand_range_table(rte, rclause,
+							   linitial(quals_list),
+							   linitial_int(flags_list));
+		else
+			expand_range_table_multi(rte, rclause,
+									 relid_list, quals_list, flags_list);
+	}
+	list_free(relid_list);
+	list_free(quals_list);
+	list_free(flags_list);
+
+	return result;
+}
+
+/*
+ * apply_subquery_rowlevelsec
+ *
+ * This routine applies row-level security policy of the table being
+ * referenced in the given sub- or toplevel- query recursively.
+ */
+static bool
+apply_subquery_rowlevelsec(Query *parse, List *active_relids)
+{
+	Index		rtindex = 0;
+	bool		has_rowlevel_sec = false;
+	ListCell   *cell;
+
+	foreach (cell, parse->rtable)
+	{
+		RangeTblEntry  *rte = lfirst(cell);
+		rtindex++;
+
+		if (rte->rtekind == RTE_RELATION)
+		{
+			if (apply_relation_rowlevelsec(parse, rte, rtindex,
+										   active_relids))
+			{
+				/*
+				 * If the referenced relation was replaced with
+				 * a sub-query, varattno of Var node needs to be
+				 * adjusted according to the target-list of the
+				 * pseudo sub-query.
+				 */
+				fixup_varattnos_context	context;
+
+				context.parse = parse;
+				context.varno = rtindex;
+				context.varlevelsup = 0;
+
+				query_tree_walker(parse, fixup_varattnos_walker,
+								  (void *) &context, 0);
+
+				has_rowlevel_sec = true;
+			}
+		}
+		else if (rte->rtekind == RTE_SUBQUERY)
+		{
+			if (apply_subquery_rowlevelsec(rte->subquery, active_relids))
+				has_rowlevel_sec = true;
+		}
+	}
+
+	/*
+	 * If the given parse tree contains SubLink node, we need to walk
+	 * into the parse tree to extract them in addition to the range-
+	 * table entries.
+	 * Query->hasSubLinks gives a hint for optimization to skip it.
+	 */
+	if (parse->hasSubLinks)
+	{
+		fixup_sublink_context	context;
+
+		memset(&context, 0, sizeof(fixup_sublink_context));
+		context.active_relids = active_relids;
+
+		query_tree_walker(parse, fixup_sublink_walker,
+						  (void *) &context,
+						  QTW_IGNORE_RANGE_TABLE);
+		if (context.has_rowlevel_sec)
+			has_rowlevel_sec = true;
+	}
+	return has_rowlevel_sec;
+}
+
+/*
+ * applyRowLevelSecurity
+ *
+ * It applies row-level security policy of the relation, if a table has
+ * its security policy, reference to the table shall be replaced by sub-
+ * query that simply scan the table being replaced with security policy
+ * as qualifiers. Of course, security-barrier flag is also added to the
+ * sub-query to prevent unintentional push-down of malicious functions
+ * being executed prior to evaluation of security policy.
+ * In the result, all user can see is rows of the relation that satisfies
+ * the qualifier supplied as security policy.
+ *
+ * One complex stuff is table reference with inherited child tables.
+ * In case when the give parse tree referenced a table with child tables,
+ * and one of them has row-level security, reference to the parent table
+ * also shall be replaced by a sub-query that contains multiple queries
+ * connected with UNION ALL.
+ *
+ * For example, if table t1 has t2 and t3 as children, security policy
+ * of t1 is (x > 10) and t3 is (x % 2 = 0), the following query
+ *   SELECT * FROM t1 WHERE f_leak(y)
+ * should be rewritten as if
+ *   SELECT * FROM (
+ *     SELECT x, y FROM only t1 WHERE x > 10
+ *     UNION ALL
+ *     SELECT x, y FROM only t2
+ *     UNION ALL
+ *     SELECT x, y FROM only t3 WHERE x % 2 = 0
+ *   ) t1 WHERE f_leak(y);
+ * would be given. It is identical with how table inheritance performs on.
+ *
+ * Row-level security policy can contain SubLink, thus, it allows to refer
+ * another relation to make access control decision. Even if it contains
+ * view, it shall be extracted to the normal form. In addition, row-level
+ * security policy of underlying tables are also applied recursively.
+ */
+void
+applyRowLevelSecurity(PlannerGlobal *glob, Query *parse)
+{
+	/* mode checks */
+	if (rowlevel_security_mode == RowLevelSecModeDisabled)
+		return;
+
+	/*
+	 * Extract row-level security policy of the underlying tables
+	 * recursively, and replace the references by sub-query with
+	 * security-barrier and qualifiers to filter out rows.
+	 * Plan has to be invalidated if different user-id was applied
+	 * on executor stage.
+	 */
+	if (apply_subquery_rowlevelsec(parse, NIL))
+	{
+		PlanInvalItem  *pi_item;
+
+		glob->planUserId = GetUserId();
+
+		pi_item = makeNode(PlanInvalItem);
+		pi_item->cacheId = AUTHOID;
+		pi_item->hashValue =
+			GetSysCacheHashValue1(AUTHOID,
+								  ObjectIdGetDatum(glob->planUserId));
+		glob->invalItems = lappend(glob->invalItems, pi_item);
+	}
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e4ff76e..a3d445d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2127,6 +2127,22 @@ alter_table_cmd:
 					n->def = (Node *)$2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> SET ROW LEVEL SECURITY (expression) */
+			| SET ROW LEVEL SECURITY '(' a_expr ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_SetRowLevelSecurity;
+					n->def = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> RESET ROW LEVEL SECURITY */
+			| RESET ROW LEVEL SECURITY
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_ResetRowLevelSecurity;
+					n->def = NULL;
+					$$ = (Node *)n;
+				}
 			| alter_generic_options
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index b75b2d9..9494889 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("aggregate functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("aggregate functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("window functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("window functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e9267c5..94c25d1 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1443,6 +1443,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
+		case EXPR_KIND_ROW_LEVEL_SEC:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2609,6 +2610,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "EXECUTE";
 		case EXPR_KIND_TRIGGER_WHEN:
 			return "WHEN";
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			return "ROW LEVEL SECURITY";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 8f75948..f08fcd8 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2245,3 +2245,19 @@ QueryRewrite(Query *parsetree)
 
 	return results;
 }
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+	fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 97e68b1..19a2255 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -43,6 +43,7 @@
 #include "parser/parse_coerce.h"
 #include "parser/parse_relation.h"
 #include "miscadmin.h"
+#include "optimizer/rowlevelsec.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
@@ -2999,6 +3000,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
+	RowLevelSecMode	save_rls_mode;
 	Datum		vals[RI_MAX_NUMKEYS * 2];
 	char		nulls[RI_MAX_NUMKEYS * 2];
 
@@ -3081,6 +3083,15 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
 						   save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
 
+	/*
+	 * Disabled row-level security in case when foreign-key relation is
+	 * queried to check existence of tupls that references the tuple to
+	 * be modified on the primary-key side.
+	 */
+	save_rls_mode = getRowLevelSecurityMode();
+	if (source_is_pk)
+		setRowLevelSecurityMode(RowLevelSecModeDisabled);
+
 	/* Finally we can run the query. */
 	spi_result = SPI_execute_snapshot(qplan,
 									  vals, nulls,
@@ -3090,6 +3101,9 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	/* Restore UID and security context */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
 
+	/* Restore row-level security performing mode */
+	setRowLevelSecurityMode(save_rls_mode);
+
 	/* Check result */
 	if (spi_result < 0)
 		elog(ERROR, "SPI_execute_snapshot returned %d", spi_result);
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 8c0391f..36a8750 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -51,6 +51,7 @@
 #include "catalog/namespace.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
@@ -665,6 +666,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
 		AcquireExecutorLocks(plan->stmt_list, true);
 
 		/*
+		 * If plan was constructed with assumption of a particular user-id,
+		 * and it is different from the current one, the cached-plan shall
+		 * be invalidated to construct suitable query plan.
+		 */
+		if (plan->is_valid &&
+			OidIsValid(plan->planUserId) &&
+			plan->planUserId == GetUserId())
+			plan->is_valid = false;
+
+		/*
 		 * If plan was transient, check to see if TransactionXmin has
 		 * advanced, and if so invalidate it.
 		 */
@@ -716,6 +727,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 {
 	CachedPlan *plan;
 	List	   *plist;
+	ListCell   *cell;
+	Oid			planUserId = InvalidOid;
 	bool		snapshot_set;
 	bool		spi_pushed;
 	MemoryContext plan_context;
@@ -794,6 +807,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	PopOverrideSearchPath();
 
 	/*
+	 * Check whether the generated plan assumes a particular user-id, or not.
+	 * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+	 * it should be kept and used to validation check of the cached plan
+	 * under the "current" user-id.
+	 */
+	foreach (cell, plist)
+	{
+		PlannedStmt	*pstmt = lfirst(cell);
+
+		if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+		{
+			Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+			planUserId = pstmt->planUserId;
+		}
+	}
+
+	/*
 	 * Make a dedicated memory context for the CachedPlan and its subsidiary
 	 * data.  It's probably not going to be large, but just in case, use the
 	 * default maxsize parameter.  It's transient for the moment.
@@ -828,6 +859,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->context = plan_context;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->planUserId = planUserId;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8c9ebe0..28ba831 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -896,6 +897,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	else
 		relation->trigdesc = NULL;
 
+	if (relation->rd_rel->relhasrowlevelsec)
+		RelationBuildRowLevelSecurity(relation);
+	else
+		relation->rlsdesc = NULL;
+
 	/*
 	 * if it's an index, initialize index-related information
 	 */
@@ -1785,6 +1791,8 @@ RelationDestroyRelation(Relation relation)
 		MemoryContextDelete(relation->rd_indexcxt);
 	if (relation->rd_rulescxt)
 		MemoryContextDelete(relation->rd_rulescxt);
+	if (relation->rlsdesc)
+		MemoryContextDelete(relation->rlsdesc->rlscxt);
 	pfree(relation);
 }
 
@@ -3024,7 +3032,13 @@ RelationCacheInitializePhase3(void)
 				relation->rd_rel->relhastriggers = false;
 			restart = true;
 		}
-
+		if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL)
+		{
+			RelationBuildRowLevelSecurity(relation);
+			if (relation->rlsdesc == NULL)
+				relation->rd_rel->relhasrowlevelsec = false;
+			restart = true;
+		}
 		/* Release hold on the relation */
 		RelationDecrementReferenceCount(relation);
 
@@ -4162,6 +4176,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_rules = NULL;
 		rel->rd_rulescxt = NULL;
 		rel->trigdesc = NULL;
+		rel->rlsdesc = NULL;
 		rel->rd_indexprs = NIL;
 		rel->rd_indpred = NIL;
 		rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index dd2019a..e18d2f4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3867,6 +3867,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reloptions;
 	int			i_toastreloptions;
 	int			i_reloftype;
+	int			i_rlsqual;
 
 	/* Make sure we are in proper schema */
 	selectSourceSchema(fout, "pg_catalog");
@@ -3891,7 +3892,45 @@ getTables(Archive *fout, int *numTables)
 	 * we cannot correctly identify inherited columns, owned sequences, etc.
 	 */
 
-	if (fout->remoteVersion >= 90100)
+	if (fout->remoteVersion >= 90300)
+	{
+		/*
+		 * Left join to pick up dependency info linking sequences to their
+		 * owning column, if any (note this dependency is AUTO as of 8.2)
+		 */
+		appendPQExpBuffer(query,
+						  "SELECT c.tableoid, c.oid, c.relname, "
+						  "c.relacl, c.relkind, c.relnamespace, "
+						  "(%s c.relowner) AS rolname, "
+						  "c.relchecks, c.relhastriggers, "
+						  "c.relhasindex, c.relhasrules, c.relhasoids, "
+						  "c.relfrozenxid, tc.oid AS toid, "
+						  "tc.relfrozenxid AS tfrozenxid, "
+						  "c.relpersistence, "
+						  "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+						  "d.refobjid AS owning_tab, "
+						  "d.refobjsubid AS owning_col, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+						"array_to_string(c.reloptions, ', ') AS reloptions, "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual "
+						  "FROM pg_class c "
+						  "LEFT JOIN pg_depend d ON "
+						  "(c.relkind = '%c' AND "
+						  "d.classid = c.tableoid AND d.objid = c.oid AND "
+						  "d.objsubid = 0 AND "
+						  "d.refclassid = c.tableoid AND d.deptype = 'a') "
+					   "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+					   "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) "
+						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+						  "ORDER BY c.oid",
+						  username_subquery,
+						  RELKIND_SEQUENCE,
+						  RELKIND_RELATION, RELKIND_SEQUENCE,
+						  RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+						  RELKIND_FOREIGN_TABLE);
+	}
+	else if (fout->remoteVersion >= 90100)
 	{
 		/*
 		 * Left join to pick up dependency info linking sequences to their
@@ -3911,7 +3950,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL as rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3947,7 +3987,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3982,7 +4023,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4017,7 +4059,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4053,7 +4096,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4088,7 +4132,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4119,7 +4164,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4145,7 +4191,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4181,7 +4228,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "WHERE relkind IN ('%c', '%c') "
 						  "ORDER BY oid",
@@ -4229,6 +4277,7 @@ getTables(Archive *fout, int *numTables)
 	i_reloptions = PQfnumber(res, "reloptions");
 	i_toastreloptions = PQfnumber(res, "toast_reloptions");
 	i_reloftype = PQfnumber(res, "reloftype");
+	i_rlsqual = PQfnumber(res, "rlsqual");
 
 	if (lockWaitTimeout && fout->remoteVersion >= 70300)
 	{
@@ -4271,6 +4320,10 @@ getTables(Archive *fout, int *numTables)
 			tblinfo[i].reloftype = NULL;
 		else
 			tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype));
+		if (PQgetisnull(res, i, i_rlsqual))
+			tblinfo[i].rlsqual = NULL;
+		else
+			tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual));
 		tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
 		if (PQgetisnull(res, i, i_owning_tab))
 		{
@@ -12803,6 +12856,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 			}
 		}
 	}
+	if (tbinfo->rlsqual)
+		appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n",
+						  fmtId(tbinfo->dobj.name), tbinfo->rlsqual);
 
 	if (binary_upgrade)
 		binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2aa2060..5485101 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -256,6 +256,7 @@ typedef struct _tableInfo
 	uint32		toast_frozenxid;	/* for restore toast frozen xid */
 	int			ncheck;			/* # of CHECK expressions */
 	char	   *reloftype;		/* underlying type for typed table */
+	char	   *rlsqual;		/* row-level security policy */
 	/* these two are set only if table is a sequence owned by a column: */
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8499768..49baa0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
 	OCLASS_EVENT_TRIGGER,		/* pg_event_trigger */
+	OCLASS_ROWLEVELSEC,			/* pg_rowlevelsec */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 238fe58..a3b07e2 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
 DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
 #define RangeTypidIndexId					3542
 
+DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId				3839
+
 /* last step of initialization script: build the indexes declared above */
 BUILD_INDICES
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f83ce80..5b6e38b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
 	bool		relhaspkey;		/* has (or has had) PRIMARY KEY index */
 	bool		relhasrules;	/* has (or has had) any rules */
 	bool		relhastriggers; /* has (or has had) any TRIGGERs */
+	bool		relhasrowlevelsec;	/* has (or has had) row-level security */
 	bool		relhassubclass; /* has (or has had) derived classes */
 	TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
 
@@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class;
  * ----------------
  */
 
-#define Natts_pg_class					27
+#define Natts_pg_class					28
 #define Anum_pg_class_relname			1
 #define Anum_pg_class_relnamespace		2
 #define Anum_pg_class_reltype			3
@@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class;
 #define Anum_pg_class_relhaspkey		21
 #define Anum_pg_class_relhasrules		22
 #define Anum_pg_class_relhastriggers	23
-#define Anum_pg_class_relhassubclass	24
-#define Anum_pg_class_relfrozenxid		25
-#define Anum_pg_class_relacl			26
-#define Anum_pg_class_reloptions		27
+#define Anum_pg_class_relhasrowlevelsec	24
+#define Anum_pg_class_relhassubclass	25
+#define Anum_pg_class_relfrozenxid		26
+#define Anum_pg_class_relacl			27
+#define Anum_pg_class_reloptions		28
 
 /* ----------------
  *		initial contents of pg_class
@@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class;
  */
 
 /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
-DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
 
 
diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h
new file mode 100644
index 0000000..5a64d1b
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,60 @@
+/*
+ * pg_rowlevelsec.h
+ *   definition of the system catalog for row-level security policy
+ *   (pg_rowlevelsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWLEVELSEC_H
+#define PG_ROWLEVELSEC_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ *		pg_rowlevelsec definition. cpp turns this into
+ *		typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId	3838
+
+CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS
+{
+	Oid				rlsrelid;
+#ifdef CATALOG_VARLEN
+	pg_node_tree	rlsqual;
+#endif
+} FormData_pg_rowlevelsec;
+
+/* ----------------
+ *		Form_pg_rowlevelsec corresponds to a pointer to a row with
+ *		the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec;
+
+/* ----------------
+ * 		compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowlevelsec				2
+#define Anum_pg_rowlevelsec_rlsrelid		1
+#define Anum_pg_rowlevelsec_rlsqual			2
+
+typedef struct
+{
+	MemoryContext	rlscxt;
+	Expr		   *rlsqual;
+	bool			rlshassublinks;
+} RowLevelSecDesc;
+
+extern void	RelationBuildRowLevelSecurity(Relation relation);
+extern void	SetRowLevelSecurity(Relation relation, Node *clause);
+extern void	ResetRowLevelSecurity(Relation relation);
+extern void	RemoveRowLevelSecurityById(Oid relationId);
+
+#endif  /* PG_ROWLEVELSEC_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 09b15e7..4118794 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
 	QSRC_PARSER,				/* added by parse analysis (now unused) */
 	QSRC_INSTEAD_RULE,			/* added by unconditional INSTEAD rule */
 	QSRC_QUAL_INSTEAD_RULE,		/* added by conditional INSTEAD rule */
-	QSRC_NON_INSTEAD_RULE		/* added by non-INSTEAD rule */
+	QSRC_NON_INSTEAD_RULE,		/* added by non-INSTEAD rule */
+	QSRC_ROW_LEVEL_SECURITY,	/* added by row-level security */
 } QuerySource;
 
 /* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -1232,6 +1233,8 @@ typedef enum AlterTableType
 	AT_DropInherit,				/* NO INHERIT parent */
 	AT_AddOf,					/* OF <type_name> */
 	AT_DropOf,					/* NOT OF */
+	AT_SetRowLevelSecurity,		/* SET ROW LEVEL SECURITY (...) */
+	AT_ResetRowLevelSecurity,	/* RESET ROW LEVEL SECURITY */
 	AT_GenericOptions			/* OPTIONS (...) */
 } AlterTableType;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fb9a863..6b3ea3d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
 	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
 
 	int			nParamExec;		/* number of PARAM_EXEC Params used */
+
+	Oid			planUserId;		/* user-id this plan assumed, or InvalidOid */
 } PlannedStmt;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 2b2742d..f20923a 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
 	Index		lastRowMarkId;	/* highest PlanRowMark ID assigned */
 
 	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
+
+	Oid			planUserId;		/* User-Id to be assumed on this plan */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h
new file mode 100644
index 0000000..a09e15c
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,31 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlevelsec.h
+ *    prototypes for optimizer/rowlevelsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLEVELSEC_H
+#define ROWLEVELSEC_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(Relation relation);
+extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook;
+
+typedef enum {
+	RowLevelSecModeEnabled,
+	RowLevelSecModeDisabled,
+} RowLevelSecMode;
+
+extern RowLevelSecMode getRowLevelSecurityMode(void);
+extern void setRowLevelSecurityMode(RowLevelSecMode new_mode);
+
+extern void	applyRowLevelSecurity(PlannerGlobal *root, Query *parse);
+
+#endif	/* ROWLEVELSEC_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index e3bb35f..d13ef32 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_INDEX_PREDICATE,		/* index predicate */
 	EXPR_KIND_ALTER_COL_TRANSFORM,	/* transform expr in ALTER COLUMN TYPE */
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
-	EXPR_KIND_TRIGGER_WHEN			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_TRIGGER_WHEN,			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_ROW_LEVEL_SEC,		/* policy of ROW LEVEL SECURITY */
 } ParseExprKind;
 
 
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 50625d4..d470cad 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
 #include "nodes/parsenodes.h"
 
 extern List *QueryRewrite(Query *parsetree);
+extern void	QueryRewriteExpr(Node *node, List *activeRIRs);
 extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
 extern Node *build_column_default(Relation rel, int attrno);
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e846..5f89028 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -115,6 +115,8 @@ typedef struct CachedPlan
 								 * bare utility statements) */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	Oid			planUserId;		/* is user-id that is assumed on this cached
+								   plan, or InvalidOid if portable for anybody */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4669d8a..dce8463 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_index.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "fmgr.h"
 #include "nodes/bitmapset.h"
 #include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
 	RuleLock   *rd_rules;		/* rewrite rules */
 	MemoryContext rd_rulescxt;	/* private memory cxt for rd_rules, if any */
 	TriggerDesc *trigdesc;		/* Trigger info, or NULL if rel has none */
+	RowLevelSecDesc	*rlsdesc;	/* Row-level security info, or NULL */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out
new file mode 100644
index 0000000..46921cf
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,853 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+(16 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-C-A)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is unclassified, category(-CBA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-C-A)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(-CBA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-C-A)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(-CBA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  60 |      0 | 0101      | this document is unclassified, category(-C-A)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  80 |      0 | 0111      | this document is unclassified, category(-CBA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 140 |      1 | 0101      | this document is classified, category(-C-A)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 160 |      1 | 0111      | this document is classified, category(-CBA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 220 |      2 | 0101      | this document is secret, category(-C-A)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+ 240 |      2 | 0111      | this document is secret, category(-CBA)
+(24 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document document_1
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Hash Join  (cost=30.34..57.95 rows=2 width=117)
+   Hash Cond: (document.did = browse.did)
+   ->  Seq Scan on document  (cost=8.27..31.15 rows=343 width=49)
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=4)
+                 Index Cond: (pguser = "current_user"())
+   ->  Hash  (cost=22.06..22.06 rows=1 width=72)
+         ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+               Filter: f_leak((browse.browse)::text)
+               ->  Seq Scan on browse browse_1  (cost=0.00..22.00 rows=4 width=168)
+                     Filter: (pguser = "current_user"())
+(12 rows)
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ERROR:  must be owner of relation document
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+ERROR:  must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(---A)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(--BA)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(---A)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(--BA)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(---A)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(--BA)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  20 |      0 | 0001      | this document is unclassified, category(---A)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  40 |      0 | 0011      | this document is unclassified, category(--BA)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 100 |      1 | 0001      | this document is classified, category(---A)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 120 |      1 | 0011      | this document is classified, category(--BA)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 180 |      2 | 0001      | this document is secret, category(---A)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 200 |      2 | 0011      | this document is secret, category(--BA)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user1,20,07-01-2012)
+NOTICE:  f_leak => (rls_regress_user1,40,07-02-2012)
+NOTICE:  f_leak => (rls_regress_user1,110,07-03-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+(3 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => this document is unclassified, category(----)
+NOTICE:  f_leak => this document is unclassified, category(--B-)
+NOTICE:  f_leak => this document is unclassified, category(-C--)
+NOTICE:  f_leak => this document is unclassified, category(-CB-)
+NOTICE:  f_leak => this document is classified, category(----)
+NOTICE:  f_leak => this document is classified, category(--B-)
+NOTICE:  f_leak => this document is classified, category(-C--)
+NOTICE:  f_leak => this document is classified, category(-CB-)
+NOTICE:  f_leak => this document is secret, category(----)
+NOTICE:  f_leak => this document is secret, category(--B-)
+NOTICE:  f_leak => this document is secret, category(-C--)
+NOTICE:  f_leak => this document is secret, category(-CB-)
+ did | dlevel | dcategory |                    dtitle                     
+-----+--------+-----------+-----------------------------------------------
+  10 |      0 | 0000      | this document is unclassified, category(----)
+  30 |      0 | 0010      | this document is unclassified, category(--B-)
+  50 |      0 | 0100      | this document is unclassified, category(-C--)
+  70 |      0 | 0110      | this document is unclassified, category(-CB-)
+  90 |      1 | 0000      | this document is classified, category(----)
+ 110 |      1 | 0010      | this document is classified, category(--B-)
+ 130 |      1 | 0100      | this document is classified, category(-C--)
+ 150 |      1 | 0110      | this document is classified, category(-CB-)
+ 170 |      2 | 0000      | this document is secret, category(----)
+ 190 |      2 | 0010      | this document is secret, category(--B-)
+ 210 |      2 | 0100      | this document is secret, category(-C--)
+ 230 |      2 | 0110      | this document is secret, category(-CB-)
+(12 rows)
+
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+NOTICE:  f_leak => (rls_regress_user2,30,07-04-2012)
+NOTICE:  f_leak => (rls_regress_user2,50,07-05-2012)
+NOTICE:  f_leak => (rls_regress_user2,90,07-06-2012)
+NOTICE:  f_leak => (rls_regress_user2,130,07-07-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,150,07-08-2012)
+NOTICE:  f_leak => (rls_regress_user2,190,07-09-2012)
+NOTICE:  f_leak => (rls_regress_user2,210,07-10-2012)
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  30 |      0 | 0010      | this document is unclassified, category(--B-) | rls_regress_user2 | 07-04-2012
+  50 |      0 | 0100      | this document is unclassified, category(-C--) | rls_regress_user2 | 07-05-2012
+  90 |      1 | 0000      | this document is classified, category(----)   | rls_regress_user2 | 07-06-2012
+ 130 |      1 | 0100      | this document is classified, category(-C--)   | rls_regress_user2 | 07-07-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 150 |      1 | 0110      | this document is classified, category(-CB-)   | rls_regress_user2 | 07-08-2012
+ 190 |      2 | 0010      | this document is secret, category(--B-)       | rls_regress_user2 | 07-09-2012
+ 210 |      2 | 0100      | this document is secret, category(-C--)       | rls_regress_user2 | 07-10-2012
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document document_1
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Nested Loop  (cost=8.27..55.90 rows=1 width=117)
+   Join Filter: (document.did = browse.did)
+   ->  Subquery Scan on browse  (cost=0.00..22.06 rows=1 width=72)
+         Filter: f_leak((browse.browse)::text)
+         ->  Seq Scan on browse browse_1  (cost=0.00..22.00 rows=4 width=168)
+               Filter: (pguser = "current_user"())
+   ->  Seq Scan on document  (cost=8.27..33.72 rows=5 width=49)
+         Filter: ((dcategory & $0) = B'0000'::"bit")
+         InitPlan 1 (returns $0)
+           ->  Index Scan using account_pkey on account  (cost=0.00..8.27 rows=1 width=9)
+                 Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+ did | dlevel | dcategory |                    dtitle                     |      pguser       |    ymd     
+-----+--------+-----------+-----------------------------------------------+-------------------+------------
+  10 |      0 | 0000      | this document is unclassified, category(----) |                   | 
+  20 |      0 | 0001      | this document is unclassified, category(---A) | rls_regress_user1 | 07-01-2012
+  30 |      0 | 0010      | this document is unclassified, category(--B-) |                   | 
+  40 |      0 | 0011      | this document is unclassified, category(--BA) | rls_regress_user1 | 07-02-2012
+  90 |      1 | 0000      | this document is classified, category(----)   |                   | 
+ 100 |      1 | 0001      | this document is classified, category(---A)   |                   | 
+ 110 |      1 | 0010      | this document is classified, category(--B-)   | rls_regress_user1 | 07-03-2012
+ 120 |      1 | 0011      | this document is classified, category(--BA)   |                   | 
+ 170 |      2 | 0000      | this document is secret, category(----)       |                   | 
+ 180 |      2 | 0001      | this document is secret, category(---A)       |                   | 
+ 190 |      2 | 0010      | this document is secret, category(--B-)       |                   | 
+ 200 |      2 | 0011      | this document is secret, category(--BA)       |                   | 
+(12 rows)
+
+DELETE FROM document WHERE did = 30;			-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(30) is still referenced from table "browse".
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+ERROR:  update or delete on table "document" violates foreign key constraint "browse_did_fkey" on table "browse"
+DETAIL:  Key (did)=(90) is still referenced from table "browse".
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+SELECT * FROM t1;
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.b)
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.b)
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+               Filter: f_leak(b)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a |  b  
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a |  b  |   t1    
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+             QUERY PLAN              
+-------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(7 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  LockRows
+                     ->  Seq Scan on t1 t1_1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  LockRows
+                     ->  Seq Scan on t2 t2_1
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3 t3_1
+(13 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.b)
+               ->  LockRows
+                     ->  Seq Scan on t1 t1_1
+                           Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.b)
+               ->  LockRows
+                     ->  Seq Scan on t2 t2_1
+                           Filter: ((a % 2) = 1)
+         ->  Subquery Scan on t3
+               ->  LockRows
+                     ->  Seq Scan on t3 t3_1
+                           Filter: f_leak(b)
+(16 rows)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion)
+ERROR:  infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW LEVEL SECURITY (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+NOTICE:  f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE:  f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a |                b                 
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+                                      QUERY PLAN                                       
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1  (cost=28.55..61.67 rows=205 width=36)
+   Filter: f_leak(s1.b)
+   ->  Hash Join  (cost=28.55..55.52 rows=615 width=36)
+         Hash Cond: (s1_1.a = s2.x)
+         ->  Seq Scan on s1 s1_1  (cost=0.00..22.30 rows=1230 width=36)
+         ->  Hash  (cost=28.54..28.54 rows=1 width=4)
+               ->  HashAggregate  (cost=28.53..28.54 rows=1 width=4)
+                     ->  Subquery Scan on s2  (cost=0.00..28.52 rows=1 width=4)
+                           Filter: (s2.y ~~ '%2f%'::text)
+                           ->  Seq Scan on s2 s2_1  (cost=0.00..28.45 rows=6 width=36)
+                                 Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from v2));		-- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+NOTICE:  f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE:  f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a  |                b                 
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+  6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Subquery Scan on s1
+   Filter: f_leak(s1.b)
+   ->  Hash Join
+         Hash Cond: (s1_1.a = s2.x)
+         ->  Seq Scan on s1 s1_1
+         ->  Hash
+               ->  HashAggregate
+                     ->  Subquery Scan on s2
+                           Filter: (s2.y ~~ '%af%'::text)
+                           ->  Seq Scan on s2 s2_1
+                                 Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x  |                y                 
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+  2 |  2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+                             QUERY PLAN                             
+--------------------------------------------------------------------
+ Subquery Scan on s2
+   Filter: (s2.y ~~ '%28%'::text)
+   ->  Seq Scan on s2 s2_1
+         Filter: ((x % 2) = 0)
+   SubPlan 1
+     ->  Limit
+           ->  Subquery Scan on s1
+                 ->  Nested Loop Semi Join
+                       Join Filter: (s1_1.a = s2_2.x)
+                       ->  Seq Scan on s1 s1_1
+                       ->  Materialize
+                             ->  Subquery Scan on s2_2
+                                   Filter: (s2_2.y ~~ '%af%'::text)
+                                   ->  Seq Scan on s2 s2_3
+                                         Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion via view)
+ERROR:  infinite recursion detected for relation "s1"
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+                     QUERY PLAN                     
+----------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a <= 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a <= 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE:  f_leak => aaa
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => bcd
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => def
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+           QUERY PLAN            
+---------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: f_leak(b)
+         ->  Seq Scan on t2
+               Filter: f_leak(b)
+         ->  Seq Scan on t3
+               Filter: f_leak(b)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a |  b  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+           QUERY PLAN           
+--------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t2
+               Filter: (a <= 2)
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+          QUERY PLAN           
+-------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a = 2)
+         ->  Seq Scan on t2
+               Filter: (a = 2)
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+                    QUERY PLAN                     
+---------------------------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: ((a = 2) AND ((a % 2) = 0))
+         ->  Seq Scan on t2
+               Filter: ((a = 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE:  drop cascades to 10 other objects
+DETAIL:  drop cascades to table account
+drop cascades to function f_leak(text)
+drop cascades to table document
+drop cascades to table browse
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 3f04442..ffcd8d3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
  pg_proc                 | t
  pg_range                | t
  pg_rewrite              | t
+ pg_rowlevelsec          | t
  pg_seclabel             | t
  pg_shdepend             | t
  pg_shdescription        | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(155 rows)
+(156 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 663bf8a..4c4552f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: privileges security_label collate
+test: privileges rowlevelsec security_label collate
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index be789e3..eea4c3e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: privileges
+test: rowlevelsec
 test: security_label
 test: collate
 test: misc
diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql
new file mode 100644
index 0000000..21def6e
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,273 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+CREATE TABLE account (
+	   pguser       name primary key,
+	   slevel		int,
+	   scategory	bit(4)
+);
+GRANT SELECT,REFERENCES ON TABLE account TO public;
+INSERT INTO account VALUES
+  ('rls_regress_user1', 1, B'0011'),
+  ('rls_regress_user2', 2, B'0110'),
+  ('rls_regress_user3', 0, B'0101');
+
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+	   COST 0.0000001 LANGUAGE plpgsql
+	   AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- Creation of Test Data
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE document (
+       did           int primary key,
+       dlevel        int,
+       dcategory     bit(4),
+	   dtitle        text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+  ( 10, 0, B'0000', 'this document is unclassified, category(----)'),
+  ( 20, 0, B'0001', 'this document is unclassified, category(---A)'),
+  ( 30, 0, B'0010', 'this document is unclassified, category(--B-)'),
+  ( 40, 0, B'0011', 'this document is unclassified, category(--BA)'),
+  ( 50, 0, B'0100', 'this document is unclassified, category(-C--)'),
+  ( 60, 0, B'0101', 'this document is unclassified, category(-C-A)'),
+  ( 70, 0, B'0110', 'this document is unclassified, category(-CB-)'),
+  ( 80, 0, B'0111', 'this document is unclassified, category(-CBA)'),
+  ( 90, 1, B'0000', 'this document is classified, category(----)'),
+  (100, 1, B'0001', 'this document is classified, category(---A)'),
+  (110, 1, B'0010', 'this document is classified, category(--B-)'),
+  (120, 1, B'0011', 'this document is classified, category(--BA)'),
+  (130, 1, B'0100', 'this document is classified, category(-C--)'),
+  (140, 1, B'0101', 'this document is classified, category(-C-A)'),
+  (150, 1, B'0110', 'this document is classified, category(-CB-)'),
+  (160, 1, B'0111', 'this document is classified, category(-CBA)'),
+  (170, 2, B'0000', 'this document is secret, category(----)'),
+  (180, 2, B'0001', 'this document is secret, category(---A)'),
+  (190, 2, B'0010', 'this document is secret, category(--B-)'),
+  (200, 2, B'0011', 'this document is secret, category(--BA)'),
+  (210, 2, B'0100', 'this document is secret, category(-C--)'),
+  (220, 2, B'0101', 'this document is secret, category(-C-A)'),
+  (230, 2, B'0110', 'this document is secret, category(-CB-)'),
+  (240, 2, B'0111', 'this document is secret, category(-CBA)');
+
+CREATE TABLE browse (
+  pguser       name references account(pguser),
+  did          int  references document(did),
+  ymd          date
+);
+GRANT ALL ON browse TO public;
+INSERT INTO browse VALUES
+  ('rls_regress_user1',  20, '2012-07-01'),
+  ('rls_regress_user1',  40, '2012-07-02'),
+  ('rls_regress_user1', 110, '2012-07-03'),
+  ('rls_regress_user2',  30, '2012-07-04'),
+  ('rls_regress_user2',  50, '2012-07-05'),
+  ('rls_regress_user2',  90, '2012-07-06'),
+  ('rls_regress_user2', 130, '2012-07-07'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 150, '2012-07-08'),
+  ('rls_regress_user2', 190, '2012-07-09'),
+  ('rls_regress_user2', 210, '2012-07-10'),
+  ('rls_regress_user3',  10, '2012-07-11'),
+  ('rls_regress_user3',  50, '2012-07-12');
+
+-- user's sensitivity level must higher than document's level
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dlevel <= (SELECT slevel FROM account WHERE pguser = current_user));
+ALTER TABLE browse SET ROW LEVEL SECURITY
+  (pguser = current_user);
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- change row-level security policy
+ALTER TABLE document RESET ROW LEVEL SECURITY;      -- failed
+ALTER TABLE document SET ROW LEVEL SECURITY (true); -- failed
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+-- switch to category based control from level based
+ALTER TABLE document SET ROW LEVEL SECURITY
+  (dcategory & (SELECT ~scategory FROM account WHERE pguser = current_user) = B'0000');
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN SELECT * FROM document NATURAL JOIN browse WHERE f_leak(browse::text);
+
+-- Failed to update PK row referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document NATURAL LEFT JOIN browse;
+DELETE FROM document WHERE did = 30;			-- failed
+UPDATE document SET did = 9999 WHERE did = 90;	-- failed
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101	1	aaa
+102	2	bbb
+103	3	ccc
+104	4	ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201	1	abc	1.1
+202	2	bcd	2.2
+203	3	cde	3.3
+204	4	def	4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301	1	xxx	X
+302	2	yyy	Y
+303	3	zzz	Z
+\.
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from v2));		-- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion via view)
+
+-- Now COPY TO command does not support RLS
+-- COPY t1 TO stdin;
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
#26Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Kohei KaiGai (#25)
Re: [v9.3] Row-Level Security

Kohei KaiGai escribió:

The revised patch fixes the problem that Daen pointed out.

Robert, would you be able to give this latest version of the patch a
look?

(KaiGai, does it still apply cleanly? If not, please submit a rebased
version.)

--
Álvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#27Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Alvaro Herrera (#26)
Re: [v9.3] Row-Level Security

2012/10/18 Alvaro Herrera <alvherre@2ndquadrant.com>:

Kohei KaiGai escribió:

The revised patch fixes the problem that Daen pointed out.

Robert, would you be able to give this latest version of the patch a
look?

(KaiGai, does it still apply cleanly? If not, please submit a rebased
version.)

I confirmed I could apply the latest patch cleanly.

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#28Erik Rijkers
er@xs4all.nl
In reply to: Kohei KaiGai (#27)
Re: [v9.3] Row-Level Security

On Fri, October 19, 2012 11:00, Kohei KaiGai wrote:

I confirmed I could apply the latest patch cleanly.

FWIW, I spent a few sessions (amounting to a few hours) trying to break, or get past SET ROW LEVEL
SECURITY and have not yet succeeded. So far so good.

(I haven't looked at code)

Erik Rijkers

#29Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#26)
Re: [v9.3] Row-Level Security

On Thu, Oct 18, 2012 at 2:19 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Kohei KaiGai escribió:

The revised patch fixes the problem that Daen pointed out.

Robert, would you be able to give this latest version of the patch a
look?

Yeah, sorry I've been completely sidelined this CommitFest. It's been
a crazy couple of months. Prognosis for future craziness reduction
uncertain. Comments:

The documentation lists several documented limitations that I would
like to analyze a little bit. First, it says that row-level security
policies are not applied on UPDATE or DELETE. That sounds downright
dangerous to me. Is there some really compelling reason we're not
doing it? Second, it says that row-level security policies are not
currently applied on INSERT, so you should use a trigger, but implies
that this will change in the future. I don't think we should change
that in the future; I think relying on triggers for that case is just
fine. Note that it could be an issue with the post-image for UPDATES,
as well, and I think the trigger solution is similarly adequate to
cover that case. With respect to the documented limitation regarding
DECLARE/FETCH, what exactly will happen? Can we describe this a bit
more clearly rather than just saying the behavior will be
unpredictable?

It looks suspiciously as if the row-level security mode needs to be
saved and restored in all the same places we call save and restore the
user ID and security context. Is there some reason the
row-level-security-enabled flag shouldn't just become another bit in
the security context? Then we'd get all of this save/restore logic
mostly for free.

ATExecSetRowLevelSecurity() calls SetRowLevelSecurity() or
ResetRowLevelSecurity() to update pg_rowlevelsec, but does the
pg_class update itself. I think that all of this logic should be
moved into a single function, or at least functions in the same file,
with the one that only updates pg_rowlevelsec being static and
therefore not able to be called from outside the file. We always need
the pg_class update and the pg_rowlevelsec update to happen together,
so it's not good to have an exposed function that does one of those
updates but not the other. I think the simplest thing is just to move
ATExecSetRowLevelSecurity to pg_rowlevelsec.c and rename it to
SetRowLevelSecurity() and then give it two static helper functions,
say InsertPolicyRow() and DeletePolicyRow().

I think it would be good if Tom could review the query-rewriting parts
of this (viz rowlevelsec.c) as I am not terribly familiar with this
machinery, and of course anything we get wrong here will have security
consequences. At first blush, I'm somewhat concerned about the fact
that we're trying to do this after query rewriting; that seems like it
could break things. I know KaiGai mentioned upthread that the
rewriter won't be rerun if the plan is invalidated, but (1) why is
that OK now? and (2) if it is OK now, then why is it OK to do
rewriting of the RLS qual - only - after rewriting if all of the rest
of the rewriting needs to happen earlier?

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

#30Tom Lane
tgl@sss.pgh.pa.us
In reply to: Robert Haas (#29)
Re: [v9.3] Row-Level Security

Robert Haas <robertmhaas@gmail.com> writes:

The documentation lists several documented limitations that I would
like to analyze a little bit. First, it says that row-level security
policies are not applied on UPDATE or DELETE. That sounds downright
dangerous to me. Is there some really compelling reason we're not
doing it?

[ blink... ] Isn't that a security hole big enough for a Mack truck?

UPDATE tab SET foo = foo RETURNING *;

sucks out all the data just fine, if RLS doesn't apply to it.

Having said that, I fear that sensible row-level security for updates is
at least one order of magnitude harder than sensible row-level security
for selects. We've speculated about how to define that in the past,
IIRC, but without any very satisfactory outcome.

regards, tom lane

#31Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#30)
Re: [v9.3] Row-Level Security

On Mon, Oct 22, 2012 at 12:17 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Robert Haas <robertmhaas@gmail.com> writes:

The documentation lists several documented limitations that I would
like to analyze a little bit. First, it says that row-level security
policies are not applied on UPDATE or DELETE. That sounds downright
dangerous to me. Is there some really compelling reason we're not
doing it?

[ blink... ] Isn't that a security hole big enough for a Mack truck?

UPDATE tab SET foo = foo RETURNING *;

sucks out all the data just fine, if RLS doesn't apply to it.

Yep.

Having said that, I fear that sensible row-level security for updates is
at least one order of magnitude harder than sensible row-level security
for selects. We've speculated about how to define that in the past,
IIRC, but without any very satisfactory outcome.

Uh, I don't agree. SELECT and DELETE are pretty much identical cases.
UPDATE needs all the same stuff that those two cases need, plus it
has an additional problem that it shares with INSERT - namely, someone
might insert a tuple that they cannot see or update a tuple such that
they can no longer see it. However, both of those problems can be
handled via triggers, for now and maybe forever. In contrast, the
problem that SELECT has - which UPDATE and DELETE also share - namely,
of rows being visible that should not be - is not nearly so
susceptible to that approach, both for performance reasons and because
there's no such thing as a trigger on SELECT.

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

#32Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Robert Haas (#29)
Re: [v9.3] Row-Level Security

2012/10/22 Robert Haas <robertmhaas@gmail.com>:

On Thu, Oct 18, 2012 at 2:19 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Kohei KaiGai escribió:

The revised patch fixes the problem that Daen pointed out.

Robert, would you be able to give this latest version of the patch a
look?

Yeah, sorry I've been completely sidelined this CommitFest. It's been
a crazy couple of months. Prognosis for future craziness reduction
uncertain. Comments:

The documentation lists several documented limitations that I would
like to analyze a little bit. First, it says that row-level security
policies are not applied on UPDATE or DELETE. That sounds downright
dangerous to me. Is there some really compelling reason we're not
doing it?

It intends to simplify the patch to avoid doing everything within a single
patch. I'll submit the patch supporting UPDATE and DELETE for CF-Nov
in addition to the base one.

Second, it says that row-level security policies are not
currently applied on INSERT, so you should use a trigger, but implies
that this will change in the future. I don't think we should change
that in the future; I think relying on triggers for that case is just
fine. Note that it could be an issue with the post-image for UPDATES,
as well, and I think the trigger solution is similarly adequate to
cover that case.

Hmm. I should not have written this in section of the current limitation.
It may give impression the behavior will be changed future.
OK, I'll try to revise the documentation stuff.

With respect to the documented limitation regarding
DECLARE/FETCH, what exactly will happen? Can we describe this a bit
more clearly rather than just saying the behavior will be
unpredictable?

In case when user-id was switched after declaration of a cursor that
contains qualifier depending on current_user, its results set contains
rows with old user-id and rows with new user-id.

Here is one other option rather than documentation fix.
As we had a discussion on the upthread, it can be solved if we restore
the user-id associated with the portal to be run, however, a problem is
some commands switches user-id inside of the portal.
http://archives.postgresql.org/pgsql-hackers/2012-07/msg00055.php

Is there some good idea to avoid the problem?

It looks suspiciously as if the row-level security mode needs to be
saved and restored in all the same places we call save and restore the
user ID and security context. Is there some reason the
row-level-security-enabled flag shouldn't just become another bit in
the security context? Then we'd get all of this save/restore logic
mostly for free.

It seems to me a good idea, but I didn't find out this.

ATExecSetRowLevelSecurity() calls SetRowLevelSecurity() or
ResetRowLevelSecurity() to update pg_rowlevelsec, but does the
pg_class update itself. I think that all of this logic should be
moved into a single function, or at least functions in the same file,
with the one that only updates pg_rowlevelsec being static and
therefore not able to be called from outside the file. We always need
the pg_class update and the pg_rowlevelsec update to happen together,
so it's not good to have an exposed function that does one of those
updates but not the other. I think the simplest thing is just to move
ATExecSetRowLevelSecurity to pg_rowlevelsec.c and rename it to
SetRowLevelSecurity() and then give it two static helper functions,
say InsertPolicyRow() and DeletePolicyRow().

OK, I'll rework the code.

I think it would be good if Tom could review the query-rewriting parts
of this (viz rowlevelsec.c) as I am not terribly familiar with this
machinery, and of course anything we get wrong here will have security
consequences. At first blush, I'm somewhat concerned about the fact
that we're trying to do this after query rewriting; that seems like it
could break things. I know KaiGai mentioned upthread that the
rewriter won't be rerun if the plan is invalidated, but (1) why is
that OK now? and (2) if it is OK now, then why is it OK to do
rewriting of the RLS qual - only - after rewriting if all of the rest
of the rewriting needs to happen earlier?

I just follow the existing behavior of plan invalidation; that does not
re-run the query rewriter. So, if we have no particular reason why
we should not run the rewriter again to handle RLS quals, it might
be an option to handle RLS as a part of rewriter.

At least, here is two problems. 1) System column is problematic
when SELECT statement is replaced by sub-query. 2) It makes
infinite recursion when a certain table has SELECT INSTEAD
rule with a sub-query on the same table.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

#33Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Kohei KaiGai (#32)
1 attachment(s)
Re: [v9.3] Row-Level Security

The attached patch is a revised version of row-level security
feature.
According to Robert's suggestion, I reworked implementation
around ALTER command, and logic to disable RLS during
FK/PK constraint checks.

In addition, I moved the entrypoint to apply row-level security
policy on the query tree next to the expand_inherited_tables,
because it became clear my previous approach is not
a straight-forward way to support update / delete cases.

This patch performs to replace RangeTblEntry of tables with
RLS policy by sub-queries that simply references the original
table with configured RLS policy. Also, the sub-queries have
security_barrier flag to prevent non-leakproof functions being
pushed down from outside of the sub-query.

This sub-query has target-list that just references columns of
underlying table, and ordered according to column definition
of the original table. So, we don't need to adjust varattno of
Var-node that reference regular columns, even though the
RangeTblEntry was replaced.
On the other hand, system-column is problematic because
sub-query does not have these columns due to nature of them.
So, I inject a logic to adjust varattno of Var-node that references
system-column of the target tables being replaced.
It works fine as follows:

postgres=> ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);
ALTER TABLE
postgres=> ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);
ALTER TABLE
postgres=> EXPLAIN (costs off) SELECT tableoid, * FROM t1 WHERE b like '%';
QUERY PLAN
-------------------------------------------
Result
-> Append
-> Subquery Scan on t1
Filter: (t1.b ~~ '%'::text)
-> Seq Scan on t1 t1_1
Filter: ((a % 2) = 0)
-> Subquery Scan on t2
Filter: (t2.b ~~ '%'::text)
-> Seq Scan on t2 t2_1
Filter: ((a % 2) = 1)
-> Seq Scan on t3
Filter: (b ~~ '%'::text)
(12 rows)

postgres=> SELECT tableoid, * FROM t1 WHERE b like '%';
tableoid | a | b
----------+----+-----
16385 | 2 | bbb
16385 | 4 | ddd
16385 | 6 | fff
16391 | 11 | sss
16391 | 13 | uuu
16391 | 15 | yyy
16397 | 21 | xyz
16397 | 22 | yzx
16397 | 23 | zxy
(9 rows)

Also, UPDATE / DELETE statement

postgres=> EXPLAIN (costs off) UPDATE t1 SET b = b || '_updt' WHERE b like '%';
QUERY PLAN
-------------------------------------
Update on t1
-> Subquery Scan on t1
Filter: (t1.b ~~ '%'::text)
-> Seq Scan on t1 t1_1
Filter: ((a % 2) = 0)
-> Subquery Scan on t2
Filter: (t2.b ~~ '%'::text)
-> Seq Scan on t2 t2_1
Filter: ((a % 2) = 1)
-> Seq Scan on t3
Filter: (b ~~ '%'::text)
(11 rows)

postgres=> UPDATE t1 SET b = b || '_updt' WHERE b like '%';
UPDATE 9

However, UPDATE / DELETE support is not perfect right now.
In case when we try to update / delete a table with inherited
children and RETURNING clause was added, is loses right
references to the pseudo columns, even though it works fine
without inherited children.

postgres=> UPDATE only t1 SET b = b || '_updt' WHERE b like '%' RETURNING *;
a | b
---+----------
2 | bbb_updt
4 | ddd_updt
6 | fff_updt
(3 rows)

UPDATE 3
postgres=> UPDATE t1 SET b = b || '_updt' WHERE b like '%' RETURNING *;
ERROR: variable not found in subplan target lists

I'm still under investigation of this behavior. Any comments
will be helpful to solve this problem.

Thanks,

2012/10/22 Kohei KaiGai <kaigai@kaigai.gr.jp>:

2012/10/22 Robert Haas <robertmhaas@gmail.com>:

On Thu, Oct 18, 2012 at 2:19 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Kohei KaiGai escribió:

The revised patch fixes the problem that Daen pointed out.

Robert, would you be able to give this latest version of the patch a
look?

Yeah, sorry I've been completely sidelined this CommitFest. It's been
a crazy couple of months. Prognosis for future craziness reduction
uncertain. Comments:

The documentation lists several documented limitations that I would
like to analyze a little bit. First, it says that row-level security
policies are not applied on UPDATE or DELETE. That sounds downright
dangerous to me. Is there some really compelling reason we're not
doing it?

It intends to simplify the patch to avoid doing everything within a single
patch. I'll submit the patch supporting UPDATE and DELETE for CF-Nov
in addition to the base one.

Second, it says that row-level security policies are not
currently applied on INSERT, so you should use a trigger, but implies
that this will change in the future. I don't think we should change
that in the future; I think relying on triggers for that case is just
fine. Note that it could be an issue with the post-image for UPDATES,
as well, and I think the trigger solution is similarly adequate to
cover that case.

Hmm. I should not have written this in section of the current limitation.
It may give impression the behavior will be changed future.
OK, I'll try to revise the documentation stuff.

With respect to the documented limitation regarding
DECLARE/FETCH, what exactly will happen? Can we describe this a bit
more clearly rather than just saying the behavior will be
unpredictable?

In case when user-id was switched after declaration of a cursor that
contains qualifier depending on current_user, its results set contains
rows with old user-id and rows with new user-id.

Here is one other option rather than documentation fix.
As we had a discussion on the upthread, it can be solved if we restore
the user-id associated with the portal to be run, however, a problem is
some commands switches user-id inside of the portal.
http://archives.postgresql.org/pgsql-hackers/2012-07/msg00055.php

Is there some good idea to avoid the problem?

It looks suspiciously as if the row-level security mode needs to be
saved and restored in all the same places we call save and restore the
user ID and security context. Is there some reason the
row-level-security-enabled flag shouldn't just become another bit in
the security context? Then we'd get all of this save/restore logic
mostly for free.

It seems to me a good idea, but I didn't find out this.

ATExecSetRowLevelSecurity() calls SetRowLevelSecurity() or
ResetRowLevelSecurity() to update pg_rowlevelsec, but does the
pg_class update itself. I think that all of this logic should be
moved into a single function, or at least functions in the same file,
with the one that only updates pg_rowlevelsec being static and
therefore not able to be called from outside the file. We always need
the pg_class update and the pg_rowlevelsec update to happen together,
so it's not good to have an exposed function that does one of those
updates but not the other. I think the simplest thing is just to move
ATExecSetRowLevelSecurity to pg_rowlevelsec.c and rename it to
SetRowLevelSecurity() and then give it two static helper functions,
say InsertPolicyRow() and DeletePolicyRow().

OK, I'll rework the code.

I think it would be good if Tom could review the query-rewriting parts
of this (viz rowlevelsec.c) as I am not terribly familiar with this
machinery, and of course anything we get wrong here will have security
consequences. At first blush, I'm somewhat concerned about the fact
that we're trying to do this after query rewriting; that seems like it
could break things. I know KaiGai mentioned upthread that the
rewriter won't be rerun if the plan is invalidated, but (1) why is
that OK now? and (2) if it is OK now, then why is it OK to do
rewriting of the RLS qual - only - after rewriting if all of the rest
of the rewriting needs to happen earlier?

I just follow the existing behavior of plan invalidation; that does not
re-run the query rewriter. So, if we have no particular reason why
we should not run the rewriter again to handle RLS quals, it might
be an option to handle RLS as a part of rewriter.

At least, here is two problems. 1) System column is problematic
when SELECT statement is replaced by sub-query. 2) It makes
infinite recursion when a certain table has SELECT INSTEAD
rule with a sub-query on the same table.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

Attachments:

pgsql-v9.3-row-level-security.rw.v6.patchapplication/octet-stream; name=pgsql-v9.3-row-level-security.rw.v6.patchDownload
 doc/src/sgml/catalogs.sgml                 |  59 ++
 doc/src/sgml/ref/alter_table.sgml          |  39 ++
 doc/src/sgml/user-manag.sgml               | 168 ++++++
 src/backend/catalog/Makefile               |   4 +-
 src/backend/catalog/dependency.c           |  23 +
 src/backend/catalog/heap.c                 |   1 +
 src/backend/catalog/pg_rowlevelsec.c       | 288 +++++++++
 src/backend/commands/copy.c                |  91 ++-
 src/backend/commands/explain.c             |   8 +-
 src/backend/commands/tablecmds.c           |  27 +
 src/backend/nodes/nodeFuncs.c              |  12 +-
 src/backend/optimizer/plan/planner.c       |  16 +
 src/backend/optimizer/prep/preptlist.c     |  52 +-
 src/backend/optimizer/prep/prepunion.c     |  61 +-
 src/backend/optimizer/util/Makefile        |   2 +-
 src/backend/optimizer/util/rowlevelsec.c   | 685 +++++++++++++++++++++
 src/backend/parser/gram.y                  |  16 +
 src/backend/parser/parse_agg.c             |   6 +
 src/backend/parser/parse_expr.c            |   3 +
 src/backend/rewrite/rewriteHandler.c       |  16 +
 src/backend/utils/adt/ri_triggers.c        |  13 +-
 src/backend/utils/cache/plancache.c        |  32 +
 src/backend/utils/cache/relcache.c         |  17 +-
 src/bin/pg_dump/pg_dump.c                  |  76 ++-
 src/bin/pg_dump/pg_dump.h                  |   1 +
 src/include/catalog/dependency.h           |   1 +
 src/include/catalog/indexing.h             |   3 +
 src/include/catalog/pg_class.h             |  20 +-
 src/include/catalog/pg_rowlevelsec.h       |  59 ++
 src/include/commands/copy.h                |   2 +-
 src/include/miscadmin.h                    |   1 +
 src/include/nodes/nodeFuncs.h              |   1 +
 src/include/nodes/parsenodes.h             |  12 +-
 src/include/nodes/plannodes.h              |   2 +
 src/include/nodes/relation.h               |   2 +
 src/include/optimizer/rowlevelsec.h        |  25 +
 src/include/parser/parse_node.h            |   3 +-
 src/include/rewrite/rewriteHandler.h       |   1 +
 src/include/utils/plancache.h              |   2 +
 src/include/utils/rel.h                    |   2 +
 src/test/regress/expected/rowlevelsec.out  | 929 +++++++++++++++++++++++++++++
 src/test/regress/expected/sanity_check.out |   3 +-
 src/test/regress/parallel_schedule         |   2 +-
 src/test/regress/serial_schedule           |   1 +
 src/test/regress/sql/rowlevelsec.sql       | 295 +++++++++
 45 files changed, 3039 insertions(+), 43 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index f999190..55898a5 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
      </row>
 
      <row>
+      <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+      <entry>row-level security policy of relation</entry>
+     </row>
+
+     <row>
       <entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
       <entry>security labels on database objects</entry>
      </row>
@@ -1807,6 +1812,16 @@
      </row>
 
      <row>
+      <entry><structfield>relhasrowlevelsec</structfield></entry>
+      <entry><type>bool</type></entry>
+      <entry></entry>
+      <entry>
+       True if table has row-level security policy; see
+       <link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link> catalog
+      </entry>
+     </row>
+
+     <row>
       <entry><structfield>relhassubclass</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
@@ -4906,6 +4921,50 @@
 
  </sect1>
 
+ <sect1 id="catalog-pg-rowlevelsec">
+  <title><structname>pg_rowlevelsec</structname></title>
+
+  <indexterm zone="catalog-pg-rowlevelsec">
+   <primary>pg_rowlevelsec</primary>
+  </indexterm>
+  <para>
+   The catalog <structname>pg_rowlevelsec</structname> expression tree of
+   row-level security policy to be performed on a particular relation.
+  </para>
+  <table>
+   <title><structname>pg_rowlevelsec</structname> Columns</title>
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>rlsrelid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+      <entry>The table this row-level security is for</entry>
+     </row>
+     <row>
+      <entry><structfield>rlsqual</structfield></entry>
+      <entry><type>text</type></entry>
+      <entry></entry>
+      <entry>An expression tree to be performed as rowl-level security policy</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+  <note>
+   <para>
+    <literal>pg_class.relhasrowlevelsec</literal>
+    must be true if a table has row-level security policy in this catalog.
+   </para>
+  </note>
+ </sect1>
 
  <sect1 id="catalog-pg-seclabel">
   <title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 356419e..4f2cd37 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     NOT OF
     OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
     SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+    SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)
+    RESET ROW LEVEL SECURITY
 
 <phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
 
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
    </varlistentry>
 
    <varlistentry>
+    <term><literal>SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+    <listitem>
+     <para>
+      This form set row-level security policy of the table.
+      Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+      as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+      clause, although mechanism guarantees to evaluate this condition earlier
+      than any other user given condition.
+      See also <xref linkend="ROW-LEVEL-SECURITY">.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET ROW LEVEL SECURITY</literal></term>
+    <listitem>
+     <para>
+      This form reset row-level security policy of the table, if exists.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
@@ -808,6 +833,20 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="PARAMETER">condition</replaceable></term>
+      <listitem>
+       <para>
+        An expression that returns a value of type boolean. Expect for a case
+        when queries are executed with superuser privilege, only rows for which
+        this expression returns true will be fetched, updated or deleted.
+        This expression can reference columns of the relation being configured.
+        Sub-queries can be contained within expression tree, unless referenced
+        relation recursively references the same relation.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..dab5b1e 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,172 @@ DROP ROLE <replaceable>name</replaceable>;
   </para>
  </sect1>
 
+ <sect1 id="row-level-security">
+  <title>Row-level Security</title>
+  <para>
+   <productname>PostgreSQL</productname> v9.3 or later provides
+   row-level security feature, like several commercial database
+   management system. It allows table owner to assign a particular
+   condition that performs as a security policy of the table; only
+   rows that satisfies the condition should be visible, except for
+   a case when superuser runs queries.
+  </para>
+  <para>
+   Row-level security policy can be set using
+   <literal>SET ROW LEVEL SECURITY</literal> command of
+   <xref linkend="SQL-ALTERTABLE"> statement, as an expression
+    form that returns a value of type boolean. This expression can
+    contain references to columns of the relation, so it enables
+    to construct arbitrary rule to make access control decision
+    based on contents of each rows.
+  </para>
+  <para>
+   For example, the following <literal>customer</literal> table
+   has <literal>uname</literal> field to store user name, and
+   it assume we don't want to expose any properties of other
+   customers.
+   The following command set <literal>current_user = uname</literal>
+   as row-level security policy on the <literal>customer</literal>
+   table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+</screen>
+   <xref linkend="SQL-EXPLAIN"> command shows how row-level
+   security policy works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+                 QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+   Filter: f_leak(customer.upasswd)
+   ->  Seq Scan on customer customer_1
+         Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+   This query execution plan means the preconfigured row-level
+   security policy is implicitly added, and scan plan on the
+   target relation being wrapped up with a sub-query.
+   It ensures user given qualifiers, including functions with
+   side effects, are never executed earlier than the row-level
+   security policy regardless of its cost, except for the cases
+   when these were fully leakproof.
+   This design helps to tackle the scenario described in
+   <xref linkend="RULES-PRIVILEGES">; that introduces the order
+   to evaluate qualifiers is significant to keep confidentiality
+   of invisible rows.
+  </para>
+
+  <para>
+   On the other hand, this design allows superusers to bypass
+   checks with row-level security.
+   It ensures <application>pg_dump</application> can obtain
+   a complete set of database backup, and avoid to execute
+   Trojan horse trap, being injected as a row-level security
+   policy of user-defined table, with privileges of superuser.
+  </para>
+
+  <para>
+   In case of queries on inherited tables, row-level security
+   policy of the parent relation is not applied to child
+   relations. Scope of the row-level security policy is limited
+   to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+                QUERY PLAN
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.y)
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((x % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: f_leak(y)
+         ->  Subquery Scan on t3
+               Filter: f_leak(t3.y)
+               ->  Seq Scan on t3 t3_1
+                     Filter: ((x % 2) = 1)
+(12 rows)
+</screen>
+    In the above example, <literal>t1</literal> has inherited
+    child table <literal>t2</literal> and <literal>t3</literal>,
+    and row-level security policy is set on only <literal>t1</literal>,
+    and <literal>t3</literal>, not <literal>t2</literal>.
+
+    The row-level security policy of <literal>t1</literal>,
+    <literal>x</literal> must be even-number, is appended only
+    <literal>t1</literal>, neither <literal>t2</literal> nor
+    <literal>t3</literal>. On the contrary, <literal>t3</literal>
+    has different row-level security policy; <literal>x</literal>
+    must be odd-number.
+  </para>
+
+  <para>
+   Row-level security feature also works to queries for writer-
+   operations; such as <xref linkend="SQL-UPDATE"> or
+   <xref linkend="SQL-DELETE"> commands.
+   It prevents to modify rows that does not satisfy the configured
+   row-level security policy.
+   The below query tries to update e-mail address of the
+   <literal>customer</> table, and the row-level security makes
+   sure any rows that don't match with <literal>current_user</>.
+  </para>
+<screen>
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+                    QUERY PLAN
+--------------------------------------------------
+ Update on customer
+   ->  Subquery Scan on customer
+         ->  Seq Scan on customer customer_1
+               Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+  <para>
+   On the other hands, please note that row-level security is
+   not applied on <xref linkend="SQL-INSERT"> command, because
+   of no sense.
+   Any row-level security shall be applied on the timing when
+   rows are fetched from the tables, prior to evaluation of
+   user given <literal>WHERE</> clause to prevent unexpected
+   information leaks using non-leakproof functions.
+   All the rows to be inserted are. at least, visible for
+   current session users. therefore, it makes no sense to
+   check something from viewpoint of row-level security.
+  </para>
+  <para>
+   If you want to apply some constraints on rows to be inserted
+   or updated, we recommend to set up <literal>CHECK</>
+   constraints or before row triggers, rather than row-level
+   security features here.
+   Please also check the <xref linkend="rules-privileges">
+   section to understand leaky-view scenario bahalf on
+   the row-level security feature.
+  </para>
+
+  <para>
+   Unlike other commercial database systems, we don't have any
+   plan to allow individual row-level security policy for each
+   command type. Even if we want to perform with difference
+   policy between <xref linkend="SQL-SELECT"> and
+   <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause
+   can leak the rows to be invisible using
+   <xref linkend="SQL-DELETE"> command.
+  </para>
+
+  <para>
+   Even though it is not a specific matter in row-level security,
+   please be careful if you plan to use <literal>current_user</>
+   in row-level security policy.
+   <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH">
+   allows to switch current user identifier during execution
+   of the query. Thus, it can fetch the first 100 rows with
+   privilege of <literal>alice</>, then remaining rows with
+   privilege of <literal>bob</>. If and when query execution plan
+   contains some kind of materialization and row-level security
+   policy contains <literal>current_user</>, the fetched tuples
+   in <literal>bob</>'s screen might be evaluated according to
+   the privilege of <literal>alice</>.
+  </para>
+ </sect1>
 </chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index df6da1f..965aa38 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
        objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \
        pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
        pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
-       pg_type.o storage.o toasting.o
+       pg_rowlevelsec.o pg_type.o storage.o toasting.o
 
 BKIFILES = postgres.bki postgres.description postgres.shdescription
 
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
-	pg_foreign_table.h \
+	pg_foreign_table.h pg_rowlevelsec.h \
 	pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
 	toasting.h indexing.h \
     )
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index b9cfee2..2142c36 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
@@ -1240,6 +1241,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveExtensionById(object->objectId);
 			break;
 
+		case OCLASS_ROWLEVELSEC:
+			RemoveRowLevelSecurityById(object->objectId);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized object class: %u",
 				 object->classId);
@@ -2291,6 +2296,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case EventTriggerRelationId:
 			return OCLASS_EVENT_TRIGGER;
+
+		case RowLevelSecurityRelationId:
+			return OCLASS_ROWLEVELSEC;
 	}
 
 	/* shouldn't get here */
@@ -2940,6 +2948,21 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_ROWLEVELSEC:
+			{
+				char	   *relname;
+
+				relname = get_rel_name(object->objectId);
+				if (!relname)
+					elog(ERROR, "cache lookup failed for relation %u",
+						 object->objectId);
+				appendStringInfo(&buffer,
+								 _("row-level security of %s"), relname);
+
+
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 8818b68..cc56143 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -776,6 +776,7 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
 	values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
 	values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+	values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec);
 	values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	if (relacl != (Datum) 0)
diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c
new file mode 100644
index 0000000..f0e49a6
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,288 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlevelsec.c
+ *    routines to support manipulation of the pg_rowlevelsec relation
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-level security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowLevelSecurity(Relation relation)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+
+	tuple = systable_getnext(sscan);
+	if (HeapTupleIsValid(tuple))
+	{
+		RowLevelSecDesc	*rlsdesc;
+		MemoryContext	rlscxt;
+		MemoryContext	oldcxt;
+		Datum	datum;
+		bool	isnull;
+		char   *temp;
+
+		/*
+		 * Make the private memory context to store RowLevelSecDesc that
+		 * includes expression tree also.
+		 */
+		rlscxt = AllocSetContextCreate(CacheMemoryContext,
+									   RelationGetRelationName(relation),
+									   ALLOCSET_SMALL_MINSIZE,
+									   ALLOCSET_SMALL_INITSIZE,
+									   ALLOCSET_SMALL_MAXSIZE);
+		PG_TRY();
+		{
+			datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual,
+								 RelationGetDescr(rlsrel), &isnull);
+			Assert(!isnull);
+			temp = TextDatumGetCString(datum);
+
+			oldcxt = MemoryContextSwitchTo(rlscxt);
+
+			rlsdesc = palloc0(sizeof(RowLevelSecDesc));
+			rlsdesc->rlscxt = rlscxt;
+			rlsdesc->rlsqual = (Expr *) stringToNode(temp);
+			Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID);
+
+			rlsdesc->rlshassublinks
+				= contain_subplans((Node *)rlsdesc->rlsqual);
+
+			MemoryContextSwitchTo(oldcxt);
+
+			pfree(temp);
+		}
+		PG_CATCH();
+		{
+			MemoryContextDelete(rlscxt);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		relation->rlsdesc = rlsdesc;
+	}
+	else
+	{
+		relation->rlsdesc = NULL;
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, AccessShareLock);
+}
+
+/*
+ * Parse the supplied row-level security policy, and insert/update a row
+ * of pg_rowlevelsec catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, Node *clause)
+{
+	Oid				relationId = RelationGetRelid(relation);
+	ParseState	   *pstate;
+	RangeTblEntry  *rte;
+	Node		   *qual;
+	Relation		rlsrel;
+	ScanKeyData		skey;
+	SysScanDesc		sscan;
+	HeapTuple		oldtup;
+	HeapTuple		newtup;
+	Datum			values[Natts_pg_rowlevelsec];
+	bool			isnull[Natts_pg_rowlevelsec];
+	bool			replaces[Natts_pg_rowlevelsec];
+	ObjectAddress	target;
+	ObjectAddress	myself;
+
+	/* Parse the supplied clause */
+	pstate = make_parsestate(NULL);
+
+	rte = addRangeTableEntryForRelation(pstate, relation,
+										NULL, false, false);
+	addRTEtoQuery(pstate, rte, false, true, true);
+
+	qual = transformWhereClause(pstate, copyObject(clause),
+								EXPR_KIND_ROW_LEVEL_SEC,
+								"ROW LEVEL SECURITY");
+	/* zero-clear */
+	memset(values,   0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(isnull,   0, sizeof(isnull));
+
+	/* Update or Insert an entry to pg_rowlevelsec catalog  */
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	oldtup = systable_getnext(sscan);
+	if (HeapTupleIsValid(oldtup))
+	{
+		replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true;
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+
+		newtup = heap_modify_tuple(oldtup,
+								   RelationGetDescr(rlsrel),
+								   values, isnull, replaces);
+		simple_heap_update(rlsrel, &newtup->t_self, newtup);
+
+		deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+								   relationId, false);
+	}
+	else
+	{
+		values[Anum_pg_rowlevelsec_rlsrelid - 1]
+			= ObjectIdGetDatum(relationId);
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+		newtup = heap_form_tuple(RelationGetDescr(rlsrel),
+								 values, isnull);
+		simple_heap_insert(rlsrel, newtup);
+	}
+	CatalogUpdateIndexes(rlsrel, newtup);
+
+	heap_freetuple(newtup);
+
+	/* records dependencies of RLS-policy and relation/columns */
+	target.classId = RelationRelationId;
+	target.objectId = relationId;
+	target.objectSubId = 0;
+
+	myself.classId = RowLevelSecurityRelationId;
+	myself.objectId = relationId;
+	myself.objectSubId = 0;
+
+	recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+	recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+						   DEPENDENCY_NORMAL);
+	free_parsestate(pstate);
+
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+/*
+ * Remove row-level security policy row of pg_rowlevelsec
+ */
+static void
+DeletePolicyRow(Relation relation)
+{
+	if (relation->rlsdesc)
+	{
+		ObjectAddress	address;
+
+		address.classId = RowLevelSecurityRelationId;
+		address.objectId = RelationGetRelid(relation);
+		address.objectSubId = 0;
+
+		performDeletion(&address, DROP_RESTRICT, 0);
+	}
+	else
+	{
+		/* Nothing to do here */
+		elog(INFO, "relation %s has no row-level security policy, skipped",
+			 RelationGetRelationName(relation));
+	}
+}
+
+/*
+ * Guts of row-level security policy deletion.
+ */
+void
+RemoveRowLevelSecurityById(Oid relationId)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relationId));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+	{
+		simple_heap_delete(rlsrel, &tuple->t_self);
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+/*
+ * ALTER TABLE <name> SET ROW LEVEL SECURITY (...) OR
+ *                    RESET ROW LEVEL SECURITY
+ */
+void
+SetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid			relid = RelationGetRelid(relation);
+	Relation	class_rel;
+	HeapTuple	tuple;
+	Form_pg_class	class_form;
+
+	class_rel = heap_open(RelationRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	class_form = (Form_pg_class) GETSTRUCT(tuple);
+	if (clause != NULL)
+	{
+		InsertOrUpdatePolicyRow(relation, clause);
+		class_form->relhasrowlevelsec = true;
+	}
+	else
+	{
+		DeletePolicyRow(relation);
+		class_form->relhasrowlevelsec = false;
+	}
+	simple_heap_update(class_rel, &tuple->t_self, tuple);
+	CatalogUpdateIndexes(class_rel, tuple);
+
+	heap_close(class_rel, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 10c89c7..7ffe4a3 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
 #include "access/htup_details.h"
 #include "access/sysattr.h"
 #include "access/xact.h"
+#include "catalog/heap.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_type.h"
 #include "commands/copy.h"
@@ -34,15 +35,19 @@
 #include "libpq/pqformat.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "optimizer/clauses.h"
 #include "optimizer/planner.h"
+#include "optimizer/rowlevelsec.h"
 #include "parser/parse_relation.h"
+#include "parser/parsetree.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/fd.h"
 #include "tcop/tcopprot.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/syscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
@@ -742,7 +747,7 @@ CopyLoadRawBuf(CopyState cstate)
  * the table or the specifically requested columns.
  */
 uint64
-DoCopy(const CopyStmt *stmt, const char *queryString)
+DoCopy(CopyStmt *stmt, const char *queryString)
 {
 	CopyState	cstate;
 	bool		is_from = stmt->is_from;
@@ -772,14 +777,26 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
 		rel = heap_openrv(stmt->relation,
 						  (is_from ? RowExclusiveLock : AccessShareLock));
 
+		tupDesc = RelationGetDescr(rel);
+		attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+		/*
+		 * We have to run regular query, if the target relation has
+ 		 * row-level security policy
+		 */
+		if (copy_row_level_security(stmt, rel, attnums))
+		{
+			heap_close(rel, NoLock);	/* close with keeping lock */
+			rel = NULL;
+		}
+		else
+		{
 		rte = makeNode(RangeTblEntry);
 		rte->rtekind = RTE_RELATION;
 		rte->relid = RelationGetRelid(rel);
 		rte->relkind = rel->rd_rel->relkind;
 		rte->requiredPerms = required_access;
 
-		tupDesc = RelationGetDescr(rel);
-		attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
 		foreach(cur, attnums)
 		{
 			int			attno = lfirst_int(cur) -
@@ -791,6 +808,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
 				rte->selectedCols = bms_add_member(rte->selectedCols, attno);
 		}
 		ExecCheckRTPerms(list_make1(rte), true);
+		}
 	}
 	else
 	{
@@ -1139,6 +1157,53 @@ ProcessCopyOptions(CopyState cstate,
 }
 
 /*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	Var			   *subvar;
+	ListCell	   *cell;
+	Form_pg_attribute	attform
+		= SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+	subrte = rt_fetch((Index) 1, query->rtable);
+	Assert(subrte->rtekind == RTE_RELATION);
+
+	if (!SearchSysCacheExists2(ATTNUM,
+							   ObjectIdGetDatum(subrte->relid),
+							   Int16GetDatum(attform->attnum)))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("table \"%s\" does not have OIDs",
+						get_rel_name(subrte->relid))));
+
+	subvar = makeVar((Index) 1,
+					 attform->attnum,
+					 attform->atttypid,
+					 attform->atttypmod,
+					 attform->attcollation,
+					 0);
+	subtle = makeTargetEntry((Expr *) subvar,
+							 0,
+							 pstrdup(NameStr(attform->attname)),
+							 false);
+
+	query->targetList = list_concat(list_make1(subtle),
+									query->targetList);
+	/* adjust resno of TargetEntry */
+	foreach (cell, query->targetList)
+	{
+		subtle = lfirst(cell);
+		subtle->resno++;
+	}
+}
+
+/*
  * Common setup routines used by BeginCopyFrom and BeginCopyTo.
  *
  * Iff <binary>, unload or reload in the binary format, as opposed to the
@@ -1210,6 +1275,25 @@ BeginCopy(bool is_from,
 		Assert(!is_from);
 		cstate->rel = NULL;
 
+		/*
+		 * In case when regular COPY TO was replaced because of row-level
+		 * security, "raw_query" node have already analyzed / rewritten
+		 * query tree.
+		 */
+		if (IsA(raw_query, Query))
+		{
+			query = (Query *) raw_query;
+
+			Assert(query->querySource == QSRC_ROW_LEVEL_SECURITY);
+			if (cstate->oids)
+			{
+				fixup_oid_of_rls_query(query);
+				cstate->oids = false;
+			}
+			attnamelist = NIL;
+		}
+		else
+		{
 		/* Don't allow COPY w/ OIDs from a select */
 		if (cstate->oids)
 			ereport(ERROR,
@@ -1234,6 +1318,7 @@ BeginCopy(bool is_from,
 			elog(ERROR, "unexpected rewrite result");
 
 		query = (Query *) linitial(rewritten);
+		}
 
 		/* The grammar allows SELECT INTO, but we don't support that */
 		if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 33252a8..24756d0 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1964,8 +1964,12 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
 		case T_TidScan:
 		case T_ForeignScan:
 		case T_ModifyTable:
-			/* Assert it's on a real relation */
-			Assert(rte->rtekind == RTE_RELATION);
+			/*
+			 * Assert it's on either a real relation, or a sub-query of
+			 * row-level security being originated from a real relation.
+			 */
+			Assert((rte->rtekind == RTE_RELATION ||
+					rte->rtekind == RTE_SUBQUERY) && OidIsValid(rte->relid));
 			objectname = get_rel_name(rte->relid);
 			if (es->verbose)
 				namespace = get_namespace_name(get_rel_namespace(rte->relid));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f88bf79..3896522 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -2749,6 +2750,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_SetTableSpace:		/* must rewrite heap */
 			case AT_DropNotNull:		/* may change some SQL plans */
 			case AT_SetNotNull:
+			case AT_SetRowLevelSecurity:
+			case AT_ResetRowLevelSecurity:
 			case AT_GenericOptions:
 			case AT_AlterColumnGenericOptions:
 				cmd_lockmode = AccessExclusiveLock;
@@ -3112,6 +3115,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_DropInherit:	/* NO INHERIT */
 		case AT_AddOf:			/* OF */
 		case AT_DropOf: /* NOT OF */
+		case AT_SetRowLevelSecurity:
+		case AT_ResetRowLevelSecurity:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* These commands never recurse */
 			/* No command-specific prep needed */
@@ -3391,6 +3396,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropOf:
 			ATExecDropOf(rel, lockmode);
 			break;
+		case AT_SetRowLevelSecurity:
+			SetRowLevelSecurity(rel, (Node *) cmd->def);
+			break;
+		case AT_ResetRowLevelSecurity:
+			SetRowLevelSecurity(rel, NULL);
+			break;
 		case AT_GenericOptions:
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
@@ -7552,6 +7563,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				Assert(defaultexpr);
 				break;
 
+			case OCLASS_ROWLEVELSEC:
+				/*
+				 * A row-level security policy can depend on a column in case
+				 * when the policy clause references a particular column.
+				 * Due to same reason why TRIGGER ... WHEN does not support
+				 * to change column's type being referenced in clause, row-
+				 * level security policy also does not support it.
+				 */
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used in a row-level security policy"),
+						 errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject),
+								   colName)));
+				break;
+
 			case OCLASS_PROC:
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c52f4ed..8e04c78 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1869,8 +1869,11 @@ query_tree_walker(Query *query,
 
 	if (walker((Node *) query->targetList, context))
 		return true;
-	if (walker((Node *) query->returningList, context))
-		return true;
+	if (!(flags & QTW_IGNORE_RETURNING))
+	{
+		if (walker((Node *) query->returningList, context))
+			return true;
+	}
 	if (walker((Node *) query->jointree, context))
 		return true;
 	if (walker(query->setOperations, context))
@@ -2583,7 +2586,10 @@ query_tree_mutator(Query *query,
 	}
 
 	MUTATE(query->targetList, query->targetList, List *);
-	MUTATE(query->returningList, query->returningList, List *);
+	if (!(flags & QTW_IGNORE_RETURNING))
+		MUTATE(query->returningList, query->returningList, List *);
+	else
+		query->returningList = copyObject(query->returningList);
 	MUTATE(query->jointree, query->jointree, FromExpr *);
 	MUTATE(query->setOperations, query->setOperations, Node *);
 	MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b61005f..c0416b5 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/analyze.h"
@@ -167,6 +168,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	glob->lastPHId = 0;
 	glob->lastRowMarkId = 0;
 	glob->transientPlan = false;
+	glob->planUserId = InvalidOid;
 
 	/* Determine what fraction of the plan is likely to be scanned */
 	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -244,6 +246,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->nParamExec = glob->nParamExec;
+	result->planUserId = glob->planUserId;
 
 	return result;
 }
@@ -393,6 +396,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	expand_inherited_tables(root);
 
 	/*
+	 * Apply row-level security policy of the relation being referenced,
+	 * if configured with either of built-in or extension's features.
+	 * RangeTblEntry of the relation with row-level security policy shall
+	 * be replaced with a RLS sub-query that has simple scan on the table
+	 * with security policy qualifiers.
+	 *
+	 * This routine assumes PlannerInfo is already handled with
+	 * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark have
+	 * valid information.
+	 */
+	apply_row_level_security(root);
+
+	/*
 	 * Set hasHavingQual to remember if HAVING clause is present.  Needed
 	 * because preprocess_expression will reduce a constant-true condition to
 	 * an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index 1af4e7f..9bf7566 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,7 +37,44 @@
 
 static List *expand_targetlist(List *tlist, int command_type,
 				  Index result_relation, List *range_table);
+/*
+ * lookup_varattno
+ *
+ * This routine returns an attribute number to reference a particular
+ * attribute. In case when the target relation is really relation,
+ * we can reference arbitrary attribute (including system column)
+ * without any translations. However, we have to translate varattno
+ * of Vat that references sub-queries being originated from regular
+ * relations with row-level security policy.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+	RangeTblEntry  *rte = rt_fetch(rt_index, rtables);
 
+	if (rte->rtekind == RTE_SUBQUERY &&
+		rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+	{
+		ListCell   *cell;
+
+		foreach (cell, rte->subquery->targetList)
+		{
+			TargetEntry *tle = lfirst(cell);
+			Var			*var;
+
+			if (IsA(tle->expr, Const))
+				continue;
+
+			var = (Var *) tle->expr;
+			Assert(IsA(var, Var));
+
+			if (var->varattno == attno)
+				return tle->resno;
+		}
+		elog(ERROR, "invalid attno %d on the pseudo targetList", attno);
+	}
+	return attno;
+}
 
 /*
  * preprocess_targetlist
@@ -62,7 +99,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 	{
 		RangeTblEntry *rte = rt_fetch(result_relation, range_table);
 
-		if (rte->subquery != NULL || rte->relid == InvalidOid)
+		if (rte->subquery != NULL &&
+			rte->subquery->querySource != QSRC_ROW_LEVEL_SECURITY)
 			elog(ERROR, "subquery cannot be result relation");
 	}
 
@@ -95,7 +133,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 		{
 			/* It's a regular table, so fetch its TID */
 			var = makeVar(rc->rti,
-						  SelfItemPointerAttributeNumber,
+						  lookup_varattno(SelfItemPointerAttributeNumber,
+										  rc->rti, range_table),
 						  TIDOID,
 						  -1,
 						  InvalidOid,
@@ -111,7 +150,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 			if (rc->isParent)
 			{
 				var = makeVar(rc->rti,
-							  TableOidAttributeNumber,
+							  lookup_varattno(TableOidAttributeNumber,
+											  rc->rti, range_table),
 							  OIDOID,
 							  -1,
 							  InvalidOid,
@@ -129,7 +169,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 			/* Not a table, so we need the whole row as a junk var */
 			var = makeWholeRowVar(rt_fetch(rc->rti, range_table),
 								  rc->rti,
-								  0,
+								  lookup_varattno(0, rc->rti, range_table),
 								  false);
 			snprintf(resname, sizeof(resname), "wholerow%u", rc->rowmarkId);
 			tle = makeTargetEntry((Expr *) var,
@@ -298,7 +338,9 @@ expand_targetlist(List *tlist, int command_type,
 					if (!att_tup->attisdropped)
 					{
 						new_expr = (Node *) makeVar(result_relation,
-													attrno,
+													lookup_varattno(attrno,
+														result_relation,
+														range_table),
 													atttype,
 													atttypmod,
 													attcollation,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index b91e9f4..e1ed690 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -1606,6 +1606,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
 									 adjust_appendrel_attrs_mutator,
 									 (void *) &context,
 									 QTW_IGNORE_RC_SUBQUERIES);
+
 		if (newnode->resultRelation == appinfo->parent_relid)
 		{
 			newnode->resultRelation = appinfo->child_relid;
@@ -1624,6 +1625,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
 }
 
 static Node *
+fixup_var_on_rls_subquery(RangeTblEntry *rte, Var *var)
+{
+	ListCell   *cell;
+
+	Assert(rte->rtekind == RTE_SUBQUERY &&
+		   rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY);
+	/*
+	 * In case when row-level security policy is applied on the referenced
+	 * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+	 * to filter out unprivileged rows of underlying relation.
+	 * Even though reference to this sub-query should perform as if ones
+	 * to real relations, system column has to be cared in special way
+	 * due to the nature of sub-query.
+	 * Target-entries that reference system columns should be added on
+	 * rowlevelsec.c, so all we need to do here is looking up underlying
+	 * target-list that can reference underlying system column, and fix-
+	 * up varattno of the referencing Var node with resno of TargetEntry.
+	 */
+	foreach (cell, rte->subquery->targetList)
+	{
+		TargetEntry *subtle = lfirst(cell);
+
+		if (IsA(subtle->expr, Var))
+		{
+			Var	   *subvar = (Var *) subtle->expr;
+			Var	   *newnode;
+
+			if (subvar->varattno == var->varattno)
+			{
+				newnode = copyObject(var);
+				newnode->varattno = subtle->resno;
+				return (Node *)newnode;
+			}
+		}
+		else
+			Assert(IsA(subtle->expr, Const));
+	}
+	elog(ERROR, "could not find pseudo column of %d in relation %s",
+		 var->varattno, get_rel_name(rte->relid));
+	return NULL;
+}
+
+static Node *
 adjust_appendrel_attrs_mutator(Node *node,
 							   adjust_appendrel_attrs_context *context)
 {
@@ -1664,6 +1708,13 @@ adjust_appendrel_attrs_mutator(Node *node,
 				 */
 				if (OidIsValid(appinfo->child_reltype))
 				{
+					Query          *parse = context->root->parse;
+					RangeTblEntry  *rte = rt_fetch(appinfo->child_relid,
+												   parse->rtable);
+					if (rte->rtekind == RTE_SUBQUERY &&
+						rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+						var = (Var *)fixup_var_on_rls_subquery(rte, var);
+
 					Assert(var->vartype == appinfo->parent_reltype);
 					if (appinfo->parent_reltype != appinfo->child_reltype)
 					{
@@ -1708,7 +1759,15 @@ adjust_appendrel_attrs_mutator(Node *node,
 					return (Node *) rowexpr;
 				}
 			}
-			/* system attributes don't need any other translation */
+			else
+			{
+				Query		   *parse = context->root->parse;
+				RangeTblEntry  *rte = rt_fetch(appinfo->child_relid,
+											   parse->rtable);
+				if (rte->rtekind == RTE_SUBQUERY &&
+					rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+					return fixup_var_on_rls_subquery(rte, var);
+			}
 		}
 		return (Node *) var;
 	}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3430689 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
-       relnode.o restrictinfo.o tlist.o var.o
+       relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c
new file mode 100644
index 0000000..1721bb5
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,685 @@
+/*
+ * optimizer/util/rowlvsec.c
+ *    Row-level security support routines
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* flags to pull row-level security policy */
+#define RLS_FLAG_HAS_SUBLINKS			0x0001
+
+/* hook to allow extensions to apply their own security policy */
+rowlevel_security_hook_type	rowlevel_security_hook = NULL;
+
+/*
+ * make_pseudo_column
+ *
+ * It makes TargetEntry that references underlying attribute. It may be
+ * Const node of dummy NULL, not Var node, if it is already dropped.
+ */
+static TargetEntry *
+make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+	Expr   *expr;
+	char   *resname;
+
+	Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+	if (attnum == InvalidAttrNumber)
+	{
+		expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+		resname = get_rel_name(subrte->relid);
+	}
+	else
+	{
+		HeapTuple	tuple;
+		Form_pg_attribute	attform;
+
+		tuple = SearchSysCache2(ATTNUM,
+								ObjectIdGetDatum(subrte->relid),
+								Int16GetDatum(attnum));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+				 attnum, subrte->relid);
+		attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+		if (attform->attisdropped)
+		{
+			char	namebuf[NAMEDATALEN];
+
+			/* Insert NULL just for a placeholder of dropped column */
+			expr = (Expr *) makeConst(INT4OID,
+									  -1,
+									  InvalidOid,
+									  sizeof(int32),
+									  (Datum) 0,
+									  true,		/* isnull */
+									  true);	/* byval */
+			sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+			resname = pstrdup(namebuf);
+		}
+		else
+		{
+			expr = (Expr *) makeVar((Index) 1,
+									attform->attnum,
+									attform->atttypid,
+									attform->atttypmod,
+									attform->attcollation,
+									0);
+			resname = pstrdup(NameStr(attform->attname));
+		}
+		ReleaseSysCache(tuple);
+	}
+	return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * append_pseudo_system_column
+ *
+ * It returns attribute number of pseudo-column relevant to the supplied
+ * Var-node referencing the RLS sub-query. If required attribute is not
+ * in target-list, it also adds a new pseudo-column.
+ */
+static AttrNumber
+append_pseudo_system_column(RangeTblEntry *rte, Var *var)
+{
+	Query		   *subqry = rte->subquery;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	ListCell	   *cell;
+
+	Assert(rte->rtekind == RTE_SUBQUERY &&
+		   rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY);
+
+	foreach (cell, subqry->targetList)
+	{
+		subtle = lfirst(cell);
+
+		/*
+		 * If referenced system column is already attached on the target-
+		 * list of RLS sub-query, nothing to do here.
+		 */
+		if (IsA(subtle->expr, Var))
+		{
+			Var	   *subvar = (Var *)subtle->expr;
+
+			if (var->varattno == subvar->varattno)
+			{
+				if (subtle->resjunk)
+					subtle->resjunk = false;
+				return subtle->resno;
+			}
+		}
+	}
+
+	/*
+	 * Here is no target-list for the referenced system column, so append
+	 * a new pseudo column on demand
+	 */
+	subrte = rt_fetch((Index) 1, subqry->rtable);
+	subtle = make_pseudo_column(subrte, var->varattno);
+	subtle->resno = list_length(subqry->targetList) + 1;
+
+	subqry->targetList = lappend(subqry->targetList, subtle);
+	rte->eref->colnames = lappend(rte->eref->colnames,
+								  makeString(pstrdup(subtle->resname)));
+	return subtle->resno;
+}
+
+/*
+ * fixup_varattno
+ *
+ * It recursively fixes up references to RLS sub-query, and adds pseudo-
+ * columns of underlying system columns, if necessary.
+ */
+typedef struct {
+	PlannerInfo	*root;
+	int		varlevelsup;
+	bool	is_returning;
+} fixup_varattno_context;
+
+static bool
+fixup_varattno_walker(Node *node, fixup_varattno_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var			   *var = (Var *) node;
+		RangeTblEntry  *rte;
+		ListCell	   *cell;
+
+		/* Var node does not reference Query node currently we focus on */
+		if (var->varlevelsup != context->varlevelsup)
+			return false;
+
+		/*
+		 * All the regular columns should already have its own pseudo
+		 * column on expansion of RTE. Its resno of TargetEntry is
+		 * identical with underlying attribute, so never need to fix-up
+		 * varattno of Var node that references the sub-query.
+		 */
+		if (var->varattno > InvalidAttrNumber)
+			return false;
+
+		rte = rt_fetch(var->varno, context->root->parse->rtable);
+		if (!context->is_returning &&
+			rte->rtekind == RTE_SUBQUERY &&
+			rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+		{
+			/*
+			 * If this Var node is system-column or whole-row reference
+			 * on RLS sub-queries, its varattno has to be adjusted to
+			 * reference correct pseudo column. Pseudo column entries
+			 * of them are not constructed at expansion time, we append
+			 * it on demand.
+			 */
+			var->varattno = append_pseudo_system_column(rte, var);
+		}
+		else if (rte->rtekind == RTE_RELATION && rte->inh)
+		{
+			/*
+			 * Also, if this Var node is system-columns or whole-row
+			 * reference on the parent relation of inheritance tree that
+			 * includes RLS sub-queries, even though the parent relation
+			 * itself was not expanded, its pseudo-column entries have to
+			 * be added on the underlying child relations.
+			 * However, no need to fix up varattno of Var node, because
+			 * it shall be handled in prep/prepunion.c.
+			 */
+			foreach (cell, context->root->append_rel_list)
+			{
+				AppendRelInfo *appinfo = lfirst(cell);
+
+				if (appinfo->parent_relid != var->varno)
+					continue;
+
+				rte = rt_fetch(appinfo->child_relid,
+							   context->root->parse->rtable);
+				if (rte->rtekind == RTE_SUBQUERY &&
+					rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+					append_pseudo_system_column(rte, var);
+			}
+		}
+		return false;
+	}
+	else if (IsA(node, Query))
+	{
+		bool	result;
+
+		context->varlevelsup++;
+		result = query_tree_walker((Query *) node,
+								   fixup_varattno_walker,
+								   (void *) context, 0);
+		context->varlevelsup--;
+
+		return result;
+	}
+	return expression_tree_walker(node,
+								  fixup_varattno_walker,
+								  (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-level security configuration, if we try to expand
+ * the relation inside of RLS sub-query originated from same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+	PlannerInfo	   *parent = root->parent_root;
+
+	if (parent && parent->parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+	{
+		RangeTblEntry  *rte = rt_fetch(1, parent->parse->rtable);
+
+		Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+		if (relid == rte->relid)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("infinite recursion detected for relation \"%s\"",
+							get_rel_name(relid))));
+		check_infinite_recursion(parent, relid);
+	}
+}
+
+/*
+ * fixup_plan_rowmark
+ *
+ * Push down the given PlanRowMark into RLS sub-query.
+ */
+static void
+fixup_plan_rowmark(RangeTblEntry *rte, PlanRowMark *rowmark)
+{
+	Query		   *subqry = rte->subquery;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+
+	Assert(!rowmark->isParent);
+
+	if (rowmark->markType == ROW_MARK_EXCLUSIVE ||
+		rowmark->markType == ROW_MARK_SHARE)
+	{
+		RowMarkClause  *rclause = makeNode(RowMarkClause);
+
+		rclause->rti = (Index) 1;
+		if (rowmark->markType == ROW_MARK_EXCLUSIVE)
+			rclause->forUpdate = true;
+		else
+			rclause->forUpdate = false;
+		rclause->noWait = rowmark->noWait;
+		rclause->pushedDown = true;
+
+		subqry->rowMarks = lappend(subqry->rowMarks, rclause);
+	}
+	rowmark->markType = ROW_MARK_REFERENCE;
+
+	/*
+	 * Add 'ctid' and 'tableoid' pseudo columns to be required for
+	 * row-level locks
+	 */
+	subrte = rt_fetch(1, subqry->rtable);
+
+	subtle = make_pseudo_column(subrte, SelfItemPointerAttributeNumber);
+	subtle->resno = list_length(subqry->targetList) + 1;
+	subqry->targetList = lappend(subqry->targetList, subtle);
+	rte->eref->colnames = lappend(rte->eref->colnames,
+		makeString(pstrdup(subtle->resname)));
+
+	subtle = make_pseudo_column(subrte, TableOidAttributeNumber);
+	subtle->resno = list_length(subqry->targetList) + 1;
+	subqry->targetList = lappend(subqry->targetList, subtle);
+	rte->eref->colnames = lappend(rte->eref->colnames,
+		makeString(pstrdup(subtle->resname)));
+}
+
+/*
+ * expand_rtentry_rls
+ *
+ * It replaces the supplied RangeTblEntry (should be RTE_RELATION) by RLS
+ * sub-query with configured row-level security policy.
+ * This sub-query should have pseudo-column relevant to the regular columns,
+ * but no pseudo-columns for system-column or whole-row reference without
+ * references to them.
+ */
+static void
+expand_rtentry_rls(PlannerInfo *root, Index rtindex, Expr *qual, int flags)
+{
+	Query		   *parse = root->parse;
+	RangeTblEntry  *rte = rt_fetch(rtindex, parse->rtable);
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+	TargetEntry	   *subtle;
+	HeapTuple		tuple;
+	AttrNumber		nattrs;
+	AttrNumber		attnum;
+	List		   *targetList = NIL;
+	List		   *colNameList = NIL;
+	PlanRowMark	   *rowmark;
+
+	/* check recursion to prevent infinite loop */
+	check_infinite_recursion(root, rte->relid);
+
+	/* Expand views inside SubLink node */
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+	/*
+	 * Construction of sub-query
+	 */
+	subqry = (Query *) makeNode(Query);
+	subqry->commandType = CMD_SELECT;
+	subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	subrte = copyObject(rte);
+	subqry->rtable = list_make1(subrte);
+
+	subrtr = makeNode(RangeTblRef);
+	subrtr->rtindex = 1;
+	subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		subqry->hasSubLinks = true;
+
+	/*
+	 * Construct pseudo columns as TargetEntry of sub-query that
+	 * references a particular regular attribute of the underlying
+	 * relation.
+	 */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+	nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+	ReleaseSysCache(tuple);
+
+	for (attnum = 1; attnum <= nattrs; attnum++)
+	{
+		subtle = make_pseudo_column(subrte, attnum);
+		subtle->resno = list_length(targetList) + 1;
+		Assert(subtle->resno == attnum);
+
+		targetList = lappend(targetList, subtle);
+		colNameList = lappend(colNameList,
+							  makeString(pstrdup(subtle->resname)));
+	}
+	subqry->targetList = targetList;
+
+	/* Replace the original RengeTblEntry by sub-query */
+	/* XXX - relid has to be kept */
+	rte->rtekind = RTE_SUBQUERY;
+	rte->subquery = subqry;
+	rte->security_barrier = true;
+
+	/* no permission checks on subquery itself */
+	rte->requiredPerms = 0;
+	rte->checkAsUser = InvalidOid;
+	rte->selectedCols = NULL;
+	rte->modifiedCols = NULL;
+
+	rte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+	/*
+	 * Push-down of PlanRowMark if needed
+	 */
+	rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+	if (rowmark)
+		fixup_plan_rowmark(rte, rowmark);
+}
+
+/*
+ * pull_row_level_security
+ *
+ * It pulls the configured row-level security policy of both built-in
+ * and extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_level_security(Relation relation, int *p_flags)
+{
+	Expr   *quals = NULL;
+	int		flags = 0;
+
+	/*
+	 * Pull the row-level security policy configured with built-in
+	 * features, if unprivileged users. Please note that superuser
+	 * can bypass it.
+	 */
+	if (relation->rlsdesc && !superuser())
+	{
+		RowLevelSecDesc *rlsdesc = relation->rlsdesc;
+
+		quals = copyObject(rlsdesc->rlsqual);
+		if (rlsdesc->rlshassublinks)
+			flags |= RLS_FLAG_HAS_SUBLINKS;
+	}
+
+	/*
+	 * Also, ask extensions whether they want to apply their own
+	 * row-level security policy. If both built-in and extension
+	 * has their own policy, it shall be merged.
+	 */
+	if (rowlevel_security_hook)
+	{
+		List   *temp;
+
+		temp = (*rowlevel_security_hook)(relation);
+		if (temp != NIL)
+		{
+			if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+				contain_subplans((Node *) temp))
+				flags |= RLS_FLAG_HAS_SUBLINKS;
+
+			if (quals != NULL)
+				temp = lappend(temp, quals);
+
+			if (list_length(temp) == 1)
+				quals = (Expr *)list_head(temp);
+			else if (list_length(temp) > 1)
+				quals = makeBoolExpr(AND_EXPR, temp, -1);
+		}
+	}
+	*p_flags = flags;
+	return quals;
+}
+
+/*
+ * copy_row_level_security
+ *
+ * It construct a RLS sub-query instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_level_security(CopyStmt *stmt, Relation rel, List *attnums)
+{
+	Expr		  *quals;
+	int			   flags;
+	Query		  *parse;
+	RangeTblEntry  *rte;
+	RangeTblRef	   *rtr;
+	TargetEntry	   *tle;
+	Var			   *var;
+	ListCell	   *cell;
+
+	if (stmt->is_from)
+		return false;
+
+	quals = pull_row_level_security(rel, &flags);
+	if (!quals)
+		return false;
+
+	parse = (Query *) makeNode(Query);
+	parse->commandType = CMD_SELECT;
+	parse->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(rel);
+	rte->relkind = RelationGetForm(rel)->relkind;
+
+	foreach (cell, attnums)
+	{
+		HeapTuple	tuple;
+		Form_pg_attribute	attform;
+		AttrNumber	attno = lfirst_int(cell);
+
+		tuple = SearchSysCache2(ATTNUM,
+								ObjectIdGetDatum(RelationGetRelid(rel)),
+								Int16GetDatum(attno));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+				 attno, RelationGetRelationName(rel));
+		attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+		var = makeVar((Index) 1,
+					  attform->attnum,
+					  attform->atttypid,
+					  attform->atttypmod,
+					  attform->attcollation,
+					  0);
+		tle = makeTargetEntry((Expr *) var,
+							  list_length(parse->targetList) + 1,
+							  pstrdup(NameStr(attform->attname)),
+							  false);
+		parse->targetList = lappend(parse->targetList, tle);
+
+		ReleaseSysCache(tuple);
+
+		rte->selectedCols = bms_add_member(rte->selectedCols,
+								attno - FirstLowInvalidHeapAttributeNumber);
+	}
+	rte->inFromCl = true;
+	rte->requiredPerms = ACL_SELECT;
+
+	rtr = makeNode(RangeTblRef);
+	rtr->rtindex = 1;
+
+	parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+	parse->rtable = list_make1(rte);
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		parse->hasSubLinks = true;
+
+	stmt->query = (Node *) parse;
+
+	return true;
+}
+
+/*
+ * apply_row_level_security
+ *
+ * Entrypoint to apply configured row-level security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-level
+ * security policy, its RangeTblEntry shall be replaced by a RLS sub-query
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the sub-query to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ *   SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ *   SELECT * FROM (
+ *     SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ *   ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag, 
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_level_security(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	Oid			curr_userid;
+	int			curr_seccxt;
+	Index		rtindex;
+	bool		has_rowlevel_security = false;
+
+	/*
+	 * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+	 * no row-level security policy should be applied regardless
+	 * whether it is built-in or extension.
+	 */
+	GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+	if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+		return;
+
+	for (rtindex = 1; rtindex <= list_length(parse->rtable); rtindex++)
+	{
+		RangeTblEntry  *rte = rt_fetch(rtindex, parse->rtable);
+		Relation	rel;
+		Expr	   *quals;
+		int			flags;
+
+		/* only relation can have row-level security policy */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * Parent relation of inheritance tree is just a placeholder here.
+		 * So, no need to apply row-level security.
+		 */
+		if (rte->inh)
+			continue;
+
+		/*
+		 * It does not make sense to check row-level security policy on
+		 * the target relation of INSERT command.
+		 */
+		if (parse->commandType == CMD_INSERT &&
+			parse->resultRelation == rtindex)
+			continue;
+
+		/*
+		 * It does not make sense to apply row-level security policy on
+		 * the relation we already handled.
+		 * Note that the underlying relation never have inh==true.
+		 */
+		if (parse->querySource == QSRC_ROW_LEVEL_SECURITY &&
+			rtindex == 1)
+			continue;
+
+		/*
+		 * OK, it is a reference to "real" relation. Let's try to apply
+		 * row-level security policy being configured, if any.
+		 */
+		rel = heap_open(rte->relid, NoLock);
+
+		quals = pull_row_level_security(rel, &flags);
+		if (quals)
+		{
+			expand_rtentry_rls(root, rtindex, quals, flags);
+			has_rowlevel_security = true;
+		}
+		heap_close(rel, NoLock);
+	}
+
+	if (has_rowlevel_security)
+	{
+		PlannerGlobal  *glob = root->glob;
+		PlanInvalItem  *pi_item;
+		fixup_varattno_context context;
+
+		/*
+		 * XXX - Constructed Plan with row-level security policy depends
+		 * on properties of current used (database superuser can bypass
+		 * configured RLS policy), thus, it has to be invalidated when
+		 * its assumption was changed.
+		 */
+		if (!OidIsValid(glob->planUserId))
+		{
+			/* Plan invalidation on session user-id */
+			glob->planUserId = GetUserId();
+
+			/* Plan invalidation on catalog updates of pg_authid */
+			pi_item = makeNode(PlanInvalItem);
+			pi_item->cacheId = AUTHOID;
+			pi_item->hashValue =
+				GetSysCacheHashValue1(AUTHOID,
+									  ObjectIdGetDatum(glob->planUserId));
+			glob->invalItems = lappend(glob->invalItems, pi_item);
+		}
+		else
+			Assert(glob->planUserId == GetUserId());
+
+		/*
+		 * XXX - varattno of Var node that references the RangeTblEntry
+		 * being replaced by RLS sub-query has to be adjusted for proper
+		 * reference to the underlying pseudo-column of the relation.
+		 */
+		context.root = root;
+		context.varlevelsup = 0;
+		context.is_returning = false;
+		query_tree_walker(parse,
+						  fixup_varattno_walker,
+						  (void *) &context,
+						  QTW_IGNORE_RETURNING);
+		context.is_returning = true;
+		expression_tree_walker((Node *)parse->returningList,
+							   fixup_varattno_walker,
+							   (void *) &context);
+	}
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e4ff76e..a3d445d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2127,6 +2127,22 @@ alter_table_cmd:
 					n->def = (Node *)$2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> SET ROW LEVEL SECURITY (expression) */
+			| SET ROW LEVEL SECURITY '(' a_expr ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_SetRowLevelSecurity;
+					n->def = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> RESET ROW LEVEL SECURITY */
+			| RESET ROW LEVEL SECURITY
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_ResetRowLevelSecurity;
+					n->def = NULL;
+					$$ = (Node *)n;
+				}
 			| alter_generic_options
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index b75b2d9..9494889 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("aggregate functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("aggregate functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("window functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("window functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e9267c5..94c25d1 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1443,6 +1443,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
+		case EXPR_KIND_ROW_LEVEL_SEC:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2609,6 +2610,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "EXECUTE";
 		case EXPR_KIND_TRIGGER_WHEN:
 			return "WHEN";
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			return "ROW LEVEL SECURITY";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index b785c26..12e7dd7 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2250,3 +2250,19 @@ QueryRewrite(Query *parsetree)
 
 	return results;
 }
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+	fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 97e68b1..e54f92f 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -2999,6 +2999,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
+	int			temp_sec_context;
 	Datum		vals[RI_MAX_NUMKEYS * 2];
 	char		nulls[RI_MAX_NUMKEYS * 2];
 
@@ -3078,8 +3079,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/* Switch to proper UID to perform check as */
 	GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+	/*
+	 * Row-level security should be disabled in case when foreign-key
+	 * relation is queried to check existence of tuples that references
+	 * the primary-key being modified.
+	 */
+	temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+	if (source_is_pk)
+		temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
 	SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
-						   save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+						   temp_sec_context);
 
 	/* Finally we can run the query. */
 	spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 8c0391f..36a8750 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -51,6 +51,7 @@
 #include "catalog/namespace.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
@@ -665,6 +666,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
 		AcquireExecutorLocks(plan->stmt_list, true);
 
 		/*
+		 * If plan was constructed with assumption of a particular user-id,
+		 * and it is different from the current one, the cached-plan shall
+		 * be invalidated to construct suitable query plan.
+		 */
+		if (plan->is_valid &&
+			OidIsValid(plan->planUserId) &&
+			plan->planUserId == GetUserId())
+			plan->is_valid = false;
+
+		/*
 		 * If plan was transient, check to see if TransactionXmin has
 		 * advanced, and if so invalidate it.
 		 */
@@ -716,6 +727,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 {
 	CachedPlan *plan;
 	List	   *plist;
+	ListCell   *cell;
+	Oid			planUserId = InvalidOid;
 	bool		snapshot_set;
 	bool		spi_pushed;
 	MemoryContext plan_context;
@@ -794,6 +807,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	PopOverrideSearchPath();
 
 	/*
+	 * Check whether the generated plan assumes a particular user-id, or not.
+	 * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+	 * it should be kept and used to validation check of the cached plan
+	 * under the "current" user-id.
+	 */
+	foreach (cell, plist)
+	{
+		PlannedStmt	*pstmt = lfirst(cell);
+
+		if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+		{
+			Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+			planUserId = pstmt->planUserId;
+		}
+	}
+
+	/*
 	 * Make a dedicated memory context for the CachedPlan and its subsidiary
 	 * data.  It's probably not going to be large, but just in case, use the
 	 * default maxsize parameter.  It's transient for the moment.
@@ -828,6 +859,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->context = plan_context;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->planUserId = planUserId;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8c9ebe0..28ba831 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -896,6 +897,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	else
 		relation->trigdesc = NULL;
 
+	if (relation->rd_rel->relhasrowlevelsec)
+		RelationBuildRowLevelSecurity(relation);
+	else
+		relation->rlsdesc = NULL;
+
 	/*
 	 * if it's an index, initialize index-related information
 	 */
@@ -1785,6 +1791,8 @@ RelationDestroyRelation(Relation relation)
 		MemoryContextDelete(relation->rd_indexcxt);
 	if (relation->rd_rulescxt)
 		MemoryContextDelete(relation->rd_rulescxt);
+	if (relation->rlsdesc)
+		MemoryContextDelete(relation->rlsdesc->rlscxt);
 	pfree(relation);
 }
 
@@ -3024,7 +3032,13 @@ RelationCacheInitializePhase3(void)
 				relation->rd_rel->relhastriggers = false;
 			restart = true;
 		}
-
+		if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL)
+		{
+			RelationBuildRowLevelSecurity(relation);
+			if (relation->rlsdesc == NULL)
+				relation->rd_rel->relhasrowlevelsec = false;
+			restart = true;
+		}
 		/* Release hold on the relation */
 		RelationDecrementReferenceCount(relation);
 
@@ -4162,6 +4176,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_rules = NULL;
 		rel->rd_rulescxt = NULL;
 		rel->trigdesc = NULL;
+		rel->rlsdesc = NULL;
 		rel->rd_indexprs = NIL;
 		rel->rd_indpred = NIL;
 		rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 82330cb..694fa95 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3908,6 +3908,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reloptions;
 	int			i_toastreloptions;
 	int			i_reloftype;
+	int			i_rlsqual;
 
 	/* Make sure we are in proper schema */
 	selectSourceSchema(fout, "pg_catalog");
@@ -3932,7 +3933,45 @@ getTables(Archive *fout, int *numTables)
 	 * we cannot correctly identify inherited columns, owned sequences, etc.
 	 */
 
-	if (fout->remoteVersion >= 90100)
+	if (fout->remoteVersion >= 90300)
+	{
+		/*
+		 * Left join to pick up dependency info linking sequences to their
+		 * owning column, if any (note this dependency is AUTO as of 8.2)
+		 */
+		appendPQExpBuffer(query,
+						  "SELECT c.tableoid, c.oid, c.relname, "
+						  "c.relacl, c.relkind, c.relnamespace, "
+						  "(%s c.relowner) AS rolname, "
+						  "c.relchecks, c.relhastriggers, "
+						  "c.relhasindex, c.relhasrules, c.relhasoids, "
+						  "c.relfrozenxid, tc.oid AS toid, "
+						  "tc.relfrozenxid AS tfrozenxid, "
+						  "c.relpersistence, "
+						  "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+						  "d.refobjid AS owning_tab, "
+						  "d.refobjsubid AS owning_col, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+						"array_to_string(c.reloptions, ', ') AS reloptions, "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual "
+						  "FROM pg_class c "
+						  "LEFT JOIN pg_depend d ON "
+						  "(c.relkind = '%c' AND "
+						  "d.classid = c.tableoid AND d.objid = c.oid AND "
+						  "d.objsubid = 0 AND "
+						  "d.refclassid = c.tableoid AND d.deptype = 'a') "
+					   "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+					   "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) "
+						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+						  "ORDER BY c.oid",
+						  username_subquery,
+						  RELKIND_SEQUENCE,
+						  RELKIND_RELATION, RELKIND_SEQUENCE,
+						  RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+						  RELKIND_FOREIGN_TABLE);
+	}
+	else if (fout->remoteVersion >= 90100)
 	{
 		/*
 		 * Left join to pick up dependency info linking sequences to their
@@ -3952,7 +3991,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL as rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3988,7 +4028,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4023,7 +4064,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4058,7 +4100,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4094,7 +4137,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4129,7 +4173,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4160,7 +4205,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4186,7 +4232,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4222,7 +4269,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "WHERE relkind IN ('%c', '%c') "
 						  "ORDER BY oid",
@@ -4270,6 +4318,7 @@ getTables(Archive *fout, int *numTables)
 	i_reloptions = PQfnumber(res, "reloptions");
 	i_toastreloptions = PQfnumber(res, "toast_reloptions");
 	i_reloftype = PQfnumber(res, "reloftype");
+	i_rlsqual = PQfnumber(res, "rlsqual");
 
 	if (lockWaitTimeout && fout->remoteVersion >= 70300)
 	{
@@ -4312,6 +4361,10 @@ getTables(Archive *fout, int *numTables)
 			tblinfo[i].reloftype = NULL;
 		else
 			tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype));
+		if (PQgetisnull(res, i, i_rlsqual))
+			tblinfo[i].rlsqual = NULL;
+		else
+			tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual));
 		tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
 		if (PQgetisnull(res, i, i_owning_tab))
 		{
@@ -12847,6 +12900,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 			}
 		}
 	}
+	if (tbinfo->rlsqual)
+		appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n",
+						  fmtId(tbinfo->dobj.name), tbinfo->rlsqual);
 
 	if (binary_upgrade)
 		binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2100d43..57bd58b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -257,6 +257,7 @@ typedef struct _tableInfo
 	uint32		toast_frozenxid;	/* for restore toast frozen xid */
 	int			ncheck;			/* # of CHECK expressions */
 	char	   *reloftype;		/* underlying type for typed table */
+	char	   *rlsqual;		/* row-level security policy */
 	/* these two are set only if table is a sequence owned by a column: */
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8499768..49baa0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
 	OCLASS_EVENT_TRIGGER,		/* pg_event_trigger */
+	OCLASS_ROWLEVELSEC,			/* pg_rowlevelsec */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 238fe58..a3b07e2 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
 DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
 #define RangeTypidIndexId					3542
 
+DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId				3839
+
 /* last step of initialization script: build the indexes declared above */
 BUILD_INDICES
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f83ce80..5b6e38b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
 	bool		relhaspkey;		/* has (or has had) PRIMARY KEY index */
 	bool		relhasrules;	/* has (or has had) any rules */
 	bool		relhastriggers; /* has (or has had) any TRIGGERs */
+	bool		relhasrowlevelsec;	/* has (or has had) row-level security */
 	bool		relhassubclass; /* has (or has had) derived classes */
 	TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
 
@@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class;
  * ----------------
  */
 
-#define Natts_pg_class					27
+#define Natts_pg_class					28
 #define Anum_pg_class_relname			1
 #define Anum_pg_class_relnamespace		2
 #define Anum_pg_class_reltype			3
@@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class;
 #define Anum_pg_class_relhaspkey		21
 #define Anum_pg_class_relhasrules		22
 #define Anum_pg_class_relhastriggers	23
-#define Anum_pg_class_relhassubclass	24
-#define Anum_pg_class_relfrozenxid		25
-#define Anum_pg_class_relacl			26
-#define Anum_pg_class_reloptions		27
+#define Anum_pg_class_relhasrowlevelsec	24
+#define Anum_pg_class_relhassubclass	25
+#define Anum_pg_class_relfrozenxid		26
+#define Anum_pg_class_relacl			27
+#define Anum_pg_class_reloptions		28
 
 /* ----------------
  *		initial contents of pg_class
@@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class;
  */
 
 /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
-DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
 
 
diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h
new file mode 100644
index 0000000..41fcb21
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,59 @@
+/*
+ * pg_rowlevelsec.h
+ *   definition of the system catalog for row-level security policy
+ *   (pg_rowlevelsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWLEVELSEC_H
+#define PG_ROWLEVELSEC_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ *		pg_rowlevelsec definition. cpp turns this into
+ *		typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId	3838
+
+CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS
+{
+	Oid				rlsrelid;
+#ifdef CATALOG_VARLEN
+	pg_node_tree	rlsqual;
+#endif
+} FormData_pg_rowlevelsec;
+
+/* ----------------
+ *		Form_pg_rowlevelsec corresponds to a pointer to a row with
+ *		the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec;
+
+/* ----------------
+ * 		compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowlevelsec				2
+#define Anum_pg_rowlevelsec_rlsrelid		1
+#define Anum_pg_rowlevelsec_rlsqual			2
+
+typedef struct
+{
+	MemoryContext	rlscxt;
+	Expr		   *rlsqual;
+	bool			rlshassublinks;
+} RowLevelSecDesc;
+
+extern void	RelationBuildRowLevelSecurity(Relation relation);
+extern void	SetRowLevelSecurity(Relation relation, Node *clause);
+extern void	RemoveRowLevelSecurityById(Oid relationId);
+
+#endif  /* PG_ROWLEVELSEC_H */
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8680ac3..1c49dfa 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -21,7 +21,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 
-extern uint64 DoCopy(const CopyStmt *stmt, const char *queryString);
+extern uint64 DoCopy(CopyStmt *stmt, const char *queryString);
 
 extern void ProcessCopyOptions(CopyState cstate, bool is_from, List *options);
 extern CopyState BeginCopyFrom(Relation rel, const char *filename,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 3ea3493..8d7a9ad 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -277,6 +277,7 @@ extern int	trace_recovery(int trace_level);
 /* flags to be OR'd to form sec_context */
 #define SECURITY_LOCAL_USERID_CHANGE	0x0001
 #define SECURITY_RESTRICTED_OPERATION	0x0002
+#define SECURITY_ROW_LEVEL_DISABLED		0x0004
 
 extern char *DatabasePath;
 
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index e609e4b..a1d98e4 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
 #define QTW_IGNORE_RANGE_TABLE		0x08		/* skip rangetable entirely */
 #define QTW_EXAMINE_RTES			0x10		/* examine RTEs */
 #define QTW_DONT_COPY_QUERY			0x20		/* do not copy top Query */
+#define QTW_IGNORE_RETURNING		0x40		/* skip returning clause */
 
 
 extern Oid	exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8834499..a771727 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
 	QSRC_PARSER,				/* added by parse analysis (now unused) */
 	QSRC_INSTEAD_RULE,			/* added by unconditional INSTEAD rule */
 	QSRC_QUAL_INSTEAD_RULE,		/* added by conditional INSTEAD rule */
-	QSRC_NON_INSTEAD_RULE		/* added by non-INSTEAD rule */
+	QSRC_NON_INSTEAD_RULE,		/* added by non-INSTEAD rule */
+	QSRC_ROW_LEVEL_SECURITY,	/* added by row-level security */
 } QuerySource;
 
 /* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -700,6 +701,13 @@ typedef struct RangeTblEntry
 
 	/*
 	 * Fields valid for a plain relation RTE (else zero):
+	 *
+	 * XXX - Query optimizer may modify and replace RangeTblEntry on
+	 * a particular relation by sub-query, but should perform as result
+	 * relation of the query. In this case, relid field is used to track
+	 * which relation is the sub-query originated.
+	 * Right now, only row-level security feature uses this field to track
+	 * the relation-id of sub-query being originated.
 	 */
 	Oid			relid;			/* OID of the relation */
 	char		relkind;		/* relation kind (see pg_class.relkind) */
@@ -1233,6 +1241,8 @@ typedef enum AlterTableType
 	AT_DropInherit,				/* NO INHERIT parent */
 	AT_AddOf,					/* OF <type_name> */
 	AT_DropOf,					/* NOT OF */
+	AT_SetRowLevelSecurity,		/* SET ROW LEVEL SECURITY (...) */
+	AT_ResetRowLevelSecurity,	/* RESET ROW LEVEL SECURITY */
 	AT_GenericOptions			/* OPTIONS (...) */
 } AlterTableType;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fb9a863..6b3ea3d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
 	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
 
 	int			nParamExec;		/* number of PARAM_EXEC Params used */
+
+	Oid			planUserId;		/* user-id this plan assumed, or InvalidOid */
 } PlannedStmt;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 0a1f8d5..b195dd7 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
 	Index		lastRowMarkId;	/* highest PlanRowMark ID assigned */
 
 	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
+
+	Oid			planUserId;		/* User-Id to be assumed on this plan */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h
new file mode 100644
index 0000000..c8e0019
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,25 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlevelsec.h
+ *    prototypes for optimizer/rowlevelsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLEVELSEC_H
+#define ROWLEVELSEC_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(Relation relation);
+extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook;
+
+extern bool	copy_row_level_security(CopyStmt *stmt,
+									Relation relation, List *attnums);
+extern void	 apply_row_level_security(PlannerInfo *root);
+
+#endif	/* ROWLEVELSEC_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index aa9c648..3a1e4ea 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_INDEX_PREDICATE,		/* index predicate */
 	EXPR_KIND_ALTER_COL_TRANSFORM,	/* transform expr in ALTER COLUMN TYPE */
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
-	EXPR_KIND_TRIGGER_WHEN			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_TRIGGER_WHEN,			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_ROW_LEVEL_SEC,		/* policy of ROW LEVEL SECURITY */
 } ParseExprKind;
 
 
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 50625d4..d470cad 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
 #include "nodes/parsenodes.h"
 
 extern List *QueryRewrite(Query *parsetree);
+extern void	QueryRewriteExpr(Node *node, List *activeRIRs);
 extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
 extern Node *build_column_default(Relation rel, int attrno);
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e846..5f89028 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -115,6 +115,8 @@ typedef struct CachedPlan
 								 * bare utility statements) */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	Oid			planUserId;		/* is user-id that is assumed on this cached
+								   plan, or InvalidOid if portable for anybody */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4669d8a..dce8463 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_index.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "fmgr.h"
 #include "nodes/bitmapset.h"
 #include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
 	RuleLock   *rd_rules;		/* rewrite rules */
 	MemoryContext rd_rulescxt;	/* private memory cxt for rd_rules, if any */
 	TriggerDesc *trigdesc;		/* Trigger info, or NULL if rel has none */
+	RowLevelSecDesc	*rlsdesc;	/* Row-level security info, or NULL */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out
new file mode 100644
index 0000000..ccaf17b
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,929 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+    COST 0.0000001 LANGUAGE plpgsql
+    AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+CREATE TABLE uaccount (
+    pguser      name primary key,
+    seclv       int
+);
+INSERT INTO uaccount VALUES
+    ('rls_regress_user0', 99),
+    ('rls_regress_user1',  1),
+    ('rls_regress_user2',  2),
+    ('rls_regress_user3',  3);
+GRANT SELECT ON uaccount TO public;
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE category (
+    cid         int primary key,
+    cname       text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+    (11, 'novel'),
+    (22, 'science fiction'),
+    (33, 'technology'),
+    (44, 'manga');
+CREATE TABLE document (
+    did         int primary key,
+    cid         int references category(cid),
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+    ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+    ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+    ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+    ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+    ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+    ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+    ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+    ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW LEVEL SECURITY
+    (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great manga
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+   1 |  11 |      1 | rls_regress_user1 | my first novel
+   4 |  44 |      1 | rls_regress_user1 | my first manga
+   6 |  22 |      1 | rls_regress_user2 | great science fiction
+   8 |  44 |      1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great manga
+ cid | did | dlevel |      dauthor      |        dtitle         |      cname      
+-----+-----+--------+-------------------+-----------------------+-----------------
+  11 |   1 |      1 | rls_regress_user1 | my first novel        | novel
+  22 |   6 |      1 | rls_regress_user2 | great science fiction | science fiction
+  44 |   8 |      1 | rls_regress_user2 | great manga           | manga
+  44 |   4 |      1 | rls_regress_user1 | my first manga        | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my second novel
+NOTICE:  f_leak => my science fiction
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => my second manga
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great technology book
+NOTICE:  f_leak => great manga
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+   1 |  11 |      1 | rls_regress_user1 | my first novel
+   2 |  11 |      2 | rls_regress_user1 | my second novel
+   3 |  22 |      2 | rls_regress_user1 | my science fiction
+   4 |  44 |      1 | rls_regress_user1 | my first manga
+   5 |  44 |      2 | rls_regress_user1 | my second manga
+   6 |  22 |      1 | rls_regress_user2 | great science fiction
+   7 |  33 |      2 | rls_regress_user2 | great technology book
+   8 |  44 |      1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my second novel
+NOTICE:  f_leak => my science fiction
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => my second manga
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great technology book
+NOTICE:  f_leak => great manga
+ cid | did | dlevel |      dauthor      |        dtitle         |      cname      
+-----+-----+--------+-------------------+-----------------------+-----------------
+  11 |   2 |      2 | rls_regress_user1 | my second novel       | novel
+  11 |   1 |      1 | rls_regress_user1 | my first novel        | novel
+  22 |   6 |      1 | rls_regress_user2 | great science fiction | science fiction
+  22 |   3 |      2 | rls_regress_user1 | my science fiction    | science fiction
+  33 |   7 |      2 | rls_regress_user2 | great technology book | technology
+  44 |   8 |      1 | rls_regress_user2 | great manga           | manga
+  44 |   5 |      2 | rls_regress_user1 | my second manga       | manga
+  44 |   4 |      1 | rls_regress_user1 | my first manga        | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document document_1
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using uaccount_pkey on uaccount
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (category.cid = document.cid)
+   ->  Seq Scan on category
+   ->  Hash
+         ->  Subquery Scan on document
+               Filter: f_leak(document.dtitle)
+               ->  Seq Scan on document document_1
+                     Filter: (dlevel <= $0)
+                     InitPlan 1 (returns $0)
+                       ->  Index Scan using uaccount_pkey on uaccount
+                             Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW LEVEL SECURITY (true);     -- fail
+ERROR:  must be owner of relation document
+ALTER TABLE document RESET ROW LEVEL SECURITY;          -- fail
+ERROR:  must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW LEVEL SECURITY (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my second novel
+NOTICE:  f_leak => my science fiction
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => my second manga
+ did | cid | dlevel |      dauthor      |       dtitle       
+-----+-----+--------+-------------------+--------------------
+   1 |  11 |      1 | rls_regress_user1 | my first novel
+   2 |  11 |      2 | rls_regress_user1 | my second novel
+   3 |  22 |      2 | rls_regress_user1 | my science fiction
+   4 |  44 |      1 | rls_regress_user1 | my first manga
+   5 |  44 |      2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my second novel
+NOTICE:  f_leak => my science fiction
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => my second manga
+ cid | did | dlevel |      dauthor      |       dtitle       |      cname      
+-----+-----+--------+-------------------+--------------------+-----------------
+  11 |   1 |      1 | rls_regress_user1 | my first novel     | novel
+  11 |   2 |      2 | rls_regress_user1 | my second novel    | novel
+  22 |   3 |      2 | rls_regress_user1 | my science fiction | science fiction
+  44 |   4 |      1 | rls_regress_user1 | my first manga     | manga
+  44 |   5 |      2 | rls_regress_user1 | my second manga    | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great technology book
+NOTICE:  f_leak => great manga
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+   6 |  22 |      1 | rls_regress_user2 | great science fiction
+   7 |  33 |      2 | rls_regress_user2 | great technology book
+   8 |  44 |      1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great technology book
+NOTICE:  f_leak => great manga
+ cid | did | dlevel |      dauthor      |        dtitle         |      cname      
+-----+-----+--------+-------------------+-----------------------+-----------------
+  22 |   6 |      1 | rls_regress_user2 | great science fiction | science fiction
+  33 |   7 |      2 | rls_regress_user2 | great technology book | technology
+  44 |   8 |      1 | rls_regress_user2 | great manga           | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                  QUERY PLAN                  
+----------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document document_1
+         Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+                     QUERY PLAN                     
+----------------------------------------------------
+ Nested Loop
+   ->  Subquery Scan on document
+         Filter: f_leak(document.dtitle)
+         ->  Seq Scan on document document_1
+               Filter: (dauthor = "current_user"())
+   ->  Index Scan using category_pkey on category
+         Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW LEVEL SECURITY
+    (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+     WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+     ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel |      dauthor      |       dtitle       | cid |   cname    
+-----+-----+--------+-------------------+--------------------+-----+------------
+   2 |  11 |      2 | rls_regress_user1 | my second novel    |  11 | novel
+   1 |  11 |      1 | rls_regress_user1 | my first novel     |  11 | novel
+     |     |        |                   |                    |  33 | technology
+   5 |  44 |      2 | rls_regress_user1 | my second manga    |     | 
+   4 |  44 |      1 | rls_regress_user1 | my first manga     |     | 
+   3 |  22 |      2 | rls_regress_user1 | my science fiction |     | 
+(6 rows)
+
+DELETE FROM category WHERE cid = 33;    -- failed
+ERROR:  update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL:  Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel |      dauthor      |        dtitle         | cid |      cname      
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+   6 |  22 |      1 | rls_regress_user2 | great science fiction |  22 | science fiction
+   8 |  44 |      1 | rls_regress_user2 | great manga           |  44 | manga
+   7 |  33 |      2 | rls_regress_user2 | great technology book |     | 
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR:  insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL:  Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+   1 |  11 |      1 | rls_regress_user1 | my first novel
+   2 |  11 |      2 | rls_regress_user1 | my second novel
+   3 |  22 |      2 | rls_regress_user1 | my science fiction
+   4 |  44 |      1 | rls_regress_user1 | my first manga
+   5 |  44 |      2 | rls_regress_user1 | my second manga
+   6 |  22 |      1 | rls_regress_user2 | great science fiction
+   7 |  33 |      2 | rls_regress_user2 | great technology book
+   8 |  44 |      1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid |      cname      
+-----+-----------------
+  11 | novel
+  22 | science fiction
+  33 | technology
+  44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+SELECT * FROM t1;
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.b)
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.b)
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+               Filter: f_leak(b)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a |  b  
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(9 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a |  b  |   t1    
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(9 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ LockRows
+   ->  Result
+         ->  Append
+               ->  Subquery Scan on t1
+                     ->  LockRows
+                           ->  Seq Scan on t1 t1_1
+                                 Filter: ((a % 2) = 0)
+               ->  Subquery Scan on t2
+                     ->  LockRows
+                           ->  Seq Scan on t2 t2_1
+                                 Filter: ((a % 2) = 1)
+               ->  Seq Scan on t3
+(12 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ LockRows
+   ->  Result
+         ->  Append
+               ->  Subquery Scan on t1
+                     Filter: f_leak(t1.b)
+                     ->  LockRows
+                           ->  Seq Scan on t1 t1_1
+                                 Filter: ((a % 2) = 0)
+               ->  Subquery Scan on t2
+                     Filter: f_leak(t2.b)
+                     ->  LockRows
+                           ->  Seq Scan on t2 t2_1
+                                 Filter: ((a % 2) = 1)
+               ->  Seq Scan on t3
+                     Filter: f_leak(b)
+(15 rows)
+
+--
+-- COPY TO statement 
+--
+COPY t1 TO stdout;
+2	bbb
+4	ddd
+COPY t1 TO stdout WITH OIDS;
+102	2	bbb
+104	4	ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201	1.1	abc
+203	3.3	cde
+COPY (SELECT * FROM t1) TO stdout;
+2	bbb
+4	ddd
+1	abc
+3	cde
+1	xxx
+2	yyy
+3	zzz
+COPY document TO stdout WITH OIDS;	-- failed (no oid column)
+ERROR:  table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion)
+ERROR:  infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW LEVEL SECURITY (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+NOTICE:  f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE:  f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a |                b                 
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+                                      QUERY PLAN                                       
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1  (cost=28.55..61.67 rows=205 width=36)
+   Filter: f_leak(s1.b)
+   ->  Hash Join  (cost=28.55..55.52 rows=615 width=36)
+         Hash Cond: (s1_1.a = s2.x)
+         ->  Seq Scan on s1 s1_1  (cost=0.00..22.30 rows=1230 width=36)
+         ->  Hash  (cost=28.54..28.54 rows=1 width=4)
+               ->  HashAggregate  (cost=28.53..28.54 rows=1 width=4)
+                     ->  Subquery Scan on s2  (cost=0.00..28.52 rows=1 width=4)
+                           Filter: (s2.y ~~ '%2f%'::text)
+                           ->  Seq Scan on s2 s2_1  (cost=0.00..28.45 rows=6 width=36)
+                                 Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from v2));		-- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+NOTICE:  f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE:  f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a  |                b                 
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+  6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Subquery Scan on s1
+   Filter: f_leak(s1.b)
+   ->  Hash Join
+         Hash Cond: (s1_1.a = s2.x)
+         ->  Seq Scan on s1 s1_1
+         ->  Hash
+               ->  HashAggregate
+                     ->  Subquery Scan on s2
+                           Filter: (s2.y ~~ '%af%'::text)
+                           ->  Seq Scan on s2 s2_1
+                                 Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x  |                y                 
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+  2 |  2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+                             QUERY PLAN                             
+--------------------------------------------------------------------
+ Subquery Scan on s2
+   Filter: (s2.y ~~ '%28%'::text)
+   ->  Seq Scan on s2 s2_1
+         Filter: ((x % 2) = 0)
+   SubPlan 1
+     ->  Limit
+           ->  Subquery Scan on s1
+                 ->  Nested Loop Semi Join
+                       Join Filter: (s1_1.a = s2_2.x)
+                       ->  Seq Scan on s1 s1_1
+                       ->  Materialize
+                             ->  Subquery Scan on s2_2
+                                   Filter: (s2_2.y ~~ '%af%'::text)
+                                   ->  Seq Scan on s2 s2_3
+                                         Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion via view)
+ERROR:  infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a <= 2) AND ((a % 2) = 0))
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a <= 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(10 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE:  f_leak => aaa
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => bcd
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => def
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+           QUERY PLAN            
+---------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: f_leak(b)
+         ->  Seq Scan on t2
+               Filter: f_leak(b)
+         ->  Seq Scan on t3
+               Filter: f_leak(b)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a |  b  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+           QUERY PLAN           
+--------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t2
+               Filter: (a <= 2)
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+          QUERY PLAN           
+-------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a = 2)
+         ->  Seq Scan on t2
+               Filter: (a = 2)
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a = 2) AND ((a % 2) = 0))
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a = 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(10 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+             QUERY PLAN              
+-------------------------------------
+ Update on t1
+   ->  Subquery Scan on t1
+         Filter: f_leak(t1.b)
+         ->  Seq Scan on t1 t1_1
+               Filter: ((a % 2) = 0)
+   ->  Subquery Scan on t2
+         Filter: f_leak(t2.b)
+         ->  Seq Scan on t2 t2_1
+               Filter: ((a % 2) = 1)
+   ->  Seq Scan on t3
+         Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+             QUERY PLAN              
+-------------------------------------
+ Update on t1
+   ->  Subquery Scan on t1
+         Filter: f_leak(t1.b)
+         ->  Seq Scan on t1 t1_1
+               Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE:  f_leak => bbbbbb
+NOTICE:  f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE:  f_leak => bbbbbb_updt
+NOTICE:  f_leak => dddddd_updt
+ oid | a |      b      |       t1        
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE:  f_leak => bbbbbb_updt
+NOTICE:  f_leak => dddddd_updt
+NOTICE:  f_leak => abcabc
+NOTICE:  f_leak => cdecde
+NOTICE:  f_leak => xxxxxx
+NOTICE:  f_leak => yyyyyy
+NOTICE:  f_leak => zzzzzz
+ a |      b      
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ERROR:  variable not found in subplan target lists
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a |      b      
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+             QUERY PLAN              
+-------------------------------------
+ Delete on t1
+   ->  Subquery Scan on t1
+         Filter: f_leak(t1.b)
+         ->  Seq Scan on t1 t1_1
+               Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+             QUERY PLAN              
+-------------------------------------
+ Delete on t1
+   ->  Subquery Scan on t1
+         Filter: f_leak(t1.b)
+         ->  Seq Scan on t1 t1_1
+               Filter: ((a % 2) = 0)
+   ->  Subquery Scan on t2
+         Filter: f_leak(t2.b)
+         ->  Seq Scan on t2 t2_1
+               Filter: ((a % 2) = 1)
+   ->  Seq Scan on t3
+         Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE:  f_leak => bbbbbb_updt
+NOTICE:  f_leak => dddddd_updt
+ oid | a |      b      |       t1        
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+ERROR:  variable not found in subplan target lists
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a |   b    
+---+--------
+ 1 | aaa
+ 3 | ccc
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(9 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE:  drop cascades to 10 other objects
+DETAIL:  drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 3f04442..ffcd8d3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
  pg_proc                 | t
  pg_range                | t
  pg_rewrite              | t
+ pg_rowlevelsec          | t
  pg_seclabel             | t
  pg_shdepend             | t
  pg_shdescription        | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(155 rows)
+(156 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 663bf8a..4c4552f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: privileges security_label collate
+test: privileges rowlevelsec security_label collate
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index be789e3..eea4c3e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: privileges
+test: rowlevelsec
 test: security_label
 test: collate
 test: misc
diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql
new file mode 100644
index 0000000..a996b8c
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,295 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+    COST 0.0000001 LANGUAGE plpgsql
+    AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+CREATE TABLE uaccount (
+    pguser      name primary key,
+    seclv       int
+);
+INSERT INTO uaccount VALUES
+    ('rls_regress_user0', 99),
+    ('rls_regress_user1',  1),
+    ('rls_regress_user2',  2),
+    ('rls_regress_user3',  3);
+GRANT SELECT ON uaccount TO public;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE category (
+    cid         int primary key,
+    cname       text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+    (11, 'novel'),
+    (22, 'science fiction'),
+    (33, 'technology'),
+    (44, 'manga');
+
+CREATE TABLE document (
+    did         int primary key,
+    cid         int references category(cid),
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+    ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+    ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+    ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+    ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+    ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+    ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+    ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+    ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW LEVEL SECURITY
+    (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW LEVEL SECURITY (true);     -- fail
+ALTER TABLE document RESET ROW LEVEL SECURITY;          -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW LEVEL SECURITY (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW LEVEL SECURITY
+    (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+     WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+     ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33;    -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101	1	aaa
+102	2	bbb
+103	3	ccc
+104	4	ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201	1	abc	1.1
+202	2	bcd	2.2
+203	3	cde	3.3
+204	4	def	4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301	1	xxx	X
+302	2	yyy	Y
+303	3	zzz	Z
+\.
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement 
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS;	-- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from v2));		-- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
#34Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: Kohei KaiGai (#33)
1 attachment(s)
Re: [v9.3] Row-Level Security

However, UPDATE / DELETE support is not perfect right now.
In case when we try to update / delete a table with inherited
children and RETURNING clause was added, is loses right
references to the pseudo columns, even though it works fine
without inherited children.

The attached patch fixed this known problem.

I oversight that inheritance_planner() fixup Var->varno when
it references a sub-query; even if it originated from regular
table with row-level security policy.

In case when system-column or whole-row of the table with
row-level security policy referenced, rowlevelsec.c adds
relevant target-entries on the sub-query that wraps this
table-reference, and "varattno" of Var node towards system-
columns is adjusted later.
However, we need to treat RETURNING clause in a special way
because its Var node is evaluated at ExecUpdate or ExecDelete,
therefore, its attribute number should indicate raw-table, not
scanned virtual tuple on sub-query.

So, I added a logic to keep Var->varattno when it tries to reference
either system-column or whole-row of the replaced tables due to
row-level security.

Thanks,

2012/11/15 Kohei KaiGai <kaigai@kaigai.gr.jp>:

The attached patch is a revised version of row-level security
feature.
According to Robert's suggestion, I reworked implementation
around ALTER command, and logic to disable RLS during
FK/PK constraint checks.

In addition, I moved the entrypoint to apply row-level security
policy on the query tree next to the expand_inherited_tables,
because it became clear my previous approach is not
a straight-forward way to support update / delete cases.

This patch performs to replace RangeTblEntry of tables with
RLS policy by sub-queries that simply references the original
table with configured RLS policy. Also, the sub-queries have
security_barrier flag to prevent non-leakproof functions being
pushed down from outside of the sub-query.

This sub-query has target-list that just references columns of
underlying table, and ordered according to column definition
of the original table. So, we don't need to adjust varattno of
Var-node that reference regular columns, even though the
RangeTblEntry was replaced.
On the other hand, system-column is problematic because
sub-query does not have these columns due to nature of them.
So, I inject a logic to adjust varattno of Var-node that references
system-column of the target tables being replaced.
It works fine as follows:

postgres=> ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);
ALTER TABLE
postgres=> ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);
ALTER TABLE
postgres=> EXPLAIN (costs off) SELECT tableoid, * FROM t1 WHERE b like '%';
QUERY PLAN
-------------------------------------------
Result
-> Append
-> Subquery Scan on t1
Filter: (t1.b ~~ '%'::text)
-> Seq Scan on t1 t1_1
Filter: ((a % 2) = 0)
-> Subquery Scan on t2
Filter: (t2.b ~~ '%'::text)
-> Seq Scan on t2 t2_1
Filter: ((a % 2) = 1)
-> Seq Scan on t3
Filter: (b ~~ '%'::text)
(12 rows)

postgres=> SELECT tableoid, * FROM t1 WHERE b like '%';
tableoid | a | b
----------+----+-----
16385 | 2 | bbb
16385 | 4 | ddd
16385 | 6 | fff
16391 | 11 | sss
16391 | 13 | uuu
16391 | 15 | yyy
16397 | 21 | xyz
16397 | 22 | yzx
16397 | 23 | zxy
(9 rows)

Also, UPDATE / DELETE statement

postgres=> EXPLAIN (costs off) UPDATE t1 SET b = b || '_updt' WHERE b like '%';
QUERY PLAN
-------------------------------------
Update on t1
-> Subquery Scan on t1
Filter: (t1.b ~~ '%'::text)
-> Seq Scan on t1 t1_1
Filter: ((a % 2) = 0)
-> Subquery Scan on t2
Filter: (t2.b ~~ '%'::text)
-> Seq Scan on t2 t2_1
Filter: ((a % 2) = 1)
-> Seq Scan on t3
Filter: (b ~~ '%'::text)
(11 rows)

postgres=> UPDATE t1 SET b = b || '_updt' WHERE b like '%';
UPDATE 9

However, UPDATE / DELETE support is not perfect right now.
In case when we try to update / delete a table with inherited
children and RETURNING clause was added, is loses right
references to the pseudo columns, even though it works fine
without inherited children.

postgres=> UPDATE only t1 SET b = b || '_updt' WHERE b like '%' RETURNING *;
a | b
---+----------
2 | bbb_updt
4 | ddd_updt
6 | fff_updt
(3 rows)

UPDATE 3
postgres=> UPDATE t1 SET b = b || '_updt' WHERE b like '%' RETURNING *;
ERROR: variable not found in subplan target lists

I'm still under investigation of this behavior. Any comments
will be helpful to solve this problem.

Thanks,

2012/10/22 Kohei KaiGai <kaigai@kaigai.gr.jp>:

2012/10/22 Robert Haas <robertmhaas@gmail.com>:

On Thu, Oct 18, 2012 at 2:19 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Kohei KaiGai escribió:

The revised patch fixes the problem that Daen pointed out.

Robert, would you be able to give this latest version of the patch a
look?

Yeah, sorry I've been completely sidelined this CommitFest. It's been
a crazy couple of months. Prognosis for future craziness reduction
uncertain. Comments:

The documentation lists several documented limitations that I would
like to analyze a little bit. First, it says that row-level security
policies are not applied on UPDATE or DELETE. That sounds downright
dangerous to me. Is there some really compelling reason we're not
doing it?

It intends to simplify the patch to avoid doing everything within a single
patch. I'll submit the patch supporting UPDATE and DELETE for CF-Nov
in addition to the base one.

Second, it says that row-level security policies are not
currently applied on INSERT, so you should use a trigger, but implies
that this will change in the future. I don't think we should change
that in the future; I think relying on triggers for that case is just
fine. Note that it could be an issue with the post-image for UPDATES,
as well, and I think the trigger solution is similarly adequate to
cover that case.

Hmm. I should not have written this in section of the current limitation.
It may give impression the behavior will be changed future.
OK, I'll try to revise the documentation stuff.

With respect to the documented limitation regarding
DECLARE/FETCH, what exactly will happen? Can we describe this a bit
more clearly rather than just saying the behavior will be
unpredictable?

In case when user-id was switched after declaration of a cursor that
contains qualifier depending on current_user, its results set contains
rows with old user-id and rows with new user-id.

Here is one other option rather than documentation fix.
As we had a discussion on the upthread, it can be solved if we restore
the user-id associated with the portal to be run, however, a problem is
some commands switches user-id inside of the portal.
http://archives.postgresql.org/pgsql-hackers/2012-07/msg00055.php

Is there some good idea to avoid the problem?

It looks suspiciously as if the row-level security mode needs to be
saved and restored in all the same places we call save and restore the
user ID and security context. Is there some reason the
row-level-security-enabled flag shouldn't just become another bit in
the security context? Then we'd get all of this save/restore logic
mostly for free.

It seems to me a good idea, but I didn't find out this.

ATExecSetRowLevelSecurity() calls SetRowLevelSecurity() or
ResetRowLevelSecurity() to update pg_rowlevelsec, but does the
pg_class update itself. I think that all of this logic should be
moved into a single function, or at least functions in the same file,
with the one that only updates pg_rowlevelsec being static and
therefore not able to be called from outside the file. We always need
the pg_class update and the pg_rowlevelsec update to happen together,
so it's not good to have an exposed function that does one of those
updates but not the other. I think the simplest thing is just to move
ATExecSetRowLevelSecurity to pg_rowlevelsec.c and rename it to
SetRowLevelSecurity() and then give it two static helper functions,
say InsertPolicyRow() and DeletePolicyRow().

OK, I'll rework the code.

I think it would be good if Tom could review the query-rewriting parts
of this (viz rowlevelsec.c) as I am not terribly familiar with this
machinery, and of course anything we get wrong here will have security
consequences. At first blush, I'm somewhat concerned about the fact
that we're trying to do this after query rewriting; that seems like it
could break things. I know KaiGai mentioned upthread that the
rewriter won't be rerun if the plan is invalidated, but (1) why is
that OK now? and (2) if it is OK now, then why is it OK to do
rewriting of the RLS qual - only - after rewriting if all of the rest
of the rewriting needs to happen earlier?

I just follow the existing behavior of plan invalidation; that does not
re-run the query rewriter. So, if we have no particular reason why
we should not run the rewriter again to handle RLS quals, it might
be an option to handle RLS as a part of rewriter.

At least, here is two problems. 1) System column is problematic
when SELECT statement is replaced by sub-query. 2) It makes
infinite recursion when a certain table has SELECT INSTEAD
rule with a sub-query on the same table.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
KaiGai Kohei <kaigai@kaigai.gr.jp>

Attachments:

pgsql-v9.3-row-level-security.rw.v7.patchapplication/octet-stream; name=pgsql-v9.3-row-level-security.rw.v7.patchDownload
 doc/src/sgml/catalogs.sgml                 |   59 ++
 doc/src/sgml/ref/alter_table.sgml          |   39 ++
 doc/src/sgml/user-manag.sgml               |  168 +++++
 src/backend/catalog/Makefile               |    4 +-
 src/backend/catalog/dependency.c           |   23 +
 src/backend/catalog/heap.c                 |    1 +
 src/backend/catalog/pg_rowlevelsec.c       |  288 +++++++++
 src/backend/commands/copy.c                |   91 ++-
 src/backend/commands/explain.c             |    8 +-
 src/backend/commands/tablecmds.c           |   27 +
 src/backend/nodes/nodeFuncs.c              |   12 +-
 src/backend/optimizer/plan/planner.c       |   19 +-
 src/backend/optimizer/prep/preptlist.c     |   52 +-
 src/backend/optimizer/prep/prepunion.c     |   84 ++-
 src/backend/optimizer/util/Makefile        |    2 +-
 src/backend/optimizer/util/rowlevelsec.c   |  685 ++++++++++++++++++++
 src/backend/parser/gram.y                  |   16 +
 src/backend/parser/parse_agg.c             |    6 +
 src/backend/parser/parse_expr.c            |    3 +
 src/backend/rewrite/rewriteHandler.c       |   16 +
 src/backend/utils/adt/ri_triggers.c        |   13 +-
 src/backend/utils/cache/plancache.c        |   32 +
 src/backend/utils/cache/relcache.c         |   17 +-
 src/bin/pg_dump/pg_dump.c                  |   76 ++-
 src/bin/pg_dump/pg_dump.h                  |    1 +
 src/include/catalog/dependency.h           |    1 +
 src/include/catalog/indexing.h             |    3 +
 src/include/catalog/pg_class.h             |   20 +-
 src/include/catalog/pg_rowlevelsec.h       |   59 ++
 src/include/commands/copy.h                |    2 +-
 src/include/miscadmin.h                    |    1 +
 src/include/nodes/nodeFuncs.h              |    1 +
 src/include/nodes/parsenodes.h             |   12 +-
 src/include/nodes/plannodes.h              |    2 +
 src/include/nodes/relation.h               |    2 +
 src/include/optimizer/rowlevelsec.h        |   25 +
 src/include/parser/parse_node.h            |    3 +-
 src/include/rewrite/rewriteHandler.h       |    1 +
 src/include/utils/plancache.h              |    2 +
 src/include/utils/rel.h                    |    2 +
 src/test/regress/expected/rowlevelsec.out  |  954 ++++++++++++++++++++++++++++
 src/test/regress/expected/sanity_check.out |    3 +-
 src/test/regress/parallel_schedule         |    2 +-
 src/test/regress/serial_schedule           |    1 +
 src/test/regress/sql/rowlevelsec.sql       |  295 +++++++++
 45 files changed, 3088 insertions(+), 45 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index f999190..55898a5 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -234,6 +234,11 @@
      </row>
 
      <row>
+      <entry><link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link></entry>
+      <entry>row-level security policy of relation</entry>
+     </row>
+
+     <row>
       <entry><link linkend="catalog-pg-seclabel"><structname>pg_seclabel</structname></link></entry>
       <entry>security labels on database objects</entry>
      </row>
@@ -1807,6 +1812,16 @@
      </row>
 
      <row>
+      <entry><structfield>relhasrowlevelsec</structfield></entry>
+      <entry><type>bool</type></entry>
+      <entry></entry>
+      <entry>
+       True if table has row-level security policy; see
+       <link linkend="catalog-pg-rowlevelsec"><structname>pg_rowlevelsec</structname></link> catalog
+      </entry>
+     </row>
+
+     <row>
       <entry><structfield>relhassubclass</structfield></entry>
       <entry><type>bool</type></entry>
       <entry></entry>
@@ -4906,6 +4921,50 @@
 
  </sect1>
 
+ <sect1 id="catalog-pg-rowlevelsec">
+  <title><structname>pg_rowlevelsec</structname></title>
+
+  <indexterm zone="catalog-pg-rowlevelsec">
+   <primary>pg_rowlevelsec</primary>
+  </indexterm>
+  <para>
+   The catalog <structname>pg_rowlevelsec</structname> expression tree of
+   row-level security policy to be performed on a particular relation.
+  </para>
+  <table>
+   <title><structname>pg_rowlevelsec</structname> Columns</title>
+   <tgroup cols="4">
+    <thead>
+     <row>
+      <entry>Name</entry>
+      <entry>Type</entry>
+      <entry>References</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><structfield>rlsrelid</structfield></entry>
+      <entry><type>oid</type></entry>
+      <entry><literal><link linkend="catalog-pg-class"><structname>pg_class</structname></link>.oid</literal></entry>
+      <entry>The table this row-level security is for</entry>
+     </row>
+     <row>
+      <entry><structfield>rlsqual</structfield></entry>
+      <entry><type>text</type></entry>
+      <entry></entry>
+      <entry>An expression tree to be performed as rowl-level security policy</entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+  <note>
+   <para>
+    <literal>pg_class.relhasrowlevelsec</literal>
+    must be true if a table has row-level security policy in this catalog.
+   </para>
+  </note>
+ </sect1>
 
  <sect1 id="catalog-pg-seclabel">
   <title><structname>pg_seclabel</structname></title>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 356419e..4f2cd37 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -68,6 +68,8 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
     NOT OF
     OWNER TO <replaceable class="PARAMETER">new_owner</replaceable>
     SET TABLESPACE <replaceable class="PARAMETER">new_tablespace</replaceable>
+    SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)
+    RESET ROW LEVEL SECURITY
 
 <phrase>and <replaceable class="PARAMETER">table_constraint_using_index</replaceable> is:</phrase>
 
@@ -567,6 +569,29 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
    </varlistentry>
 
    <varlistentry>
+    <term><literal>SET ROW LEVEL SECURITY (<replaceable class="PARAMETER">condition</replaceable>)</literal></term>
+    <listitem>
+     <para>
+      This form set row-level security policy of the table.
+      Supplied <replaceable class="PARAMETER">condition</replaceable> performs
+      as if it is implicitly appended to the qualifiers of <literal>WHERE</literal>
+      clause, although mechanism guarantees to evaluate this condition earlier
+      than any other user given condition.
+      See also <xref linkend="ROW-LEVEL-SECURITY">.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESET ROW LEVEL SECURITY</literal></term>
+    <listitem>
+     <para>
+      This form reset row-level security policy of the table, if exists.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><literal>RENAME</literal></term>
     <listitem>
      <para>
@@ -808,6 +833,20 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="PARAMETER">name</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><replaceable class="PARAMETER">condition</replaceable></term>
+      <listitem>
+       <para>
+        An expression that returns a value of type boolean. Expect for a case
+        when queries are executed with superuser privilege, only rows for which
+        this expression returns true will be fetched, updated or deleted.
+        This expression can reference columns of the relation being configured.
+        Sub-queries can be contained within expression tree, unless referenced
+        relation recursively references the same relation.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
  </refsect1>
 
diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 177ac7a..dab5b1e 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -439,4 +439,172 @@ DROP ROLE <replaceable>name</replaceable>;
   </para>
  </sect1>
 
+ <sect1 id="row-level-security">
+  <title>Row-level Security</title>
+  <para>
+   <productname>PostgreSQL</productname> v9.3 or later provides
+   row-level security feature, like several commercial database
+   management system. It allows table owner to assign a particular
+   condition that performs as a security policy of the table; only
+   rows that satisfies the condition should be visible, except for
+   a case when superuser runs queries.
+  </para>
+  <para>
+   Row-level security policy can be set using
+   <literal>SET ROW LEVEL SECURITY</literal> command of
+   <xref linkend="SQL-ALTERTABLE"> statement, as an expression
+    form that returns a value of type boolean. This expression can
+    contain references to columns of the relation, so it enables
+    to construct arbitrary rule to make access control decision
+    based on contents of each rows.
+  </para>
+  <para>
+   For example, the following <literal>customer</literal> table
+   has <literal>uname</literal> field to store user name, and
+   it assume we don't want to expose any properties of other
+   customers.
+   The following command set <literal>current_user = uname</literal>
+   as row-level security policy on the <literal>customer</literal>
+   table.
+<screen>
+postgres=> ALTER TABLE customer SET ROW LEVEL SECURITY (current_user = uname);
+ALTER TABLE
+</screen>
+   <xref linkend="SQL-EXPLAIN"> command shows how row-level
+   security policy works on the supplied query.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM customer WHERE f_leak(upasswd);
+                 QUERY PLAN
+--------------------------------------------
+ Subquery Scan on customer
+   Filter: f_leak(customer.upasswd)
+   ->  Seq Scan on customer customer_1
+         Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+   This query execution plan means the preconfigured row-level
+   security policy is implicitly added, and scan plan on the
+   target relation being wrapped up with a sub-query.
+   It ensures user given qualifiers, including functions with
+   side effects, are never executed earlier than the row-level
+   security policy regardless of its cost, except for the cases
+   when these were fully leakproof.
+   This design helps to tackle the scenario described in
+   <xref linkend="RULES-PRIVILEGES">; that introduces the order
+   to evaluate qualifiers is significant to keep confidentiality
+   of invisible rows.
+  </para>
+
+  <para>
+   On the other hand, this design allows superusers to bypass
+   checks with row-level security.
+   It ensures <application>pg_dump</application> can obtain
+   a complete set of database backup, and avoid to execute
+   Trojan horse trap, being injected as a row-level security
+   policy of user-defined table, with privileges of superuser.
+  </para>
+
+  <para>
+   In case of queries on inherited tables, row-level security
+   policy of the parent relation is not applied to child
+   relations. Scope of the row-level security policy is limited
+   to the relation on which it is set.
+<screen>
+postgres=> EXPLAIN(costs off) SELECT * FROM t1 WHERE f_leak(y);
+                QUERY PLAN
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.y)
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((x % 2) = 0)
+         ->  Seq Scan on t2
+               Filter: f_leak(y)
+         ->  Subquery Scan on t3
+               Filter: f_leak(t3.y)
+               ->  Seq Scan on t3 t3_1
+                     Filter: ((x % 2) = 1)
+(12 rows)
+</screen>
+    In the above example, <literal>t1</literal> has inherited
+    child table <literal>t2</literal> and <literal>t3</literal>,
+    and row-level security policy is set on only <literal>t1</literal>,
+    and <literal>t3</literal>, not <literal>t2</literal>.
+
+    The row-level security policy of <literal>t1</literal>,
+    <literal>x</literal> must be even-number, is appended only
+    <literal>t1</literal>, neither <literal>t2</literal> nor
+    <literal>t3</literal>. On the contrary, <literal>t3</literal>
+    has different row-level security policy; <literal>x</literal>
+    must be odd-number.
+  </para>
+
+  <para>
+   Row-level security feature also works to queries for writer-
+   operations; such as <xref linkend="SQL-UPDATE"> or
+   <xref linkend="SQL-DELETE"> commands.
+   It prevents to modify rows that does not satisfy the configured
+   row-level security policy.
+   The below query tries to update e-mail address of the
+   <literal>customer</> table, and the row-level security makes
+   sure any rows that don't match with <literal>current_user</>.
+  </para>
+<screen>
+postgres=> EXPLAIN (costs off) UPDATE customer SET email = 'alice@example.com';
+                    QUERY PLAN
+--------------------------------------------------
+ Update on customer
+   ->  Subquery Scan on customer
+         ->  Seq Scan on customer customer_1
+               Filter: ("current_user"() = uname)
+(4 rows)
+</screen>
+  <para>
+   On the other hands, please note that row-level security is
+   not applied on <xref linkend="SQL-INSERT"> command, because
+   of no sense.
+   Any row-level security shall be applied on the timing when
+   rows are fetched from the tables, prior to evaluation of
+   user given <literal>WHERE</> clause to prevent unexpected
+   information leaks using non-leakproof functions.
+   All the rows to be inserted are. at least, visible for
+   current session users. therefore, it makes no sense to
+   check something from viewpoint of row-level security.
+  </para>
+  <para>
+   If you want to apply some constraints on rows to be inserted
+   or updated, we recommend to set up <literal>CHECK</>
+   constraints or before row triggers, rather than row-level
+   security features here.
+   Please also check the <xref linkend="rules-privileges">
+   section to understand leaky-view scenario bahalf on
+   the row-level security feature.
+  </para>
+
+  <para>
+   Unlike other commercial database systems, we don't have any
+   plan to allow individual row-level security policy for each
+   command type. Even if we want to perform with difference
+   policy between <xref linkend="SQL-SELECT"> and
+   <xref linkend="SQL-DELETE">, <literal>RETURNING</> clause
+   can leak the rows to be invisible using
+   <xref linkend="SQL-DELETE"> command.
+  </para>
+
+  <para>
+   Even though it is not a specific matter in row-level security,
+   please be careful if you plan to use <literal>current_user</>
+   in row-level security policy.
+   <xref linkend="SQL-DECLARE"> and <xref linkend="SQL-FETCH">
+   allows to switch current user identifier during execution
+   of the query. Thus, it can fetch the first 100 rows with
+   privilege of <literal>alice</>, then remaining rows with
+   privilege of <literal>bob</>. If and when query execution plan
+   contains some kind of materialization and row-level security
+   policy contains <literal>current_user</>, the fetched tuples
+   in <literal>bob</>'s screen might be evaluated according to
+   the privilege of <literal>alice</>.
+  </para>
+ </sect1>
 </chapter>
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index df6da1f..965aa38 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -14,7 +14,7 @@ OBJS = catalog.o dependency.o heap.o index.o indexing.o namespace.o aclchk.o \
        objectaddress.o pg_aggregate.o pg_collation.o pg_constraint.o pg_conversion.o \
        pg_depend.o pg_enum.o pg_inherits.o pg_largeobject.o pg_namespace.o \
        pg_operator.o pg_proc.o pg_range.o pg_db_role_setting.o pg_shdepend.o \
-       pg_type.o storage.o toasting.o
+       pg_rowlevelsec.o pg_type.o storage.o toasting.o
 
 BKIFILES = postgres.bki postgres.description postgres.shdescription
 
@@ -38,7 +38,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\
 	pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \
 	pg_ts_parser.h pg_ts_template.h pg_extension.h \
 	pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \
-	pg_foreign_table.h \
+	pg_foreign_table.h pg_rowlevelsec.h \
 	pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \
 	toasting.h indexing.h \
     )
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index b9cfee2..2142c36 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_opfamily.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_ts_config.h"
@@ -1240,6 +1241,10 @@ doDeletion(const ObjectAddress *object, int flags)
 			RemoveExtensionById(object->objectId);
 			break;
 
+		case OCLASS_ROWLEVELSEC:
+			RemoveRowLevelSecurityById(object->objectId);
+			break;
+
 		default:
 			elog(ERROR, "unrecognized object class: %u",
 				 object->classId);
@@ -2291,6 +2296,9 @@ getObjectClass(const ObjectAddress *object)
 
 		case EventTriggerRelationId:
 			return OCLASS_EVENT_TRIGGER;
+
+		case RowLevelSecurityRelationId:
+			return OCLASS_ROWLEVELSEC;
 	}
 
 	/* shouldn't get here */
@@ -2940,6 +2948,21 @@ getObjectDescription(const ObjectAddress *object)
 				break;
 			}
 
+		case OCLASS_ROWLEVELSEC:
+			{
+				char	   *relname;
+
+				relname = get_rel_name(object->objectId);
+				if (!relname)
+					elog(ERROR, "cache lookup failed for relation %u",
+						 object->objectId);
+				appendStringInfo(&buffer,
+								 _("row-level security of %s"), relname);
+
+
+				break;
+			}
+
 		default:
 			appendStringInfo(&buffer, "unrecognized object %u %u %d",
 							 object->classId,
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 8818b68..cc56143 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -776,6 +776,7 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey);
 	values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules);
 	values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers);
+	values[Anum_pg_class_relhasrowlevelsec - 1] = BoolGetDatum(rd_rel->relhasrowlevelsec);
 	values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	if (relacl != (Datum) 0)
diff --git a/src/backend/catalog/pg_rowlevelsec.c b/src/backend/catalog/pg_rowlevelsec.c
new file mode 100644
index 0000000..f0e49a6
--- /dev/null
+++ b/src/backend/catalog/pg_rowlevelsec.c
@@ -0,0 +1,288 @@
+/* -------------------------------------------------------------------------
+ *
+ * pg_rowlevelsec.c
+ *    routines to support manipulation of the pg_rowlevelsec relation
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_node.h"
+#include "parser/parse_relation.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/tqual.h"
+
+/*
+ * Load row-level security policy from the catalog, and keep it on
+ * the relation cache.
+ */
+void
+RelationBuildRowLevelSecurity(Relation relation)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+
+	tuple = systable_getnext(sscan);
+	if (HeapTupleIsValid(tuple))
+	{
+		RowLevelSecDesc	*rlsdesc;
+		MemoryContext	rlscxt;
+		MemoryContext	oldcxt;
+		Datum	datum;
+		bool	isnull;
+		char   *temp;
+
+		/*
+		 * Make the private memory context to store RowLevelSecDesc that
+		 * includes expression tree also.
+		 */
+		rlscxt = AllocSetContextCreate(CacheMemoryContext,
+									   RelationGetRelationName(relation),
+									   ALLOCSET_SMALL_MINSIZE,
+									   ALLOCSET_SMALL_INITSIZE,
+									   ALLOCSET_SMALL_MAXSIZE);
+		PG_TRY();
+		{
+			datum = heap_getattr(tuple, Anum_pg_rowlevelsec_rlsqual,
+								 RelationGetDescr(rlsrel), &isnull);
+			Assert(!isnull);
+			temp = TextDatumGetCString(datum);
+
+			oldcxt = MemoryContextSwitchTo(rlscxt);
+
+			rlsdesc = palloc0(sizeof(RowLevelSecDesc));
+			rlsdesc->rlscxt = rlscxt;
+			rlsdesc->rlsqual = (Expr *) stringToNode(temp);
+			Assert(exprType((Node *)rlsdesc->rlsqual) == BOOLOID);
+
+			rlsdesc->rlshassublinks
+				= contain_subplans((Node *)rlsdesc->rlsqual);
+
+			MemoryContextSwitchTo(oldcxt);
+
+			pfree(temp);
+		}
+		PG_CATCH();
+		{
+			MemoryContextDelete(rlscxt);
+			PG_RE_THROW();
+		}
+		PG_END_TRY();
+
+		relation->rlsdesc = rlsdesc;
+	}
+	else
+	{
+		relation->rlsdesc = NULL;
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, AccessShareLock);
+}
+
+/*
+ * Parse the supplied row-level security policy, and insert/update a row
+ * of pg_rowlevelsec catalog.
+ */
+static void
+InsertOrUpdatePolicyRow(Relation relation, Node *clause)
+{
+	Oid				relationId = RelationGetRelid(relation);
+	ParseState	   *pstate;
+	RangeTblEntry  *rte;
+	Node		   *qual;
+	Relation		rlsrel;
+	ScanKeyData		skey;
+	SysScanDesc		sscan;
+	HeapTuple		oldtup;
+	HeapTuple		newtup;
+	Datum			values[Natts_pg_rowlevelsec];
+	bool			isnull[Natts_pg_rowlevelsec];
+	bool			replaces[Natts_pg_rowlevelsec];
+	ObjectAddress	target;
+	ObjectAddress	myself;
+
+	/* Parse the supplied clause */
+	pstate = make_parsestate(NULL);
+
+	rte = addRangeTableEntryForRelation(pstate, relation,
+										NULL, false, false);
+	addRTEtoQuery(pstate, rte, false, true, true);
+
+	qual = transformWhereClause(pstate, copyObject(clause),
+								EXPR_KIND_ROW_LEVEL_SEC,
+								"ROW LEVEL SECURITY");
+	/* zero-clear */
+	memset(values,   0, sizeof(values));
+	memset(replaces, 0, sizeof(replaces));
+	memset(isnull,   0, sizeof(isnull));
+
+	/* Update or Insert an entry to pg_rowlevelsec catalog  */
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(relation)));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	oldtup = systable_getnext(sscan);
+	if (HeapTupleIsValid(oldtup))
+	{
+		replaces[Anum_pg_rowlevelsec_rlsqual - 1] = true;
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+
+		newtup = heap_modify_tuple(oldtup,
+								   RelationGetDescr(rlsrel),
+								   values, isnull, replaces);
+		simple_heap_update(rlsrel, &newtup->t_self, newtup);
+
+		deleteDependencyRecordsFor(RowLevelSecurityRelationId,
+								   relationId, false);
+	}
+	else
+	{
+		values[Anum_pg_rowlevelsec_rlsrelid - 1]
+			= ObjectIdGetDatum(relationId);
+		values[Anum_pg_rowlevelsec_rlsqual - 1]
+			= CStringGetTextDatum(nodeToString(qual));
+		newtup = heap_form_tuple(RelationGetDescr(rlsrel),
+								 values, isnull);
+		simple_heap_insert(rlsrel, newtup);
+	}
+	CatalogUpdateIndexes(rlsrel, newtup);
+
+	heap_freetuple(newtup);
+
+	/* records dependencies of RLS-policy and relation/columns */
+	target.classId = RelationRelationId;
+	target.objectId = relationId;
+	target.objectSubId = 0;
+
+	myself.classId = RowLevelSecurityRelationId;
+	myself.objectId = relationId;
+	myself.objectSubId = 0;
+
+	recordDependencyOn(&myself, &target, DEPENDENCY_AUTO);
+
+	recordDependencyOnExpr(&myself, qual, pstate->p_rtable,
+						   DEPENDENCY_NORMAL);
+	free_parsestate(pstate);
+
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+/*
+ * Remove row-level security policy row of pg_rowlevelsec
+ */
+static void
+DeletePolicyRow(Relation relation)
+{
+	if (relation->rlsdesc)
+	{
+		ObjectAddress	address;
+
+		address.classId = RowLevelSecurityRelationId;
+		address.objectId = RelationGetRelid(relation);
+		address.objectSubId = 0;
+
+		performDeletion(&address, DROP_RESTRICT, 0);
+	}
+	else
+	{
+		/* Nothing to do here */
+		elog(INFO, "relation %s has no row-level security policy, skipped",
+			 RelationGetRelationName(relation));
+	}
+}
+
+/*
+ * Guts of row-level security policy deletion.
+ */
+void
+RemoveRowLevelSecurityById(Oid relationId)
+{
+	Relation	rlsrel;
+	ScanKeyData	skey;
+	SysScanDesc	sscan;
+	HeapTuple	tuple;
+
+	rlsrel = heap_open(RowLevelSecurityRelationId, RowExclusiveLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_rowlevelsec_rlsrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(relationId));
+	sscan = systable_beginscan(rlsrel, RowLevelSecurityIndexId, true,
+							   SnapshotNow, 1, &skey);
+	while (HeapTupleIsValid(tuple = systable_getnext(sscan)))
+	{
+		simple_heap_delete(rlsrel, &tuple->t_self);
+	}
+	systable_endscan(sscan);
+	heap_close(rlsrel, RowExclusiveLock);
+}
+
+/*
+ * ALTER TABLE <name> SET ROW LEVEL SECURITY (...) OR
+ *                    RESET ROW LEVEL SECURITY
+ */
+void
+SetRowLevelSecurity(Relation relation, Node *clause)
+{
+	Oid			relid = RelationGetRelid(relation);
+	Relation	class_rel;
+	HeapTuple	tuple;
+	Form_pg_class	class_form;
+
+	class_rel = heap_open(RelationRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	class_form = (Form_pg_class) GETSTRUCT(tuple);
+	if (clause != NULL)
+	{
+		InsertOrUpdatePolicyRow(relation, clause);
+		class_form->relhasrowlevelsec = true;
+	}
+	else
+	{
+		DeletePolicyRow(relation);
+		class_form->relhasrowlevelsec = false;
+	}
+	simple_heap_update(class_rel, &tuple->t_self, tuple);
+	CatalogUpdateIndexes(class_rel, tuple);
+
+	heap_close(class_rel, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 10c89c7..7ffe4a3 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -24,6 +24,7 @@
 #include "access/htup_details.h"
 #include "access/sysattr.h"
 #include "access/xact.h"
+#include "catalog/heap.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_type.h"
 #include "commands/copy.h"
@@ -34,15 +35,19 @@
 #include "libpq/pqformat.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
+#include "nodes/makefuncs.h"
 #include "optimizer/clauses.h"
 #include "optimizer/planner.h"
+#include "optimizer/rowlevelsec.h"
 #include "parser/parse_relation.h"
+#include "parser/parsetree.h"
 #include "rewrite/rewriteHandler.h"
 #include "storage/fd.h"
 #include "tcop/tcopprot.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/syscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
@@ -742,7 +747,7 @@ CopyLoadRawBuf(CopyState cstate)
  * the table or the specifically requested columns.
  */
 uint64
-DoCopy(const CopyStmt *stmt, const char *queryString)
+DoCopy(CopyStmt *stmt, const char *queryString)
 {
 	CopyState	cstate;
 	bool		is_from = stmt->is_from;
@@ -772,14 +777,26 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
 		rel = heap_openrv(stmt->relation,
 						  (is_from ? RowExclusiveLock : AccessShareLock));
 
+		tupDesc = RelationGetDescr(rel);
+		attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
+
+		/*
+		 * We have to run regular query, if the target relation has
+ 		 * row-level security policy
+		 */
+		if (copy_row_level_security(stmt, rel, attnums))
+		{
+			heap_close(rel, NoLock);	/* close with keeping lock */
+			rel = NULL;
+		}
+		else
+		{
 		rte = makeNode(RangeTblEntry);
 		rte->rtekind = RTE_RELATION;
 		rte->relid = RelationGetRelid(rel);
 		rte->relkind = rel->rd_rel->relkind;
 		rte->requiredPerms = required_access;
 
-		tupDesc = RelationGetDescr(rel);
-		attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist);
 		foreach(cur, attnums)
 		{
 			int			attno = lfirst_int(cur) -
@@ -791,6 +808,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
 				rte->selectedCols = bms_add_member(rte->selectedCols, attno);
 		}
 		ExecCheckRTPerms(list_make1(rte), true);
+		}
 	}
 	else
 	{
@@ -1139,6 +1157,53 @@ ProcessCopyOptions(CopyState cstate,
 }
 
 /*
+ * Adjust Query tree constructed with row-level security feature.
+ * If WITH OIDS option was supplied, it adds Var node to reference
+ * object-id system column.
+ */
+static void
+fixup_oid_of_rls_query(Query *query)
+{
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	Var			   *subvar;
+	ListCell	   *cell;
+	Form_pg_attribute	attform
+		= SystemAttributeDefinition(ObjectIdAttributeNumber, true);
+
+	subrte = rt_fetch((Index) 1, query->rtable);
+	Assert(subrte->rtekind == RTE_RELATION);
+
+	if (!SearchSysCacheExists2(ATTNUM,
+							   ObjectIdGetDatum(subrte->relid),
+							   Int16GetDatum(attform->attnum)))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_COLUMN),
+				 errmsg("table \"%s\" does not have OIDs",
+						get_rel_name(subrte->relid))));
+
+	subvar = makeVar((Index) 1,
+					 attform->attnum,
+					 attform->atttypid,
+					 attform->atttypmod,
+					 attform->attcollation,
+					 0);
+	subtle = makeTargetEntry((Expr *) subvar,
+							 0,
+							 pstrdup(NameStr(attform->attname)),
+							 false);
+
+	query->targetList = list_concat(list_make1(subtle),
+									query->targetList);
+	/* adjust resno of TargetEntry */
+	foreach (cell, query->targetList)
+	{
+		subtle = lfirst(cell);
+		subtle->resno++;
+	}
+}
+
+/*
  * Common setup routines used by BeginCopyFrom and BeginCopyTo.
  *
  * Iff <binary>, unload or reload in the binary format, as opposed to the
@@ -1210,6 +1275,25 @@ BeginCopy(bool is_from,
 		Assert(!is_from);
 		cstate->rel = NULL;
 
+		/*
+		 * In case when regular COPY TO was replaced because of row-level
+		 * security, "raw_query" node have already analyzed / rewritten
+		 * query tree.
+		 */
+		if (IsA(raw_query, Query))
+		{
+			query = (Query *) raw_query;
+
+			Assert(query->querySource == QSRC_ROW_LEVEL_SECURITY);
+			if (cstate->oids)
+			{
+				fixup_oid_of_rls_query(query);
+				cstate->oids = false;
+			}
+			attnamelist = NIL;
+		}
+		else
+		{
 		/* Don't allow COPY w/ OIDs from a select */
 		if (cstate->oids)
 			ereport(ERROR,
@@ -1234,6 +1318,7 @@ BeginCopy(bool is_from,
 			elog(ERROR, "unexpected rewrite result");
 
 		query = (Query *) linitial(rewritten);
+		}
 
 		/* The grammar allows SELECT INTO, but we don't support that */
 		if (query->utilityStmt != NULL &&
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 33252a8..24756d0 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1964,8 +1964,12 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
 		case T_TidScan:
 		case T_ForeignScan:
 		case T_ModifyTable:
-			/* Assert it's on a real relation */
-			Assert(rte->rtekind == RTE_RELATION);
+			/*
+			 * Assert it's on either a real relation, or a sub-query of
+			 * row-level security being originated from a real relation.
+			 */
+			Assert((rte->rtekind == RTE_RELATION ||
+					rte->rtekind == RTE_SUBQUERY) && OidIsValid(rte->relid));
 			objectname = get_rel_name(rte->relid);
 			if (es->verbose)
 				namespace = get_namespace_name(get_rel_namespace(rte->relid));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f88bf79..3896522 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_opclass.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -2749,6 +2750,8 @@ AlterTableGetLockLevel(List *cmds)
 			case AT_SetTableSpace:		/* must rewrite heap */
 			case AT_DropNotNull:		/* may change some SQL plans */
 			case AT_SetNotNull:
+			case AT_SetRowLevelSecurity:
+			case AT_ResetRowLevelSecurity:
 			case AT_GenericOptions:
 			case AT_AlterColumnGenericOptions:
 				cmd_lockmode = AccessExclusiveLock;
@@ -3112,6 +3115,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		case AT_DropInherit:	/* NO INHERIT */
 		case AT_AddOf:			/* OF */
 		case AT_DropOf: /* NOT OF */
+		case AT_SetRowLevelSecurity:
+		case AT_ResetRowLevelSecurity:
 			ATSimplePermissions(rel, ATT_TABLE);
 			/* These commands never recurse */
 			/* No command-specific prep needed */
@@ -3391,6 +3396,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		case AT_DropOf:
 			ATExecDropOf(rel, lockmode);
 			break;
+		case AT_SetRowLevelSecurity:
+			SetRowLevelSecurity(rel, (Node *) cmd->def);
+			break;
+		case AT_ResetRowLevelSecurity:
+			SetRowLevelSecurity(rel, NULL);
+			break;
 		case AT_GenericOptions:
 			ATExecGenericOptions(rel, (List *) cmd->def);
 			break;
@@ -7552,6 +7563,22 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 				Assert(defaultexpr);
 				break;
 
+			case OCLASS_ROWLEVELSEC:
+				/*
+				 * A row-level security policy can depend on a column in case
+				 * when the policy clause references a particular column.
+				 * Due to same reason why TRIGGER ... WHEN does not support
+				 * to change column's type being referenced in clause, row-
+				 * level security policy also does not support it.
+				 */
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("cannot alter type of a column used in a row-level security policy"),
+						 errdetail("%s depends on column \"%s\"",
+								   getObjectDescription(&foundObject),
+								   colName)));
+				break;
+
 			case OCLASS_PROC:
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c52f4ed..8e04c78 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1869,8 +1869,11 @@ query_tree_walker(Query *query,
 
 	if (walker((Node *) query->targetList, context))
 		return true;
-	if (walker((Node *) query->returningList, context))
-		return true;
+	if (!(flags & QTW_IGNORE_RETURNING))
+	{
+		if (walker((Node *) query->returningList, context))
+			return true;
+	}
 	if (walker((Node *) query->jointree, context))
 		return true;
 	if (walker(query->setOperations, context))
@@ -2583,7 +2586,10 @@ query_tree_mutator(Query *query,
 	}
 
 	MUTATE(query->targetList, query->targetList, List *);
-	MUTATE(query->returningList, query->returningList, List *);
+	if (!(flags & QTW_IGNORE_RETURNING))
+		MUTATE(query->returningList, query->returningList, List *);
+	else
+		query->returningList = copyObject(query->returningList);
 	MUTATE(query->jointree, query->jointree, FromExpr *);
 	MUTATE(query->setOperations, query->setOperations, Node *);
 	MUTATE(query->havingQual, query->havingQual, Node *);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b61005f..7a4ba39 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -33,6 +33,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
 #include "parser/analyze.h"
@@ -167,6 +168,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	glob->lastPHId = 0;
 	glob->lastRowMarkId = 0;
 	glob->transientPlan = false;
+	glob->planUserId = InvalidOid;
 
 	/* Determine what fraction of the plan is likely to be scanned */
 	if (cursorOptions & CURSOR_OPT_FAST_PLAN)
@@ -244,6 +246,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams)
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->nParamExec = glob->nParamExec;
+	result->planUserId = glob->planUserId;
 
 	return result;
 }
@@ -393,6 +396,19 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	expand_inherited_tables(root);
 
 	/*
+	 * Apply row-level security policy of the relation being referenced,
+	 * if configured with either of built-in or extension's features.
+	 * RangeTblEntry of the relation with row-level security policy shall
+	 * be replaced with a RLS sub-query that has simple scan on the table
+	 * with security policy qualifiers.
+	 *
+	 * This routine assumes PlannerInfo is already handled with
+	 * expand_inherited_tables, thus, AppendRelInfo or PlanRowMark have
+	 * valid information.
+	 */
+	apply_row_level_security(root);
+
+	/*
 	 * Set hasHavingQual to remember if HAVING clause is present.  Needed
 	 * because preprocess_expression will reduce a constant-true condition to
 	 * an empty qual list ... but "HAVING TRUE" is not a semantic no-op.
@@ -842,7 +858,8 @@ inheritance_planner(PlannerInfo *root)
 			{
 				RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
 
-				if (rte->rtekind == RTE_SUBQUERY)
+				if (rte->rtekind == RTE_SUBQUERY &&
+					rte->subquery->querySource != QSRC_ROW_LEVEL_SECURITY)
 				{
 					Index		newrti;
 
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index 1af4e7f..9bf7566 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -37,7 +37,44 @@
 
 static List *expand_targetlist(List *tlist, int command_type,
 				  Index result_relation, List *range_table);
+/*
+ * lookup_varattno
+ *
+ * This routine returns an attribute number to reference a particular
+ * attribute. In case when the target relation is really relation,
+ * we can reference arbitrary attribute (including system column)
+ * without any translations. However, we have to translate varattno
+ * of Vat that references sub-queries being originated from regular
+ * relations with row-level security policy.
+ */
+static AttrNumber
+lookup_varattno(AttrNumber attno, Index rt_index, List *rtables)
+{
+	RangeTblEntry  *rte = rt_fetch(rt_index, rtables);
 
+	if (rte->rtekind == RTE_SUBQUERY &&
+		rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+	{
+		ListCell   *cell;
+
+		foreach (cell, rte->subquery->targetList)
+		{
+			TargetEntry *tle = lfirst(cell);
+			Var			*var;
+
+			if (IsA(tle->expr, Const))
+				continue;
+
+			var = (Var *) tle->expr;
+			Assert(IsA(var, Var));
+
+			if (var->varattno == attno)
+				return tle->resno;
+		}
+		elog(ERROR, "invalid attno %d on the pseudo targetList", attno);
+	}
+	return attno;
+}
 
 /*
  * preprocess_targetlist
@@ -62,7 +99,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 	{
 		RangeTblEntry *rte = rt_fetch(result_relation, range_table);
 
-		if (rte->subquery != NULL || rte->relid == InvalidOid)
+		if (rte->subquery != NULL &&
+			rte->subquery->querySource != QSRC_ROW_LEVEL_SECURITY)
 			elog(ERROR, "subquery cannot be result relation");
 	}
 
@@ -95,7 +133,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 		{
 			/* It's a regular table, so fetch its TID */
 			var = makeVar(rc->rti,
-						  SelfItemPointerAttributeNumber,
+						  lookup_varattno(SelfItemPointerAttributeNumber,
+										  rc->rti, range_table),
 						  TIDOID,
 						  -1,
 						  InvalidOid,
@@ -111,7 +150,8 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 			if (rc->isParent)
 			{
 				var = makeVar(rc->rti,
-							  TableOidAttributeNumber,
+							  lookup_varattno(TableOidAttributeNumber,
+											  rc->rti, range_table),
 							  OIDOID,
 							  -1,
 							  InvalidOid,
@@ -129,7 +169,7 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
 			/* Not a table, so we need the whole row as a junk var */
 			var = makeWholeRowVar(rt_fetch(rc->rti, range_table),
 								  rc->rti,
-								  0,
+								  lookup_varattno(0, rc->rti, range_table),
 								  false);
 			snprintf(resname, sizeof(resname), "wholerow%u", rc->rowmarkId);
 			tle = makeTargetEntry((Expr *) var,
@@ -298,7 +338,9 @@ expand_targetlist(List *tlist, int command_type,
 					if (!att_tup->attisdropped)
 					{
 						new_expr = (Node *) makeVar(result_relation,
-													attrno,
+													lookup_varattno(attrno,
+														result_relation,
+														range_table),
 													atttype,
 													atttypmod,
 													attcollation,
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index b91e9f4..5b60c16 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;
+	bool		in_returning;
 } adjust_appendrel_attrs_context;
 
 static Plan *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1594,6 +1595,7 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
 
 	context.root = root;
 	context.appinfo = appinfo;
+	context.in_returning = false;
 
 	/*
 	 * Must be prepared to start with a Query or a bare expression tree.
@@ -1605,7 +1607,25 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
 		newnode = query_tree_mutator((Query *) node,
 									 adjust_appendrel_attrs_mutator,
 									 (void *) &context,
-									 QTW_IGNORE_RC_SUBQUERIES);
+									 QTW_IGNORE_RC_SUBQUERIES |
+									 QTW_IGNORE_RETURNING);
+		/*
+		 * XXX - Returning clause should be handled in a special way.
+		 * In case when result relation of UPDATE / DELETE has row-level
+		 * security policy, its RangeTblEntry was replace by a sub-query,
+		 * thus, references to system-column need to be adjusted to point
+		 * pseudo-column behalf on the target system column.
+		 * However, Var nodes in returning clause are exception, because
+		 * its attribute number is evaluated towards the written image of
+		 * the tuple being updated or deleted, not virtual tuple of the
+		 * sub-query.
+		 */
+		context.in_returning = true;
+		newnode->returningList =
+			(List *) expression_tree_mutator((Node *) newnode->returningList,
+											 adjust_appendrel_attrs_mutator,
+											 (void *) &context);
+
 		if (newnode->resultRelation == appinfo->parent_relid)
 		{
 			newnode->resultRelation = appinfo->child_relid;
@@ -1624,6 +1644,49 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, AppendRelInfo *appinfo)
 }
 
 static Node *
+fixup_var_on_rls_subquery(RangeTblEntry *rte, Var *var)
+{
+	ListCell   *cell;
+
+	Assert(rte->rtekind == RTE_SUBQUERY &&
+		   rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY);
+	/*
+	 * In case when row-level security policy is applied on the referenced
+	 * table, its RangeTblEntry (RTE_RELATION) is replaced with sub-query
+	 * to filter out unprivileged rows of underlying relation.
+	 * Even though reference to this sub-query should perform as if ones
+	 * to real relations, system column has to be cared in special way
+	 * due to the nature of sub-query.
+	 * Target-entries that reference system columns should be added on
+	 * rowlevelsec.c, so all we need to do here is looking up underlying
+	 * target-list that can reference underlying system column, and fix-
+	 * up varattno of the referencing Var node with resno of TargetEntry.
+	 */
+	foreach (cell, rte->subquery->targetList)
+	{
+		TargetEntry *subtle = lfirst(cell);
+
+		if (IsA(subtle->expr, Var))
+		{
+			Var	   *subvar = (Var *) subtle->expr;
+			Var	   *newnode;
+
+			if (subvar->varattno == var->varattno)
+			{
+				newnode = copyObject(var);
+				newnode->varattno = subtle->resno;
+				return (Node *)newnode;
+			}
+		}
+		else
+			Assert(IsA(subtle->expr, Const));
+	}
+	elog(ERROR, "could not find pseudo column of %d in relation %s",
+		 var->varattno, get_rel_name(rte->relid));
+	return NULL;
+}
+
+static Node *
 adjust_appendrel_attrs_mutator(Node *node,
 							   adjust_appendrel_attrs_context *context)
 {
@@ -1664,6 +1727,14 @@ adjust_appendrel_attrs_mutator(Node *node,
 				 */
 				if (OidIsValid(appinfo->child_reltype))
 				{
+					Query          *parse = context->root->parse;
+					RangeTblEntry  *rte = rt_fetch(appinfo->child_relid,
+												   parse->rtable);
+					if (!context->in_returning &&
+						rte->rtekind == RTE_SUBQUERY &&
+						rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+						var = (Var *)fixup_var_on_rls_subquery(rte, var);
+
 					Assert(var->vartype == appinfo->parent_reltype);
 					if (appinfo->parent_reltype != appinfo->child_reltype)
 					{
@@ -1708,7 +1779,16 @@ adjust_appendrel_attrs_mutator(Node *node,
 					return (Node *) rowexpr;
 				}
 			}
-			/* system attributes don't need any other translation */
+			else
+			{
+				Query		   *parse = context->root->parse;
+				RangeTblEntry  *rte = rt_fetch(appinfo->child_relid,
+											   parse->rtable);
+				if (!context->in_returning &&
+					rte->rtekind == RTE_SUBQUERY &&
+					rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+					return fixup_var_on_rls_subquery(rte, var);
+			}
 		}
 		return (Node *) var;
 	}
diff --git a/src/backend/optimizer/util/Makefile b/src/backend/optimizer/util/Makefile
index 3b2d16b..3430689 100644
--- a/src/backend/optimizer/util/Makefile
+++ b/src/backend/optimizer/util/Makefile
@@ -13,6 +13,6 @@ top_builddir = ../../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = clauses.o joininfo.o pathnode.o placeholder.o plancat.o predtest.o \
-       relnode.o restrictinfo.o tlist.o var.o
+       relnode.o restrictinfo.o tlist.o var.o rowlevelsec.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/optimizer/util/rowlevelsec.c b/src/backend/optimizer/util/rowlevelsec.c
new file mode 100644
index 0000000..1721bb5
--- /dev/null
+++ b/src/backend/optimizer/util/rowlevelsec.c
@@ -0,0 +1,685 @@
+/*
+ * optimizer/util/rowlvsec.c
+ *    Row-level security support routines
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ */
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits_fn.h"
+#include "catalog/pg_rowlevelsec.h"
+#include "catalog/pg_type.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "nodes/plannodes.h"
+#include "optimizer/clauses.h"
+#include "optimizer/prep.h"
+#include "optimizer/rowlevelsec.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+/* flags to pull row-level security policy */
+#define RLS_FLAG_HAS_SUBLINKS			0x0001
+
+/* hook to allow extensions to apply their own security policy */
+rowlevel_security_hook_type	rowlevel_security_hook = NULL;
+
+/*
+ * make_pseudo_column
+ *
+ * It makes TargetEntry that references underlying attribute. It may be
+ * Const node of dummy NULL, not Var node, if it is already dropped.
+ */
+static TargetEntry *
+make_pseudo_column(RangeTblEntry *subrte, AttrNumber attnum)
+{
+	Expr   *expr;
+	char   *resname;
+
+	Assert(subrte->rtekind == RTE_RELATION && OidIsValid(subrte->relid));
+	if (attnum == InvalidAttrNumber)
+	{
+		expr = (Expr *) makeWholeRowVar(subrte, (Index) 1, 0, false);
+		resname = get_rel_name(subrte->relid);
+	}
+	else
+	{
+		HeapTuple	tuple;
+		Form_pg_attribute	attform;
+
+		tuple = SearchSysCache2(ATTNUM,
+								ObjectIdGetDatum(subrte->relid),
+								Int16GetDatum(attnum));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+				 attnum, subrte->relid);
+		attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+		if (attform->attisdropped)
+		{
+			char	namebuf[NAMEDATALEN];
+
+			/* Insert NULL just for a placeholder of dropped column */
+			expr = (Expr *) makeConst(INT4OID,
+									  -1,
+									  InvalidOid,
+									  sizeof(int32),
+									  (Datum) 0,
+									  true,		/* isnull */
+									  true);	/* byval */
+			sprintf(namebuf, "dummy-%d", (int)attform->attnum);
+			resname = pstrdup(namebuf);
+		}
+		else
+		{
+			expr = (Expr *) makeVar((Index) 1,
+									attform->attnum,
+									attform->atttypid,
+									attform->atttypmod,
+									attform->attcollation,
+									0);
+			resname = pstrdup(NameStr(attform->attname));
+		}
+		ReleaseSysCache(tuple);
+	}
+	return makeTargetEntry(expr, -1, resname, false);
+}
+
+/*
+ * append_pseudo_system_column
+ *
+ * It returns attribute number of pseudo-column relevant to the supplied
+ * Var-node referencing the RLS sub-query. If required attribute is not
+ * in target-list, it also adds a new pseudo-column.
+ */
+static AttrNumber
+append_pseudo_system_column(RangeTblEntry *rte, Var *var)
+{
+	Query		   *subqry = rte->subquery;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+	ListCell	   *cell;
+
+	Assert(rte->rtekind == RTE_SUBQUERY &&
+		   rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY);
+
+	foreach (cell, subqry->targetList)
+	{
+		subtle = lfirst(cell);
+
+		/*
+		 * If referenced system column is already attached on the target-
+		 * list of RLS sub-query, nothing to do here.
+		 */
+		if (IsA(subtle->expr, Var))
+		{
+			Var	   *subvar = (Var *)subtle->expr;
+
+			if (var->varattno == subvar->varattno)
+			{
+				if (subtle->resjunk)
+					subtle->resjunk = false;
+				return subtle->resno;
+			}
+		}
+	}
+
+	/*
+	 * Here is no target-list for the referenced system column, so append
+	 * a new pseudo column on demand
+	 */
+	subrte = rt_fetch((Index) 1, subqry->rtable);
+	subtle = make_pseudo_column(subrte, var->varattno);
+	subtle->resno = list_length(subqry->targetList) + 1;
+
+	subqry->targetList = lappend(subqry->targetList, subtle);
+	rte->eref->colnames = lappend(rte->eref->colnames,
+								  makeString(pstrdup(subtle->resname)));
+	return subtle->resno;
+}
+
+/*
+ * fixup_varattno
+ *
+ * It recursively fixes up references to RLS sub-query, and adds pseudo-
+ * columns of underlying system columns, if necessary.
+ */
+typedef struct {
+	PlannerInfo	*root;
+	int		varlevelsup;
+	bool	is_returning;
+} fixup_varattno_context;
+
+static bool
+fixup_varattno_walker(Node *node, fixup_varattno_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var			   *var = (Var *) node;
+		RangeTblEntry  *rte;
+		ListCell	   *cell;
+
+		/* Var node does not reference Query node currently we focus on */
+		if (var->varlevelsup != context->varlevelsup)
+			return false;
+
+		/*
+		 * All the regular columns should already have its own pseudo
+		 * column on expansion of RTE. Its resno of TargetEntry is
+		 * identical with underlying attribute, so never need to fix-up
+		 * varattno of Var node that references the sub-query.
+		 */
+		if (var->varattno > InvalidAttrNumber)
+			return false;
+
+		rte = rt_fetch(var->varno, context->root->parse->rtable);
+		if (!context->is_returning &&
+			rte->rtekind == RTE_SUBQUERY &&
+			rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+		{
+			/*
+			 * If this Var node is system-column or whole-row reference
+			 * on RLS sub-queries, its varattno has to be adjusted to
+			 * reference correct pseudo column. Pseudo column entries
+			 * of them are not constructed at expansion time, we append
+			 * it on demand.
+			 */
+			var->varattno = append_pseudo_system_column(rte, var);
+		}
+		else if (rte->rtekind == RTE_RELATION && rte->inh)
+		{
+			/*
+			 * Also, if this Var node is system-columns or whole-row
+			 * reference on the parent relation of inheritance tree that
+			 * includes RLS sub-queries, even though the parent relation
+			 * itself was not expanded, its pseudo-column entries have to
+			 * be added on the underlying child relations.
+			 * However, no need to fix up varattno of Var node, because
+			 * it shall be handled in prep/prepunion.c.
+			 */
+			foreach (cell, context->root->append_rel_list)
+			{
+				AppendRelInfo *appinfo = lfirst(cell);
+
+				if (appinfo->parent_relid != var->varno)
+					continue;
+
+				rte = rt_fetch(appinfo->child_relid,
+							   context->root->parse->rtable);
+				if (rte->rtekind == RTE_SUBQUERY &&
+					rte->subquery->querySource == QSRC_ROW_LEVEL_SECURITY)
+					append_pseudo_system_column(rte, var);
+			}
+		}
+		return false;
+	}
+	else if (IsA(node, Query))
+	{
+		bool	result;
+
+		context->varlevelsup++;
+		result = query_tree_walker((Query *) node,
+								   fixup_varattno_walker,
+								   (void *) context, 0);
+		context->varlevelsup--;
+
+		return result;
+	}
+	return expression_tree_walker(node,
+								  fixup_varattno_walker,
+								  (void *) context);
+}
+
+/*
+ * check_infinite_recursion
+ *
+ * It is a wrong row-level security configuration, if we try to expand
+ * the relation inside of RLS sub-query originated from same relation!
+ */
+static void
+check_infinite_recursion(PlannerInfo *root, Oid relid)
+{
+	PlannerInfo	   *parent = root->parent_root;
+
+	if (parent && parent->parse->querySource == QSRC_ROW_LEVEL_SECURITY)
+	{
+		RangeTblEntry  *rte = rt_fetch(1, parent->parse->rtable);
+
+		Assert(rte->rtekind == RTE_RELATION && OidIsValid(rte->relid));
+
+		if (relid == rte->relid)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("infinite recursion detected for relation \"%s\"",
+							get_rel_name(relid))));
+		check_infinite_recursion(parent, relid);
+	}
+}
+
+/*
+ * fixup_plan_rowmark
+ *
+ * Push down the given PlanRowMark into RLS sub-query.
+ */
+static void
+fixup_plan_rowmark(RangeTblEntry *rte, PlanRowMark *rowmark)
+{
+	Query		   *subqry = rte->subquery;
+	RangeTblEntry  *subrte;
+	TargetEntry	   *subtle;
+
+	Assert(!rowmark->isParent);
+
+	if (rowmark->markType == ROW_MARK_EXCLUSIVE ||
+		rowmark->markType == ROW_MARK_SHARE)
+	{
+		RowMarkClause  *rclause = makeNode(RowMarkClause);
+
+		rclause->rti = (Index) 1;
+		if (rowmark->markType == ROW_MARK_EXCLUSIVE)
+			rclause->forUpdate = true;
+		else
+			rclause->forUpdate = false;
+		rclause->noWait = rowmark->noWait;
+		rclause->pushedDown = true;
+
+		subqry->rowMarks = lappend(subqry->rowMarks, rclause);
+	}
+	rowmark->markType = ROW_MARK_REFERENCE;
+
+	/*
+	 * Add 'ctid' and 'tableoid' pseudo columns to be required for
+	 * row-level locks
+	 */
+	subrte = rt_fetch(1, subqry->rtable);
+
+	subtle = make_pseudo_column(subrte, SelfItemPointerAttributeNumber);
+	subtle->resno = list_length(subqry->targetList) + 1;
+	subqry->targetList = lappend(subqry->targetList, subtle);
+	rte->eref->colnames = lappend(rte->eref->colnames,
+		makeString(pstrdup(subtle->resname)));
+
+	subtle = make_pseudo_column(subrte, TableOidAttributeNumber);
+	subtle->resno = list_length(subqry->targetList) + 1;
+	subqry->targetList = lappend(subqry->targetList, subtle);
+	rte->eref->colnames = lappend(rte->eref->colnames,
+		makeString(pstrdup(subtle->resname)));
+}
+
+/*
+ * expand_rtentry_rls
+ *
+ * It replaces the supplied RangeTblEntry (should be RTE_RELATION) by RLS
+ * sub-query with configured row-level security policy.
+ * This sub-query should have pseudo-column relevant to the regular columns,
+ * but no pseudo-columns for system-column or whole-row reference without
+ * references to them.
+ */
+static void
+expand_rtentry_rls(PlannerInfo *root, Index rtindex, Expr *qual, int flags)
+{
+	Query		   *parse = root->parse;
+	RangeTblEntry  *rte = rt_fetch(rtindex, parse->rtable);
+	Query		   *subqry;
+	RangeTblEntry  *subrte;
+	RangeTblRef	   *subrtr;
+	TargetEntry	   *subtle;
+	HeapTuple		tuple;
+	AttrNumber		nattrs;
+	AttrNumber		attnum;
+	List		   *targetList = NIL;
+	List		   *colNameList = NIL;
+	PlanRowMark	   *rowmark;
+
+	/* check recursion to prevent infinite loop */
+	check_infinite_recursion(root, rte->relid);
+
+	/* Expand views inside SubLink node */
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		QueryRewriteExpr((Node *)qual, list_make1_oid(rte->relid));
+
+	/*
+	 * Construction of sub-query
+	 */
+	subqry = (Query *) makeNode(Query);
+	subqry->commandType = CMD_SELECT;
+	subqry->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	subrte = copyObject(rte);
+	subqry->rtable = list_make1(subrte);
+
+	subrtr = makeNode(RangeTblRef);
+	subrtr->rtindex = 1;
+	subqry->jointree = makeFromExpr(list_make1(subrtr), (Node *) qual);
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		subqry->hasSubLinks = true;
+
+	/*
+	 * Construct pseudo columns as TargetEntry of sub-query that
+	 * references a particular regular attribute of the underlying
+	 * relation.
+	 */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(rte->relid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for relation %u", rte->relid);
+	nattrs = ((Form_pg_class) GETSTRUCT(tuple))->relnatts;
+	ReleaseSysCache(tuple);
+
+	for (attnum = 1; attnum <= nattrs; attnum++)
+	{
+		subtle = make_pseudo_column(subrte, attnum);
+		subtle->resno = list_length(targetList) + 1;
+		Assert(subtle->resno == attnum);
+
+		targetList = lappend(targetList, subtle);
+		colNameList = lappend(colNameList,
+							  makeString(pstrdup(subtle->resname)));
+	}
+	subqry->targetList = targetList;
+
+	/* Replace the original RengeTblEntry by sub-query */
+	/* XXX - relid has to be kept */
+	rte->rtekind = RTE_SUBQUERY;
+	rte->subquery = subqry;
+	rte->security_barrier = true;
+
+	/* no permission checks on subquery itself */
+	rte->requiredPerms = 0;
+	rte->checkAsUser = InvalidOid;
+	rte->selectedCols = NULL;
+	rte->modifiedCols = NULL;
+
+	rte->eref = makeAlias(get_rel_name(rte->relid), colNameList);
+
+	/*
+	 * Push-down of PlanRowMark if needed
+	 */
+	rowmark = get_plan_rowmark(root->rowMarks, rtindex);
+	if (rowmark)
+		fixup_plan_rowmark(rte, rowmark);
+}
+
+/*
+ * pull_row_level_security
+ *
+ * It pulls the configured row-level security policy of both built-in
+ * and extensions. If any, it returns expression tree.
+ */
+static Expr *
+pull_row_level_security(Relation relation, int *p_flags)
+{
+	Expr   *quals = NULL;
+	int		flags = 0;
+
+	/*
+	 * Pull the row-level security policy configured with built-in
+	 * features, if unprivileged users. Please note that superuser
+	 * can bypass it.
+	 */
+	if (relation->rlsdesc && !superuser())
+	{
+		RowLevelSecDesc *rlsdesc = relation->rlsdesc;
+
+		quals = copyObject(rlsdesc->rlsqual);
+		if (rlsdesc->rlshassublinks)
+			flags |= RLS_FLAG_HAS_SUBLINKS;
+	}
+
+	/*
+	 * Also, ask extensions whether they want to apply their own
+	 * row-level security policy. If both built-in and extension
+	 * has their own policy, it shall be merged.
+	 */
+	if (rowlevel_security_hook)
+	{
+		List   *temp;
+
+		temp = (*rowlevel_security_hook)(relation);
+		if (temp != NIL)
+		{
+			if ((flags & RLS_FLAG_HAS_SUBLINKS) == 0 &&
+				contain_subplans((Node *) temp))
+				flags |= RLS_FLAG_HAS_SUBLINKS;
+
+			if (quals != NULL)
+				temp = lappend(temp, quals);
+
+			if (list_length(temp) == 1)
+				quals = (Expr *)list_head(temp);
+			else if (list_length(temp) > 1)
+				quals = makeBoolExpr(AND_EXPR, temp, -1);
+		}
+	}
+	*p_flags = flags;
+	return quals;
+}
+
+/*
+ * copy_row_level_security
+ *
+ * It construct a RLS sub-query instead of raw COPY TO statement,
+ * if target relation has a row-level security policy
+ */
+bool
+copy_row_level_security(CopyStmt *stmt, Relation rel, List *attnums)
+{
+	Expr		  *quals;
+	int			   flags;
+	Query		  *parse;
+	RangeTblEntry  *rte;
+	RangeTblRef	   *rtr;
+	TargetEntry	   *tle;
+	Var			   *var;
+	ListCell	   *cell;
+
+	if (stmt->is_from)
+		return false;
+
+	quals = pull_row_level_security(rel, &flags);
+	if (!quals)
+		return false;
+
+	parse = (Query *) makeNode(Query);
+	parse->commandType = CMD_SELECT;
+	parse->querySource = QSRC_ROW_LEVEL_SECURITY;
+
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(rel);
+	rte->relkind = RelationGetForm(rel)->relkind;
+
+	foreach (cell, attnums)
+	{
+		HeapTuple	tuple;
+		Form_pg_attribute	attform;
+		AttrNumber	attno = lfirst_int(cell);
+
+		tuple = SearchSysCache2(ATTNUM,
+								ObjectIdGetDatum(RelationGetRelid(rel)),
+								Int16GetDatum(attno));
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for attribute %d of relation %s",
+				 attno, RelationGetRelationName(rel));
+		attform = (Form_pg_attribute) GETSTRUCT(tuple);
+
+		var = makeVar((Index) 1,
+					  attform->attnum,
+					  attform->atttypid,
+					  attform->atttypmod,
+					  attform->attcollation,
+					  0);
+		tle = makeTargetEntry((Expr *) var,
+							  list_length(parse->targetList) + 1,
+							  pstrdup(NameStr(attform->attname)),
+							  false);
+		parse->targetList = lappend(parse->targetList, tle);
+
+		ReleaseSysCache(tuple);
+
+		rte->selectedCols = bms_add_member(rte->selectedCols,
+								attno - FirstLowInvalidHeapAttributeNumber);
+	}
+	rte->inFromCl = true;
+	rte->requiredPerms = ACL_SELECT;
+
+	rtr = makeNode(RangeTblRef);
+	rtr->rtindex = 1;
+
+	parse->jointree = makeFromExpr(list_make1(rtr), (Node *) quals);
+	parse->rtable = list_make1(rte);
+	if (flags & RLS_FLAG_HAS_SUBLINKS)
+		parse->hasSubLinks = true;
+
+	stmt->query = (Node *) parse;
+
+	return true;
+}
+
+/*
+ * apply_row_level_security
+ *
+ * Entrypoint to apply configured row-level security policy of the relation.
+ *
+ * In case when the supplied query references relations with row-level
+ * security policy, its RangeTblEntry shall be replaced by a RLS sub-query
+ * that has simple scan on the referenced table with policy qualifiers.
+ * Of course, security-barrier shall be set on the sub-query to prevent
+ * unexpected push-down of functions without leakproof flag.
+ *
+ * For example, when table t1 has a security policy "(x % 2 = 0)", the
+ * following query:
+ *   SELECT * FROM t1 WHERE f_leak(y)
+ * performs as if
+ *   SELECT * FROM (
+ *     SELECT x, y FROM t1 WHERE (x % 2 = 0)
+ *   ) AS t1 WHERE f_leak(y)
+ * would be given. Because the sub-query has security barrier flag, 
+ * configured security policy qualifier is always executed prior to
+ * user given functions.
+ */
+void
+apply_row_level_security(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	Oid			curr_userid;
+	int			curr_seccxt;
+	Index		rtindex;
+	bool		has_rowlevel_security = false;
+
+	/*
+	 * Mode checks. In case when SECURITY_ROW_LEVEL_DISABLED is set,
+	 * no row-level security policy should be applied regardless
+	 * whether it is built-in or extension.
+	 */
+	GetUserIdAndSecContext(&curr_userid, &curr_seccxt);
+	if (curr_seccxt & SECURITY_ROW_LEVEL_DISABLED)
+		return;
+
+	for (rtindex = 1; rtindex <= list_length(parse->rtable); rtindex++)
+	{
+		RangeTblEntry  *rte = rt_fetch(rtindex, parse->rtable);
+		Relation	rel;
+		Expr	   *quals;
+		int			flags;
+
+		/* only relation can have row-level security policy */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * Parent relation of inheritance tree is just a placeholder here.
+		 * So, no need to apply row-level security.
+		 */
+		if (rte->inh)
+			continue;
+
+		/*
+		 * It does not make sense to check row-level security policy on
+		 * the target relation of INSERT command.
+		 */
+		if (parse->commandType == CMD_INSERT &&
+			parse->resultRelation == rtindex)
+			continue;
+
+		/*
+		 * It does not make sense to apply row-level security policy on
+		 * the relation we already handled.
+		 * Note that the underlying relation never have inh==true.
+		 */
+		if (parse->querySource == QSRC_ROW_LEVEL_SECURITY &&
+			rtindex == 1)
+			continue;
+
+		/*
+		 * OK, it is a reference to "real" relation. Let's try to apply
+		 * row-level security policy being configured, if any.
+		 */
+		rel = heap_open(rte->relid, NoLock);
+
+		quals = pull_row_level_security(rel, &flags);
+		if (quals)
+		{
+			expand_rtentry_rls(root, rtindex, quals, flags);
+			has_rowlevel_security = true;
+		}
+		heap_close(rel, NoLock);
+	}
+
+	if (has_rowlevel_security)
+	{
+		PlannerGlobal  *glob = root->glob;
+		PlanInvalItem  *pi_item;
+		fixup_varattno_context context;
+
+		/*
+		 * XXX - Constructed Plan with row-level security policy depends
+		 * on properties of current used (database superuser can bypass
+		 * configured RLS policy), thus, it has to be invalidated when
+		 * its assumption was changed.
+		 */
+		if (!OidIsValid(glob->planUserId))
+		{
+			/* Plan invalidation on session user-id */
+			glob->planUserId = GetUserId();
+
+			/* Plan invalidation on catalog updates of pg_authid */
+			pi_item = makeNode(PlanInvalItem);
+			pi_item->cacheId = AUTHOID;
+			pi_item->hashValue =
+				GetSysCacheHashValue1(AUTHOID,
+									  ObjectIdGetDatum(glob->planUserId));
+			glob->invalItems = lappend(glob->invalItems, pi_item);
+		}
+		else
+			Assert(glob->planUserId == GetUserId());
+
+		/*
+		 * XXX - varattno of Var node that references the RangeTblEntry
+		 * being replaced by RLS sub-query has to be adjusted for proper
+		 * reference to the underlying pseudo-column of the relation.
+		 */
+		context.root = root;
+		context.varlevelsup = 0;
+		context.is_returning = false;
+		query_tree_walker(parse,
+						  fixup_varattno_walker,
+						  (void *) &context,
+						  QTW_IGNORE_RETURNING);
+		context.is_returning = true;
+		expression_tree_walker((Node *)parse->returningList,
+							   fixup_varattno_walker,
+							   (void *) &context);
+	}
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index e4ff76e..a3d445d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2127,6 +2127,22 @@ alter_table_cmd:
 					n->def = (Node *)$2;
 					$$ = (Node *)n;
 				}
+			/* ALTER TABLE <name> SET ROW LEVEL SECURITY (expression) */
+			| SET ROW LEVEL SECURITY '(' a_expr ')'
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_SetRowLevelSecurity;
+					n->def = (Node *) $6;
+					$$ = (Node *)n;
+				}
+			/* ALTER TABLE <name> RESET ROW LEVEL SECURITY */
+			| RESET ROW LEVEL SECURITY
+				{
+					AlterTableCmd *n = makeNode(AlterTableCmd);
+					n->subtype = AT_ResetRowLevelSecurity;
+					n->def = NULL;
+					$$ = (Node *)n;
+				}
 			| alter_generic_options
 				{
 					AlterTableCmd *n = makeNode(AlterTableCmd);
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index b75b2d9..9494889 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -269,6 +269,9 @@ transformAggregateCall(ParseState *pstate, Aggref *agg,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("aggregate functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("aggregate functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
@@ -537,6 +540,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
 		case EXPR_KIND_TRIGGER_WHEN:
 			err = _("window functions are not allowed in trigger WHEN conditions");
 			break;
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			err = _("window functions are not allowed in row-level security");
+			break;
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e9267c5..94c25d1 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1443,6 +1443,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
 		case EXPR_KIND_VALUES:
+		case EXPR_KIND_ROW_LEVEL_SEC:
 			/* okay */
 			break;
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -2609,6 +2610,8 @@ ParseExprKindName(ParseExprKind exprKind)
 			return "EXECUTE";
 		case EXPR_KIND_TRIGGER_WHEN:
 			return "WHEN";
+		case EXPR_KIND_ROW_LEVEL_SEC:
+			return "ROW LEVEL SECURITY";
 
 			/*
 			 * There is intentionally no default: case here, so that the
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index b785c26..12e7dd7 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2250,3 +2250,19 @@ QueryRewrite(Query *parsetree)
 
 	return results;
 }
+
+/*
+ * QueryRewriteExpr
+ *
+ * This routine provides an entry point of query rewriter towards
+ * a certain expression tree with SubLink node; being added after
+ * the top level query rewrite.
+ * It primarily intends to expand views appeared in the qualifiers
+ * appended with row-level security which needs to modify query
+ * tree at head of the planner stage.
+ */
+void
+QueryRewriteExpr(Node *node, List *activeRIRs)
+{
+	fireRIRonSubLink(node, activeRIRs);
+}
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 97e68b1..e54f92f 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -2999,6 +2999,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 	int			spi_result;
 	Oid			save_userid;
 	int			save_sec_context;
+	int			temp_sec_context;
 	Datum		vals[RI_MAX_NUMKEYS * 2];
 	char		nulls[RI_MAX_NUMKEYS * 2];
 
@@ -3078,8 +3079,18 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo,
 
 	/* Switch to proper UID to perform check as */
 	GetUserIdAndSecContext(&save_userid, &save_sec_context);
+
+	/*
+	 * Row-level security should be disabled in case when foreign-key
+	 * relation is queried to check existence of tuples that references
+	 * the primary-key being modified.
+	 */
+	temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE;
+	if (source_is_pk)
+		temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED;
+
 	SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner,
-						   save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+						   temp_sec_context);
 
 	/* Finally we can run the query. */
 	spi_result = SPI_execute_snapshot(qplan,
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 8c0391f..36a8750 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -51,6 +51,7 @@
 #include "catalog/namespace.h"
 #include "executor/executor.h"
 #include "executor/spi.h"
+#include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/planmain.h"
 #include "optimizer/prep.h"
@@ -665,6 +666,16 @@ CheckCachedPlan(CachedPlanSource *plansource)
 		AcquireExecutorLocks(plan->stmt_list, true);
 
 		/*
+		 * If plan was constructed with assumption of a particular user-id,
+		 * and it is different from the current one, the cached-plan shall
+		 * be invalidated to construct suitable query plan.
+		 */
+		if (plan->is_valid &&
+			OidIsValid(plan->planUserId) &&
+			plan->planUserId == GetUserId())
+			plan->is_valid = false;
+
+		/*
 		 * If plan was transient, check to see if TransactionXmin has
 		 * advanced, and if so invalidate it.
 		 */
@@ -716,6 +727,8 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 {
 	CachedPlan *plan;
 	List	   *plist;
+	ListCell   *cell;
+	Oid			planUserId = InvalidOid;
 	bool		snapshot_set;
 	bool		spi_pushed;
 	MemoryContext plan_context;
@@ -794,6 +807,24 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	PopOverrideSearchPath();
 
 	/*
+	 * Check whether the generated plan assumes a particular user-id, or not.
+	 * In case when a valid user-id is recorded on PlannedStmt->planUserId,
+	 * it should be kept and used to validation check of the cached plan
+	 * under the "current" user-id.
+	 */
+	foreach (cell, plist)
+	{
+		PlannedStmt	*pstmt = lfirst(cell);
+
+		if (IsA(pstmt, PlannedStmt) && OidIsValid(pstmt->planUserId))
+		{
+			Assert(!OidIsValid(planUserId) || planUserId == pstmt->planUserId);
+
+			planUserId = pstmt->planUserId;
+		}
+	}
+
+	/*
 	 * Make a dedicated memory context for the CachedPlan and its subsidiary
 	 * data.  It's probably not going to be large, but just in case, use the
 	 * default maxsize parameter.  It's transient for the moment.
@@ -828,6 +859,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	plan->context = plan_context;
 	plan->is_saved = false;
 	plan->is_valid = true;
+	plan->planUserId = planUserId;
 
 	/* assign generation number to new plan */
 	plan->generation = ++(plansource->generation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 8c9ebe0..28ba831 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -49,6 +49,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_rewrite.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -896,6 +897,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt)
 	else
 		relation->trigdesc = NULL;
 
+	if (relation->rd_rel->relhasrowlevelsec)
+		RelationBuildRowLevelSecurity(relation);
+	else
+		relation->rlsdesc = NULL;
+
 	/*
 	 * if it's an index, initialize index-related information
 	 */
@@ -1785,6 +1791,8 @@ RelationDestroyRelation(Relation relation)
 		MemoryContextDelete(relation->rd_indexcxt);
 	if (relation->rd_rulescxt)
 		MemoryContextDelete(relation->rd_rulescxt);
+	if (relation->rlsdesc)
+		MemoryContextDelete(relation->rlsdesc->rlscxt);
 	pfree(relation);
 }
 
@@ -3024,7 +3032,13 @@ RelationCacheInitializePhase3(void)
 				relation->rd_rel->relhastriggers = false;
 			restart = true;
 		}
-
+		if (relation->rd_rel->relhasrowlevelsec && relation->rlsdesc == NULL)
+		{
+			RelationBuildRowLevelSecurity(relation);
+			if (relation->rlsdesc == NULL)
+				relation->rd_rel->relhasrowlevelsec = false;
+			restart = true;
+		}
 		/* Release hold on the relation */
 		RelationDecrementReferenceCount(relation);
 
@@ -4162,6 +4176,7 @@ load_relcache_init_file(bool shared)
 		rel->rd_rules = NULL;
 		rel->rd_rulescxt = NULL;
 		rel->trigdesc = NULL;
+		rel->rlsdesc = NULL;
 		rel->rd_indexprs = NIL;
 		rel->rd_indpred = NIL;
 		rel->rd_exclops = NULL;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 82330cb..694fa95 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3908,6 +3908,7 @@ getTables(Archive *fout, int *numTables)
 	int			i_reloptions;
 	int			i_toastreloptions;
 	int			i_reloftype;
+	int			i_rlsqual;
 
 	/* Make sure we are in proper schema */
 	selectSourceSchema(fout, "pg_catalog");
@@ -3932,7 +3933,45 @@ getTables(Archive *fout, int *numTables)
 	 * we cannot correctly identify inherited columns, owned sequences, etc.
 	 */
 
-	if (fout->remoteVersion >= 90100)
+	if (fout->remoteVersion >= 90300)
+	{
+		/*
+		 * Left join to pick up dependency info linking sequences to their
+		 * owning column, if any (note this dependency is AUTO as of 8.2)
+		 */
+		appendPQExpBuffer(query,
+						  "SELECT c.tableoid, c.oid, c.relname, "
+						  "c.relacl, c.relkind, c.relnamespace, "
+						  "(%s c.relowner) AS rolname, "
+						  "c.relchecks, c.relhastriggers, "
+						  "c.relhasindex, c.relhasrules, c.relhasoids, "
+						  "c.relfrozenxid, tc.oid AS toid, "
+						  "tc.relfrozenxid AS tfrozenxid, "
+						  "c.relpersistence, "
+						  "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, "
+						  "d.refobjid AS owning_tab, "
+						  "d.refobjsubid AS owning_col, "
+						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
+						"array_to_string(c.reloptions, ', ') AS reloptions, "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "pg_catalog.pg_get_expr(rls.rlsqual, rls.rlsrelid) AS rlsqual "
+						  "FROM pg_class c "
+						  "LEFT JOIN pg_depend d ON "
+						  "(c.relkind = '%c' AND "
+						  "d.classid = c.tableoid AND d.objid = c.oid AND "
+						  "d.objsubid = 0 AND "
+						  "d.refclassid = c.tableoid AND d.deptype = 'a') "
+					   "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) "
+					   "LEFT JOIN pg_rowlevelsec rls ON (c.oid = rls.rlsrelid) "
+						  "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c') "
+						  "ORDER BY c.oid",
+						  username_subquery,
+						  RELKIND_SEQUENCE,
+						  RELKIND_RELATION, RELKIND_SEQUENCE,
+						  RELKIND_VIEW, RELKIND_COMPOSITE_TYPE,
+						  RELKIND_FOREIGN_TABLE);
+	}
+	else if (fout->remoteVersion >= 90100)
 	{
 		/*
 		 * Left join to pick up dependency info linking sequences to their
@@ -3952,7 +3991,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL as rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -3988,7 +4028,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4023,7 +4064,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions "
+						  "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4058,7 +4100,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						"array_to_string(c.reloptions, ', ') AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4094,7 +4137,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4129,7 +4173,8 @@ getTables(Archive *fout, int *numTables)
 						  "d.refobjsubid AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "LEFT JOIN pg_depend d ON "
 						  "(c.relkind = '%c' AND "
@@ -4160,7 +4205,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4186,7 +4232,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class "
 						  "WHERE relkind IN ('%c', '%c', '%c') "
 						  "ORDER BY oid",
@@ -4222,7 +4269,8 @@ getTables(Archive *fout, int *numTables)
 						  "NULL::int4 AS owning_col, "
 						  "NULL AS reltablespace, "
 						  "NULL AS reloptions, "
-						  "NULL AS toast_reloptions "
+						  "NULL AS toast_reloptions, "
+						  "NULL AS rlsqual "
 						  "FROM pg_class c "
 						  "WHERE relkind IN ('%c', '%c') "
 						  "ORDER BY oid",
@@ -4270,6 +4318,7 @@ getTables(Archive *fout, int *numTables)
 	i_reloptions = PQfnumber(res, "reloptions");
 	i_toastreloptions = PQfnumber(res, "toast_reloptions");
 	i_reloftype = PQfnumber(res, "reloftype");
+	i_rlsqual = PQfnumber(res, "rlsqual");
 
 	if (lockWaitTimeout && fout->remoteVersion >= 70300)
 	{
@@ -4312,6 +4361,10 @@ getTables(Archive *fout, int *numTables)
 			tblinfo[i].reloftype = NULL;
 		else
 			tblinfo[i].reloftype = pg_strdup(PQgetvalue(res, i, i_reloftype));
+		if (PQgetisnull(res, i, i_rlsqual))
+			tblinfo[i].rlsqual = NULL;
+		else
+			tblinfo[i].rlsqual = pg_strdup(PQgetvalue(res, i, i_rlsqual));
 		tblinfo[i].ncheck = atoi(PQgetvalue(res, i, i_relchecks));
 		if (PQgetisnull(res, i, i_owning_tab))
 		{
@@ -12847,6 +12900,9 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
 			}
 		}
 	}
+	if (tbinfo->rlsqual)
+		appendPQExpBuffer(q, "ALTER TABLE ONLY %s SET ROW LEVEL SECURITY %s;\n",
+						  fmtId(tbinfo->dobj.name), tbinfo->rlsqual);
 
 	if (binary_upgrade)
 		binary_upgrade_extension_member(q, &tbinfo->dobj, labelq->data);
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2100d43..57bd58b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -257,6 +257,7 @@ typedef struct _tableInfo
 	uint32		toast_frozenxid;	/* for restore toast frozen xid */
 	int			ncheck;			/* # of CHECK expressions */
 	char	   *reloftype;		/* underlying type for typed table */
+	char	   *rlsqual;		/* row-level security policy */
 	/* these two are set only if table is a sequence owned by a column: */
 	Oid			owning_tab;		/* OID of table owning sequence */
 	int			owning_col;		/* attr # of column owning sequence */
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 8499768..49baa0e 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -147,6 +147,7 @@ typedef enum ObjectClass
 	OCLASS_DEFACL,				/* pg_default_acl */
 	OCLASS_EXTENSION,			/* pg_extension */
 	OCLASS_EVENT_TRIGGER,		/* pg_event_trigger */
+	OCLASS_ROWLEVELSEC,			/* pg_rowlevelsec */
 	MAX_OCLASS					/* MUST BE LAST */
 } ObjectClass;
 
diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h
index 238fe58..a3b07e2 100644
--- a/src/include/catalog/indexing.h
+++ b/src/include/catalog/indexing.h
@@ -311,6 +311,9 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree(
 DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops));
 #define RangeTypidIndexId					3542
 
+DECLARE_UNIQUE_INDEX(pg_rowlevelsec_relid_index, 3839, on pg_rowlevelsec using btree(rlsrelid oid_ops));
+#define RowLevelSecurityIndexId				3839
+
 /* last step of initialization script: build the indexes declared above */
 BUILD_INDICES
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index f83ce80..5b6e38b 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO
 	bool		relhaspkey;		/* has (or has had) PRIMARY KEY index */
 	bool		relhasrules;	/* has (or has had) any rules */
 	bool		relhastriggers; /* has (or has had) any TRIGGERs */
+	bool		relhasrowlevelsec;	/* has (or has had) row-level security */
 	bool		relhassubclass; /* has (or has had) derived classes */
 	TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */
 
@@ -91,7 +92,7 @@ typedef FormData_pg_class *Form_pg_class;
  * ----------------
  */
 
-#define Natts_pg_class					27
+#define Natts_pg_class					28
 #define Anum_pg_class_relname			1
 #define Anum_pg_class_relnamespace		2
 #define Anum_pg_class_reltype			3
@@ -115,10 +116,11 @@ typedef FormData_pg_class *Form_pg_class;
 #define Anum_pg_class_relhaspkey		21
 #define Anum_pg_class_relhasrules		22
 #define Anum_pg_class_relhastriggers	23
-#define Anum_pg_class_relhassubclass	24
-#define Anum_pg_class_relfrozenxid		25
-#define Anum_pg_class_relacl			26
-#define Anum_pg_class_reloptions		27
+#define Anum_pg_class_relhasrowlevelsec	24
+#define Anum_pg_class_relhassubclass	25
+#define Anum_pg_class_relfrozenxid		26
+#define Anum_pg_class_relacl			27
+#define Anum_pg_class_reloptions		28
 
 /* ----------------
  *		initial contents of pg_class
@@ -130,13 +132,13 @@ typedef FormData_pg_class *Form_pg_class;
  */
 
 /* Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId */
-DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1247 (  pg_type		PGNSP 71 0 PGUID 0 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1249 (  pg_attribute	PGNSP 75 0 PGUID 0 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1255 (  pg_proc		PGNSP 81 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
-DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 27 0 t f f f f 3 _null_ _null_ ));
+DATA(insert OID = 1259 (  pg_class		PGNSP 83 0 PGUID 0 0 0 0 0 0 0 0 f f p r 28 0 t f f f f f 3 _null_ _null_ ));
 DESCR("");
 
 
diff --git a/src/include/catalog/pg_rowlevelsec.h b/src/include/catalog/pg_rowlevelsec.h
new file mode 100644
index 0000000..41fcb21
--- /dev/null
+++ b/src/include/catalog/pg_rowlevelsec.h
@@ -0,0 +1,59 @@
+/*
+ * pg_rowlevelsec.h
+ *   definition of the system catalog for row-level security policy
+ *   (pg_rowlevelsec)
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ */
+#ifndef PG_ROWLEVELSEC_H
+#define PG_ROWLEVELSEC_H
+
+#include "catalog/genbki.h"
+#include "nodes/primnodes.h"
+#include "utils/memutils.h"
+#include "utils/relcache.h"
+
+/* ----------------
+ *		pg_rowlevelsec definition. cpp turns this into
+ *		typedef struct FormData_pg_rowlevelsec
+ * ----------------
+ */
+#define RowLevelSecurityRelationId	3838
+
+CATALOG(pg_rowlevelsec,3838) BKI_WITHOUT_OIDS
+{
+	Oid				rlsrelid;
+#ifdef CATALOG_VARLEN
+	pg_node_tree	rlsqual;
+#endif
+} FormData_pg_rowlevelsec;
+
+/* ----------------
+ *		Form_pg_rowlevelsec corresponds to a pointer to a row with
+ *		the format of pg_rowlevelsec relation.
+ * ----------------
+ */
+typedef FormData_pg_rowlevelsec *Form_pg_rowlevelsec;
+
+/* ----------------
+ * 		compiler constants for pg_rowlevelsec
+ * ----------------
+ */
+#define Natts_pg_rowlevelsec				2
+#define Anum_pg_rowlevelsec_rlsrelid		1
+#define Anum_pg_rowlevelsec_rlsqual			2
+
+typedef struct
+{
+	MemoryContext	rlscxt;
+	Expr		   *rlsqual;
+	bool			rlshassublinks;
+} RowLevelSecDesc;
+
+extern void	RelationBuildRowLevelSecurity(Relation relation);
+extern void	SetRowLevelSecurity(Relation relation, Node *clause);
+extern void	RemoveRowLevelSecurityById(Oid relationId);
+
+#endif  /* PG_ROWLEVELSEC_H */
diff --git a/src/include/commands/copy.h b/src/include/commands/copy.h
index 8680ac3..1c49dfa 100644
--- a/src/include/commands/copy.h
+++ b/src/include/commands/copy.h
@@ -21,7 +21,7 @@
 /* CopyStateData is private in commands/copy.c */
 typedef struct CopyStateData *CopyState;
 
-extern uint64 DoCopy(const CopyStmt *stmt, const char *queryString);
+extern uint64 DoCopy(CopyStmt *stmt, const char *queryString);
 
 extern void ProcessCopyOptions(CopyState cstate, bool is_from, List *options);
 extern CopyState BeginCopyFrom(Relation rel, const char *filename,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 3ea3493..8d7a9ad 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -277,6 +277,7 @@ extern int	trace_recovery(int trace_level);
 /* flags to be OR'd to form sec_context */
 #define SECURITY_LOCAL_USERID_CHANGE	0x0001
 #define SECURITY_RESTRICTED_OPERATION	0x0002
+#define SECURITY_ROW_LEVEL_DISABLED		0x0004
 
 extern char *DatabasePath;
 
diff --git a/src/include/nodes/nodeFuncs.h b/src/include/nodes/nodeFuncs.h
index e609e4b..a1d98e4 100644
--- a/src/include/nodes/nodeFuncs.h
+++ b/src/include/nodes/nodeFuncs.h
@@ -24,6 +24,7 @@
 #define QTW_IGNORE_RANGE_TABLE		0x08		/* skip rangetable entirely */
 #define QTW_EXAMINE_RTES			0x10		/* examine RTEs */
 #define QTW_DONT_COPY_QUERY			0x20		/* do not copy top Query */
+#define QTW_IGNORE_RETURNING		0x40		/* skip returning clause */
 
 
 extern Oid	exprType(const Node *expr);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 8834499..a771727 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -31,7 +31,8 @@ typedef enum QuerySource
 	QSRC_PARSER,				/* added by parse analysis (now unused) */
 	QSRC_INSTEAD_RULE,			/* added by unconditional INSTEAD rule */
 	QSRC_QUAL_INSTEAD_RULE,		/* added by conditional INSTEAD rule */
-	QSRC_NON_INSTEAD_RULE		/* added by non-INSTEAD rule */
+	QSRC_NON_INSTEAD_RULE,		/* added by non-INSTEAD rule */
+	QSRC_ROW_LEVEL_SECURITY,	/* added by row-level security */
 } QuerySource;
 
 /* Sort ordering options for ORDER BY and CREATE INDEX */
@@ -700,6 +701,13 @@ typedef struct RangeTblEntry
 
 	/*
 	 * Fields valid for a plain relation RTE (else zero):
+	 *
+	 * XXX - Query optimizer may modify and replace RangeTblEntry on
+	 * a particular relation by sub-query, but should perform as result
+	 * relation of the query. In this case, relid field is used to track
+	 * which relation is the sub-query originated.
+	 * Right now, only row-level security feature uses this field to track
+	 * the relation-id of sub-query being originated.
 	 */
 	Oid			relid;			/* OID of the relation */
 	char		relkind;		/* relation kind (see pg_class.relkind) */
@@ -1233,6 +1241,8 @@ typedef enum AlterTableType
 	AT_DropInherit,				/* NO INHERIT parent */
 	AT_AddOf,					/* OF <type_name> */
 	AT_DropOf,					/* NOT OF */
+	AT_SetRowLevelSecurity,		/* SET ROW LEVEL SECURITY (...) */
+	AT_ResetRowLevelSecurity,	/* RESET ROW LEVEL SECURITY */
 	AT_GenericOptions			/* OPTIONS (...) */
 } AlterTableType;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index fb9a863..6b3ea3d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -67,6 +67,8 @@ typedef struct PlannedStmt
 	List	   *invalItems;		/* other dependencies, as PlanInvalItems */
 
 	int			nParamExec;		/* number of PARAM_EXEC Params used */
+
+	Oid			planUserId;		/* user-id this plan assumed, or InvalidOid */
 } PlannedStmt;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 0a1f8d5..b195dd7 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -98,6 +98,8 @@ typedef struct PlannerGlobal
 	Index		lastRowMarkId;	/* highest PlanRowMark ID assigned */
 
 	bool		transientPlan;	/* redo plan when TransactionXmin changes? */
+
+	Oid			planUserId;		/* User-Id to be assumed on this plan */
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/optimizer/rowlevelsec.h b/src/include/optimizer/rowlevelsec.h
new file mode 100644
index 0000000..c8e0019
--- /dev/null
+++ b/src/include/optimizer/rowlevelsec.h
@@ -0,0 +1,25 @@
+/* -------------------------------------------------------------------------
+ *
+ * rowlevelsec.h
+ *    prototypes for optimizer/rowlevelsec.c
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * -------------------------------------------------------------------------
+ */
+#ifndef ROWLEVELSEC_H
+#define ROWLEVELSEC_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/relation.h"
+#include "utils/rel.h"
+
+typedef List *(*rowlevel_security_hook_type)(Relation relation);
+extern PGDLLIMPORT rowlevel_security_hook_type rowlevel_security_hook;
+
+extern bool	copy_row_level_security(CopyStmt *stmt,
+									Relation relation, List *attnums);
+extern void	 apply_row_level_security(PlannerInfo *root);
+
+#endif	/* ROWLEVELSEC_H */
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index aa9c648..3a1e4ea 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -62,7 +62,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_INDEX_PREDICATE,		/* index predicate */
 	EXPR_KIND_ALTER_COL_TRANSFORM,	/* transform expr in ALTER COLUMN TYPE */
 	EXPR_KIND_EXECUTE_PARAMETER,	/* parameter value in EXECUTE */
-	EXPR_KIND_TRIGGER_WHEN			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_TRIGGER_WHEN,			/* WHEN condition in CREATE TRIGGER */
+	EXPR_KIND_ROW_LEVEL_SEC,		/* policy of ROW LEVEL SECURITY */
 } ParseExprKind;
 
 
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 50625d4..d470cad 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -18,6 +18,7 @@
 #include "nodes/parsenodes.h"
 
 extern List *QueryRewrite(Query *parsetree);
+extern void	QueryRewriteExpr(Node *node, List *activeRIRs);
 extern void AcquireRewriteLocks(Query *parsetree, bool forUpdatePushedDown);
 extern Node *build_column_default(Relation rel, int attrno);
 
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e846..5f89028 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -115,6 +115,8 @@ typedef struct CachedPlan
 								 * bare utility statements) */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	Oid			planUserId;		/* is user-id that is assumed on this cached
+								   plan, or InvalidOid if portable for anybody */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 4669d8a..dce8463 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -18,6 +18,7 @@
 #include "catalog/pg_am.h"
 #include "catalog/pg_class.h"
 #include "catalog/pg_index.h"
+#include "catalog/pg_rowlevelsec.h"
 #include "fmgr.h"
 #include "nodes/bitmapset.h"
 #include "rewrite/prs2lock.h"
@@ -109,6 +110,7 @@ typedef struct RelationData
 	RuleLock   *rd_rules;		/* rewrite rules */
 	MemoryContext rd_rulescxt;	/* private memory cxt for rd_rules, if any */
 	TriggerDesc *trigdesc;		/* Trigger info, or NULL if rel has none */
+	RowLevelSecDesc	*rlsdesc;	/* Row-level security info, or NULL */
 
 	/*
 	 * rd_options is set whenever rd_rel is loaded into the relcache entry.
diff --git a/src/test/regress/expected/rowlevelsec.out b/src/test/regress/expected/rowlevelsec.out
new file mode 100644
index 0000000..854acb0
--- /dev/null
+++ b/src/test/regress/expected/rowlevelsec.out
@@ -0,0 +1,954 @@
+--
+-- Test of Row-level security feature
+--
+-- Clean up in case a prior regression run failed
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+RESET client_min_messages;
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+    COST 0.0000001 LANGUAGE plpgsql
+    AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+-- BASIC Row-Level Security Scenario
+CREATE TABLE uaccount (
+    pguser      name primary key,
+    seclv       int
+);
+INSERT INTO uaccount VALUES
+    ('rls_regress_user0', 99),
+    ('rls_regress_user1',  1),
+    ('rls_regress_user2',  2),
+    ('rls_regress_user3',  3);
+GRANT SELECT ON uaccount TO public;
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE category (
+    cid         int primary key,
+    cname       text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+    (11, 'novel'),
+    (22, 'science fiction'),
+    (33, 'technology'),
+    (44, 'manga');
+CREATE TABLE document (
+    did         int primary key,
+    cid         int references category(cid),
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+    ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+    ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+    ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+    ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+    ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+    ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+    ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+    ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW LEVEL SECURITY
+    (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great manga
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+   1 |  11 |      1 | rls_regress_user1 | my first novel
+   4 |  44 |      1 | rls_regress_user1 | my first manga
+   6 |  22 |      1 | rls_regress_user2 | great science fiction
+   8 |  44 |      1 | rls_regress_user2 | great manga
+(4 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great manga
+ cid | did | dlevel |      dauthor      |        dtitle         |      cname      
+-----+-----+--------+-------------------+-----------------------+-----------------
+  11 |   1 |      1 | rls_regress_user1 | my first novel        | novel
+  22 |   6 |      1 | rls_regress_user2 | great science fiction | science fiction
+  44 |   8 |      1 | rls_regress_user2 | great manga           | manga
+  44 |   4 |      1 | rls_regress_user1 | my first manga        | manga
+(4 rows)
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my second novel
+NOTICE:  f_leak => my science fiction
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => my second manga
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great technology book
+NOTICE:  f_leak => great manga
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+   1 |  11 |      1 | rls_regress_user1 | my first novel
+   2 |  11 |      2 | rls_regress_user1 | my second novel
+   3 |  22 |      2 | rls_regress_user1 | my science fiction
+   4 |  44 |      1 | rls_regress_user1 | my first manga
+   5 |  44 |      2 | rls_regress_user1 | my second manga
+   6 |  22 |      1 | rls_regress_user2 | great science fiction
+   7 |  33 |      2 | rls_regress_user2 | great technology book
+   8 |  44 |      1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my second novel
+NOTICE:  f_leak => my science fiction
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => my second manga
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great technology book
+NOTICE:  f_leak => great manga
+ cid | did | dlevel |      dauthor      |        dtitle         |      cname      
+-----+-----+--------+-------------------+-----------------------+-----------------
+  11 |   2 |      2 | rls_regress_user1 | my second novel       | novel
+  11 |   1 |      1 | rls_regress_user1 | my first novel        | novel
+  22 |   6 |      1 | rls_regress_user2 | great science fiction | science fiction
+  22 |   3 |      2 | rls_regress_user1 | my science fiction    | science fiction
+  33 |   7 |      2 | rls_regress_user2 | great technology book | technology
+  44 |   8 |      1 | rls_regress_user2 | great manga           | manga
+  44 |   5 |      2 | rls_regress_user1 | my second manga       | manga
+  44 |   4 |      1 | rls_regress_user1 | my first manga        | manga
+(8 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document document_1
+         Filter: (dlevel <= $0)
+         InitPlan 1 (returns $0)
+           ->  Index Scan using uaccount_pkey on uaccount
+                 Index Cond: (pguser = "current_user"())
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (category.cid = document.cid)
+   ->  Seq Scan on category
+   ->  Hash
+         ->  Subquery Scan on document
+               Filter: f_leak(document.dtitle)
+               ->  Seq Scan on document document_1
+                     Filter: (dlevel <= $0)
+                     InitPlan 1 (returns $0)
+                       ->  Index Scan using uaccount_pkey on uaccount
+                             Index Cond: (pguser = "current_user"())
+(11 rows)
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW LEVEL SECURITY (true);     -- fail
+ERROR:  must be owner of relation document
+ALTER TABLE document RESET ROW LEVEL SECURITY;          -- fail
+ERROR:  must be owner of relation document
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW LEVEL SECURITY (dauthor = current_user);
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my second novel
+NOTICE:  f_leak => my science fiction
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => my second manga
+ did | cid | dlevel |      dauthor      |       dtitle       
+-----+-----+--------+-------------------+--------------------
+   1 |  11 |      1 | rls_regress_user1 | my first novel
+   2 |  11 |      2 | rls_regress_user1 | my second novel
+   3 |  22 |      2 | rls_regress_user1 | my science fiction
+   4 |  44 |      1 | rls_regress_user1 | my first manga
+   5 |  44 |      2 | rls_regress_user1 | my second manga
+(5 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE:  f_leak => my first novel
+NOTICE:  f_leak => my second novel
+NOTICE:  f_leak => my science fiction
+NOTICE:  f_leak => my first manga
+NOTICE:  f_leak => my second manga
+ cid | did | dlevel |      dauthor      |       dtitle       |      cname      
+-----+-----+--------+-------------------+--------------------+-----------------
+  11 |   1 |      1 | rls_regress_user1 | my first novel     | novel
+  11 |   2 |      2 | rls_regress_user1 | my second novel    | novel
+  22 |   3 |      2 | rls_regress_user1 | my science fiction | science fiction
+  44 |   4 |      1 | rls_regress_user1 | my first manga     | manga
+  44 |   5 |      2 | rls_regress_user1 | my second manga    | manga
+(5 rows)
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great technology book
+NOTICE:  f_leak => great manga
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+   6 |  22 |      1 | rls_regress_user2 | great science fiction
+   7 |  33 |      2 | rls_regress_user2 | great technology book
+   8 |  44 |      1 | rls_regress_user2 | great manga
+(3 rows)
+
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+NOTICE:  f_leak => great science fiction
+NOTICE:  f_leak => great technology book
+NOTICE:  f_leak => great manga
+ cid | did | dlevel |      dauthor      |        dtitle         |      cname      
+-----+-----+--------+-------------------+-----------------------+-----------------
+  22 |   6 |      1 | rls_regress_user2 | great science fiction | science fiction
+  33 |   7 |      2 | rls_regress_user2 | great technology book | technology
+  44 |   8 |      1 | rls_regress_user2 | great manga           | manga
+(3 rows)
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+                  QUERY PLAN                  
+----------------------------------------------
+ Subquery Scan on document
+   Filter: f_leak(document.dtitle)
+   ->  Seq Scan on document document_1
+         Filter: (dauthor = "current_user"())
+(4 rows)
+
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+                     QUERY PLAN                     
+----------------------------------------------------
+ Nested Loop
+   ->  Subquery Scan on document
+         Filter: f_leak(document.dtitle)
+         ->  Seq Scan on document document_1
+               Filter: (dauthor = "current_user"())
+   ->  Index Scan using category_pkey on category
+         Index Cond: (cid = document.cid)
+(7 rows)
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW LEVEL SECURITY
+    (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+     WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+     ELSE false END);
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel |      dauthor      |       dtitle       | cid |   cname    
+-----+-----+--------+-------------------+--------------------+-----+------------
+   2 |  11 |      2 | rls_regress_user1 | my second novel    |  11 | novel
+   1 |  11 |      1 | rls_regress_user1 | my first novel     |  11 | novel
+     |     |        |                   |                    |  33 | technology
+   5 |  44 |      2 | rls_regress_user1 | my second manga    |     | 
+   4 |  44 |      1 | rls_regress_user1 | my first manga     |     | 
+   3 |  22 |      2 | rls_regress_user1 | my science fiction |     | 
+(6 rows)
+
+DELETE FROM category WHERE cid = 33;    -- failed
+ERROR:  update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document"
+DETAIL:  Key (cid)=(33) is still referenced from table "document".
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+ did | cid | dlevel |      dauthor      |        dtitle         | cid |      cname      
+-----+-----+--------+-------------------+-----------------------+-----+-----------------
+   6 |  22 |      1 | rls_regress_user2 | great science fiction |  22 | science fiction
+   8 |  44 |      1 | rls_regress_user2 | great manga           |  44 | manga
+   7 |  33 |      2 | rls_regress_user2 | great technology book |     | 
+(3 rows)
+
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+ERROR:  insert or update on table "document" violates foreign key constraint "document_cid_fkey"
+DETAIL:  Key (cid)=(33) is not present in table "category".
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+ did | cid | dlevel |      dauthor      |        dtitle         
+-----+-----+--------+-------------------+-----------------------
+   1 |  11 |      1 | rls_regress_user1 | my first novel
+   2 |  11 |      2 | rls_regress_user1 | my second novel
+   3 |  22 |      2 | rls_regress_user1 | my science fiction
+   4 |  44 |      1 | rls_regress_user1 | my first manga
+   5 |  44 |      2 | rls_regress_user1 | my second manga
+   6 |  22 |      1 | rls_regress_user2 | great science fiction
+   7 |  33 |      2 | rls_regress_user2 | great technology book
+   8 |  44 |      1 | rls_regress_user2 | great manga
+(8 rows)
+
+SELECT * FROM category;
+ cid |      cname      
+-----+-----------------
+  11 | novel
+  22 | science fiction
+  33 | technology
+  44 | manga
+(4 rows)
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+COPY t1 FROM stdin WITH (oids);
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+SELECT * FROM t1;
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(9 rows)
+
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               Filter: f_leak(t1.b)
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               Filter: f_leak(t2.b)
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+               Filter: f_leak(b)
+(12 rows)
+
+-- reference to system column
+SELECT oid, * FROM t1;
+ oid | a |  b  
+-----+---+-----
+ 102 | 2 | bbb
+ 104 | 4 | ddd
+ 201 | 1 | abc
+ 203 | 3 | cde
+ 301 | 1 | xxx
+ 302 | 2 | yyy
+ 303 | 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(9 rows)
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+ a |  b  |   t1    
+---+-----+---------
+ 2 | bbb | (2,bbb)
+ 4 | ddd | (4,ddd)
+ 1 | abc | (1,abc)
+ 3 | cde | (3,cde)
+ 1 | xxx | (1,xxx)
+ 2 | yyy | (2,yyy)
+ 3 | zzz | (3,zzz)
+(7 rows)
+
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+                QUERY PLAN                 
+-------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a % 2) = 0)
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a % 2) = 1)
+         ->  Seq Scan on t3
+(9 rows)
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ LockRows
+   ->  Result
+         ->  Append
+               ->  Subquery Scan on t1
+                     ->  LockRows
+                           ->  Seq Scan on t1 t1_1
+                                 Filter: ((a % 2) = 0)
+               ->  Subquery Scan on t2
+                     ->  LockRows
+                           ->  Seq Scan on t2 t2_1
+                                 Filter: ((a % 2) = 1)
+               ->  Seq Scan on t3
+(12 rows)
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 2 | bbb
+ 4 | ddd
+ 1 | abc
+ 3 | cde
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(7 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+                      QUERY PLAN                       
+-------------------------------------------------------
+ LockRows
+   ->  Result
+         ->  Append
+               ->  Subquery Scan on t1
+                     Filter: f_leak(t1.b)
+                     ->  LockRows
+                           ->  Seq Scan on t1 t1_1
+                                 Filter: ((a % 2) = 0)
+               ->  Subquery Scan on t2
+                     Filter: f_leak(t2.b)
+                     ->  LockRows
+                           ->  Seq Scan on t2 t2_1
+                                 Filter: ((a % 2) = 1)
+               ->  Seq Scan on t3
+                     Filter: f_leak(b)
+(15 rows)
+
+--
+-- COPY TO statement 
+--
+COPY t1 TO stdout;
+2	bbb
+4	ddd
+COPY t1 TO stdout WITH OIDS;
+102	2	bbb
+104	4	ddd
+COPY t2(c,b) TO stdout WITH OIDS;
+201	1.1	abc
+203	3.3	cde
+COPY (SELECT * FROM t1) TO stdout;
+2	bbb
+4	ddd
+1	abc
+3	cde
+1	xxx
+2	yyy
+3	zzz
+COPY document TO stdout WITH OIDS;	-- failed (no oid column)
+ERROR:  table "document" does not have OIDs
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from s2 where y like '%2f%'));
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%22%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion)
+ERROR:  infinite recursion detected for relation "s1"
+ALTER TABLE s2 SET ROW LEVEL SECURITY (x % 2 = 0);
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+NOTICE:  f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE:  f_leak => a87ff679a2f3e71d9181a67b7542122c
+ a |                b                 
+---+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+(2 rows)
+
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+                                      QUERY PLAN                                       
+---------------------------------------------------------------------------------------
+ Subquery Scan on s1  (cost=28.55..61.67 rows=205 width=36)
+   Filter: f_leak(s1.b)
+   ->  Hash Join  (cost=28.55..55.52 rows=615 width=36)
+         Hash Cond: (s1_1.a = s2.x)
+         ->  Seq Scan on s1 s1_1  (cost=0.00..22.30 rows=1230 width=36)
+         ->  Hash  (cost=28.54..28.54 rows=1 width=4)
+               ->  HashAggregate  (cost=28.53..28.54 rows=1 width=4)
+                     ->  Subquery Scan on s2  (cost=0.00..28.52 rows=1 width=4)
+                           Filter: (s2.y ~~ '%2f%'::text)
+                           ->  Seq Scan on s2 s2_1  (cost=0.00..28.45 rows=6 width=36)
+                                 Filter: ((x % 2) = 0)
+(11 rows)
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from v2));		-- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+NOTICE:  f_leak => 0267aaf632e87a63288a08331f22c7c3
+NOTICE:  f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ a  |                b                 
+----+----------------------------------
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+  6 | 1679091c5a880faf6fb5e6087eb1b2dc
+(2 rows)
+
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Subquery Scan on s1
+   Filter: f_leak(s1.b)
+   ->  Hash Join
+         Hash Cond: (s1_1.a = s2.x)
+         ->  Seq Scan on s1 s1_1
+         ->  Hash
+               ->  HashAggregate
+                     ->  Subquery Scan on s2
+                           Filter: (s2.y ~~ '%af%'::text)
+                           ->  Seq Scan on s2 s2_1
+                                 Filter: ((x % 2) = 0)
+(11 rows)
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+ xx | x  |                y                 
+----+----+----------------------------------
+ -6 | -6 | 596a3d04481816330f07e4f97510c28f
+ -4 | -4 | 0267aaf632e87a63288a08331f22c7c3
+  2 |  2 | c81e728d9d4c2f636f067f89cc14862c
+(3 rows)
+
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+                             QUERY PLAN                             
+--------------------------------------------------------------------
+ Subquery Scan on s2
+   Filter: (s2.y ~~ '%28%'::text)
+   ->  Seq Scan on s2 s2_1
+         Filter: ((x % 2) = 0)
+   SubPlan 1
+     ->  Limit
+           ->  Subquery Scan on s1
+                 ->  Nested Loop Semi Join
+                       Join Filter: (s1_1.a = s2_2.x)
+                       ->  Seq Scan on s1 s1_1
+                       ->  Materialize
+                             ->  Subquery Scan on s2_2
+                                   Filter: (s2_2.y ~~ '%af%'::text)
+                                   ->  Seq Scan on s2 s2_3
+                                         Filter: ((x % 2) = 0)
+(15 rows)
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion via view)
+ERROR:  infinite recursion detected for relation "s1"
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 1 | abc
+ 1 | xxx
+ 2 | yyy
+(4 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a <= 2) AND ((a % 2) = 0))
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a <= 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(10 rows)
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+NOTICE:  f_leak => aaa
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ccc
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => bcd
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => def
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+ a |  b  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 3 | ccc
+ 4 | ddd
+ 1 | abc
+ 2 | bcd
+ 3 | cde
+ 4 | def
+ 1 | xxx
+ 2 | yyy
+ 3 | zzz
+(11 rows)
+
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+           QUERY PLAN            
+---------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: f_leak(b)
+         ->  Seq Scan on t2
+               Filter: f_leak(b)
+         ->  Seq Scan on t3
+               Filter: f_leak(b)
+(8 rows)
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+ a |  b  
+---+-----
+ 1 | aaa
+ 2 | bbb
+ 1 | abc
+ 2 | bcd
+ 1 | xxx
+ 2 | yyy
+(6 rows)
+
+EXPLAIN (costs off) EXECUTE p1(2);
+           QUERY PLAN           
+--------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a <= 2)
+         ->  Seq Scan on t2
+               Filter: (a <= 2)
+         ->  Seq Scan on t3
+               Filter: (a <= 2)
+(8 rows)
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 2 | bcd
+ 2 | yyy
+(3 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+          QUERY PLAN           
+-------------------------------
+ Result
+   ->  Append
+         ->  Seq Scan on t1
+               Filter: (a = 2)
+         ->  Seq Scan on t2
+               Filter: (a = 2)
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(8 rows)
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+ a |  b  
+---+-----
+ 2 | bbb
+ 2 | yyy
+(2 rows)
+
+EXPLAIN (costs off) EXECUTE p2(2);
+                       QUERY PLAN                        
+---------------------------------------------------------
+ Result
+   ->  Append
+         ->  Subquery Scan on t1
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a = 2) AND ((a % 2) = 0))
+         ->  Subquery Scan on t2
+               ->  Seq Scan on t2 t2_1
+                     Filter: ((a = 2) AND ((a % 2) = 1))
+         ->  Seq Scan on t3
+               Filter: (a = 2)
+(10 rows)
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+             QUERY PLAN              
+-------------------------------------
+ Update on t1
+   ->  Subquery Scan on t1
+         Filter: f_leak(t1.b)
+         ->  Seq Scan on t1 t1_1
+               Filter: ((a % 2) = 0)
+   ->  Subquery Scan on t2
+         Filter: f_leak(t2.b)
+         ->  Seq Scan on t2 t2_1
+               Filter: ((a % 2) = 1)
+   ->  Seq Scan on t3
+         Filter: f_leak(b)
+(11 rows)
+
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+NOTICE:  f_leak => bbb
+NOTICE:  f_leak => ddd
+NOTICE:  f_leak => abc
+NOTICE:  f_leak => cde
+NOTICE:  f_leak => xxx
+NOTICE:  f_leak => yyy
+NOTICE:  f_leak => zzz
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+             QUERY PLAN              
+-------------------------------------
+ Update on t1
+   ->  Subquery Scan on t1
+         Filter: f_leak(t1.b)
+         ->  Seq Scan on t1 t1_1
+               Filter: ((a % 2) = 0)
+(5 rows)
+
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+NOTICE:  f_leak => bbbbbb
+NOTICE:  f_leak => dddddd
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE:  f_leak => bbbbbb_updt
+NOTICE:  f_leak => dddddd_updt
+ oid | a |      b      |       t1        
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+NOTICE:  f_leak => bbbbbb_updt
+NOTICE:  f_leak => dddddd_updt
+NOTICE:  f_leak => abcabc
+NOTICE:  f_leak => cdecde
+NOTICE:  f_leak => xxxxxx
+NOTICE:  f_leak => yyyyyy
+NOTICE:  f_leak => zzzzzz
+ a |      b      
+---+-------------
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(7 rows)
+
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE:  f_leak => bbbbbb_updt
+NOTICE:  f_leak => dddddd_updt
+NOTICE:  f_leak => abcabc
+NOTICE:  f_leak => cdecde
+NOTICE:  f_leak => xxxxxx
+NOTICE:  f_leak => yyyyyy
+NOTICE:  f_leak => zzzzzz
+ oid | a |      b      |       t1        
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+ 201 | 1 | abcabc      | (1,abcabc)
+ 203 | 3 | cdecde      | (3,cdecde)
+ 301 | 1 | xxxxxx      | (1,xxxxxx)
+ 302 | 2 | yyyyyy      | (2,yyyyyy)
+ 303 | 3 | zzzzzz      | (3,zzzzzz)
+(7 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a |      b      
+---+-------------
+ 1 | aaa
+ 3 | ccc
+ 2 | bbbbbb_updt
+ 4 | dddddd_updt
+ 2 | bcd
+ 4 | def
+ 1 | abcabc
+ 3 | cdecde
+ 1 | xxxxxx
+ 2 | yyyyyy
+ 3 | zzzzzz
+(11 rows)
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+             QUERY PLAN              
+-------------------------------------
+ Delete on t1
+   ->  Subquery Scan on t1
+         Filter: f_leak(t1.b)
+         ->  Seq Scan on t1 t1_1
+               Filter: ((a % 2) = 0)
+(5 rows)
+
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+             QUERY PLAN              
+-------------------------------------
+ Delete on t1
+   ->  Subquery Scan on t1
+         Filter: f_leak(t1.b)
+         ->  Seq Scan on t1 t1_1
+               Filter: ((a % 2) = 0)
+   ->  Subquery Scan on t2
+         Filter: f_leak(t2.b)
+         ->  Seq Scan on t2 t2_1
+               Filter: ((a % 2) = 1)
+   ->  Seq Scan on t3
+         Filter: f_leak(b)
+(11 rows)
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE:  f_leak => bbbbbb_updt
+NOTICE:  f_leak => dddddd_updt
+ oid | a |      b      |       t1        
+-----+---+-------------+-----------------
+ 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt)
+ 104 | 4 | dddddd_updt | (4,dddddd_updt)
+(2 rows)
+
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+NOTICE:  f_leak => abcabc
+NOTICE:  f_leak => cdecde
+NOTICE:  f_leak => xxxxxx
+NOTICE:  f_leak => yyyyyy
+NOTICE:  f_leak => zzzzzz
+ oid | a |   b    |     t1     
+-----+---+--------+------------
+ 201 | 1 | abcabc | (1,abcabc)
+ 203 | 3 | cdecde | (3,cdecde)
+ 301 | 1 | xxxxxx | (1,xxxxxx)
+ 302 | 2 | yyyyyy | (2,yyyyyy)
+ 303 | 3 | zzzzzz | (3,zzzzzz)
+(5 rows)
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+ a |  b  
+---+-----
+ 1 | aaa
+ 3 | ccc
+ 2 | bcd
+ 4 | def
+(4 rows)
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+DROP SCHEMA rls_regress_schema CASCADE;
+NOTICE:  drop cascades to 10 other objects
+DETAIL:  drop cascades to function f_leak(text)
+drop cascades to table uaccount
+drop cascades to table category
+drop cascades to table document
+drop cascades to table t1
+drop cascades to table t2
+drop cascades to table t3
+drop cascades to table s1
+drop cascades to table s2
+drop cascades to view v2
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 3f04442..ffcd8d3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -120,6 +120,7 @@ SELECT relname, relhasindex
  pg_proc                 | t
  pg_range                | t
  pg_rewrite              | t
+ pg_rowlevelsec          | t
  pg_seclabel             | t
  pg_shdepend             | t
  pg_shdescription        | t
@@ -166,7 +167,7 @@ SELECT relname, relhasindex
  timetz_tbl              | f
  tinterval_tbl           | f
  varchar_tbl             | f
-(155 rows)
+(156 rows)
 
 --
 -- another sanity check: every system catalog that has OIDs should have
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 663bf8a..4c4552f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
 # ----------
 # Another group of parallel tests
 # ----------
-test: privileges security_label collate
+test: privileges rowlevelsec security_label collate
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index be789e3..eea4c3e 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -92,6 +92,7 @@ test: delete
 test: namespace
 test: prepared_xacts
 test: privileges
+test: rowlevelsec
 test: security_label
 test: collate
 test: misc
diff --git a/src/test/regress/sql/rowlevelsec.sql b/src/test/regress/sql/rowlevelsec.sql
new file mode 100644
index 0000000..a996b8c
--- /dev/null
+++ b/src/test/regress/sql/rowlevelsec.sql
@@ -0,0 +1,295 @@
+--
+-- Test of Row-level security feature
+--
+
+-- Clean up in case a prior regression run failed
+
+-- Suppress NOTICE messages when users/groups don't exist
+SET client_min_messages TO 'warning';
+
+DROP USER IF EXISTS rls_regress_user0;
+DROP USER IF EXISTS rls_regress_user1;
+DROP USER IF EXISTS rls_regress_user2;
+
+DROP SCHEMA IF EXISTS rls_regress_schema CASCADE;
+
+RESET client_min_messages;
+
+-- initial setup
+CREATE USER rls_regress_user0;
+CREATE USER rls_regress_user1;
+CREATE USER rls_regress_user2;
+
+CREATE SCHEMA rls_regress_schema;
+GRANT ALL ON SCHEMA rls_regress_schema TO public;
+SET search_path = rls_regress_schema;
+
+-- setup of malicious function
+CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool
+    COST 0.0000001 LANGUAGE plpgsql
+    AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END';
+GRANT EXECUTE ON FUNCTION f_leak(text) TO public;
+
+-- BASIC Row-Level Security Scenario
+CREATE TABLE uaccount (
+    pguser      name primary key,
+    seclv       int
+);
+INSERT INTO uaccount VALUES
+    ('rls_regress_user0', 99),
+    ('rls_regress_user1',  1),
+    ('rls_regress_user2',  2),
+    ('rls_regress_user3',  3);
+GRANT SELECT ON uaccount TO public;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE category (
+    cid         int primary key,
+    cname       text
+);
+GRANT ALL ON category TO public;
+INSERT INTO category VALUES
+    (11, 'novel'),
+    (22, 'science fiction'),
+    (33, 'technology'),
+    (44, 'manga');
+
+CREATE TABLE document (
+    did         int primary key,
+    cid         int references category(cid),
+    dlevel      int not null,
+    dauthor     name,
+    dtitle      text
+);
+GRANT ALL ON document TO public;
+INSERT INTO document VALUES
+    ( 1, 11, 1, 'rls_regress_user1', 'my first novel'),
+    ( 2, 11, 2, 'rls_regress_user1', 'my second novel'),
+    ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'),
+    ( 4, 44, 1, 'rls_regress_user1', 'my first manga'),
+    ( 5, 44, 2, 'rls_regress_user1', 'my second manga'),
+    ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'),
+    ( 7, 33, 2, 'rls_regress_user2', 'great technology book'),
+    ( 8, 44, 1, 'rls_regress_user2', 'great manga');
+
+-- user's security level must higher than or equal to document's one
+ALTER TABLE document SET ROW LEVEL SECURITY
+    (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user));
+
+-- viewpoint from rls_regress_user1
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- only owner can change row-level security
+ALTER TABLE document SET ROW LEVEL SECURITY (true);     -- fail
+ALTER TABLE document RESET ROW LEVEL SECURITY;          -- fail
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE document SET ROW LEVEL SECURITY (dauthor = current_user);
+
+-- viewpoint from rls_regress_user1 again
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- viewpoint from rls_regress_user2 again
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document WHERE f_leak(dtitle);
+SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+EXPLAIN (costs off) SELECT * FROM document WHERE f_leak(dtitle);
+EXPLAIN (costs off) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle);
+
+-- interaction of FK/PK constraints
+SET SESSION AUTHORIZATION rls_regress_user0;
+ALTER TABLE category SET ROW LEVEL SECURITY
+    (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33)
+     WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44)
+     ELSE false END);
+
+-- cannot delete PK referenced by invisible FK
+SET SESSION AUTHORIZATION rls_regress_user1;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+DELETE FROM category WHERE cid = 33;    -- failed
+
+-- cannot insert FK referencing invisible PK
+SET SESSION AUTHORIZATION rls_regress_user2;
+SELECT * FROM document d full outer join category c on d.cid = c.cid;
+INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- failed
+
+-- database superuser can bypass RLS policy
+RESET SESSION AUTHORIZATION;
+SELECT * FROM document;
+SELECT * FROM category;
+
+--
+-- Table inheritance and RLS policy
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+
+CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS;
+ALTER TABLE t1 DROP COLUMN junk1;    -- just a disturbing factor
+GRANT ALL ON t1 TO public;
+
+COPY t1 FROM stdin WITH (oids);
+101	1	aaa
+102	2	bbb
+103	3	ccc
+104	4	ddd
+\.
+
+CREATE TABLE t2 (c float) INHERITS (t1);
+COPY t2 FROM stdin WITH (oids);
+201	1	abc	1.1
+202	2	bcd	2.2
+203	3	cde	3.3
+204	4	def	4.4
+\.
+
+CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
+ALTER TABLE t3 INHERIT t1;
+COPY t3(a,b,c) FROM stdin WITH (oids);
+301	1	xxx	X
+302	2	yyy	Y
+303	3	zzz	Z
+\.
+
+ALTER TABLE t1 SET ROW LEVEL SECURITY (a % 2 = 0);	-- be even number
+ALTER TABLE t2 SET ROW LEVEL SECURITY (a % 2 = 1);	-- be odd number
+
+SELECT * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- reference to system column
+SELECT oid, * FROM t1;
+EXPLAIN (costs off) SELECT * FROM t1;
+
+-- reference to whole-row reference
+SELECT *,t1 FROM t1;
+EXPLAIN (costs off) SELECT *,t1 FROM t1;
+
+-- for share/update lock
+SELECT * FROM t1 FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 FOR SHARE;
+
+SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
+
+--
+-- COPY TO statement 
+--
+COPY t1 TO stdout;
+COPY t1 TO stdout WITH OIDS;
+COPY t2(c,b) TO stdout WITH OIDS;
+COPY (SELECT * FROM t1) TO stdout;
+COPY document TO stdout WITH OIDS;	-- failed (no oid column)
+
+--
+-- recursive RLS and VIEWs in policy
+--
+CREATE TABLE s1 (a int, b text);
+INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE TABLE s2 (x int, y text);
+INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x);
+CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%';
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from s2 where y like '%2f%'));
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%22%'));
+
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion)
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY (x % 2 = 0);
+
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+EXPLAIN SELECT * FROM only s1 WHERE f_leak(b);
+
+ALTER TABLE s1 SET ROW LEVEL SECURITY
+   (a in (select x from v2));		-- using VIEW in RLS policy
+SELECT * FROM s1 WHERE f_leak(b);	-- OK
+EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b);
+
+SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%';
+
+ALTER TABLE s2 SET ROW LEVEL SECURITY
+   (x in (select a from s1 where b like '%d2%'));
+SELECT * FROM s1 WHERE f_leak(b);	-- fail (infinite recursion via view)
+
+-- prepared statement with rls_regress_user0 privilege
+PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1;
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+-- superuser is allowed to bypass RLS checks
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1 WHERE f_leak(b);
+EXPLAIN (costs off) SELECT * FROM t1 WHERE f_leak(b);
+
+-- plan cache should be invalidated
+EXECUTE p1(2);
+EXPLAIN (costs off) EXECUTE p1(2);
+
+PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+-- also, case when privilege switch from superuser
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXECUTE p2(2);
+EXPLAIN (costs off) EXECUTE p2(2);
+
+--
+-- UPDATE / DELETE and Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) UPDATE t1 SET b = b || b WHERE f_leak(b);
+UPDATE t1 SET b = b || b WHERE f_leak(b);
+
+EXPLAIN (costs off) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b);
+
+-- returning clause with system column
+UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
+UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b);
+EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b);
+
+DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
+DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
+
+RESET SESSION AUTHORIZATION;
+SELECT * FROM t1;
+
+--
+-- Clean up objects
+--
+RESET SESSION AUTHORIZATION;
+
+DROP SCHEMA rls_regress_schema CASCADE;
+
+DROP USER rls_regress_user0;
+DROP USER rls_regress_user1;
+DROP USER rls_regress_user2;
#35David Fetter
david@fetter.org
In reply to: Kohei KaiGai (#34)
Re: [v9.3] Row-Level Security

On Sun, Nov 25, 2012 at 03:20:28PM +0100, Kohei KaiGai wrote:

However, UPDATE / DELETE support is not perfect right now.
In case when we try to update / delete a table with inherited
children and RETURNING clause was added, is loses right
references to the pseudo columns, even though it works fine
without inherited children.

The attached patch fixed this known problem.

This patch no longer applies to git master. Any chance of a rebase?

Also, might this approach work for the catalog? The use case I have
in mind is multi-tenancy, although one can imagine organizations where
internal access controls might be required on it, too.

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

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

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

#36Kohei KaiGai
kaigai@kaigai.gr.jp
In reply to: David Fetter (#35)
Re: [v9.3] Row-Level Security

2012/12/3 David Fetter <david@fetter.org>:

On Sun, Nov 25, 2012 at 03:20:28PM +0100, Kohei KaiGai wrote:

However, UPDATE / DELETE support is not perfect right now.
In case when we try to update / delete a table with inherited
children and RETURNING clause was added, is loses right
references to the pseudo columns, even though it works fine
without inherited children.

The attached patch fixed this known problem.

This patch no longer applies to git master. Any chance of a rebase?

OK, I'll rebese it.

Also, might this approach work for the catalog? The use case I have
in mind is multi-tenancy, although one can imagine organizations where
internal access controls might be required on it, too.

If you intend to control behavior of DDL commands that internally takes
access towards system catalog, RLS feature is not helpful.
Please use sepgsql instead. :-)
If you intend to control DML commands towards system catalogs, here
is nothing special, so I expect it works as we are doing at user tables.

Thanks,
--
KaiGai Kohei <kaigai@kaigai.gr.jp>

--
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: Kohei KaiGai (#36)
Re: [v9.3] Row-Level Security

On 3 December 2012 15:36, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

2012/12/3 David Fetter <david@fetter.org>:

On Sun, Nov 25, 2012 at 03:20:28PM +0100, Kohei KaiGai wrote:

However, UPDATE / DELETE support is not perfect right now.
In case when we try to update / delete a table with inherited
children and RETURNING clause was added, is loses right
references to the pseudo columns, even though it works fine
without inherited children.

The attached patch fixed this known problem.

This patch no longer applies to git master. Any chance of a rebase?

OK, I'll rebese it.

No chunk failures, its just fuzz.

--
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

#38David Fetter
david@fetter.org
In reply to: Simon Riggs (#37)
Re: [v9.3] Row-Level Security

On Mon, Dec 03, 2012 at 03:41:47PM +0000, Simon Riggs wrote:

On 3 December 2012 15:36, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

2012/12/3 David Fetter <david@fetter.org>:

On Sun, Nov 25, 2012 at 03:20:28PM +0100, Kohei KaiGai wrote:

However, UPDATE / DELETE support is not perfect right now.
In case when we try to update / delete a table with inherited
children and RETURNING clause was added, is loses right
references to the pseudo columns, even though it works fine
without inherited children.

The attached patch fixed this known problem.

This patch no longer applies to git master. Any chance of a rebase?

OK, I'll rebese it.

No chunk failures, its just fuzz.

I must have done something wrong.

I downloaded the patch from the web email archives, then ran "git
apply" on it, and got this:

$ git apply ../pgsql-v9.3-row-level-security.rw.v7.patch
../pgsql-v9.3-row-level-security.rw.v7.patch:806: space before tab in indent.
* row-level security policy
../pgsql-v9.3-row-level-security.rw.v7.patch:1909: trailing whitespace.
* would be given. Because the sub-query has security barrier flag,
../pgsql-v9.3-row-level-security.rw.v7.patch:2886: trailing whitespace.
did | cid | dlevel | dauthor | dtitle
../pgsql-v9.3-row-level-security.rw.v7.patch:2899: trailing whitespace.
cid | did | dlevel | dauthor | dtitle | cname
../pgsql-v9.3-row-level-security.rw.v7.patch:2918: trailing whitespace.
did | cid | dlevel | dauthor | dtitle
error: patch failed: src/backend/commands/copy.c:34
error: src/backend/commands/copy.c: patch does not apply

What did I do wrong here?

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

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

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

#39Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: David Fetter (#38)
Re: [v9.3] Row-Level Security

David Fetter escribió:

On Mon, Dec 03, 2012 at 03:41:47PM +0000, Simon Riggs wrote:

On 3 December 2012 15:36, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

2012/12/3 David Fetter <david@fetter.org>:

On Sun, Nov 25, 2012 at 03:20:28PM +0100, Kohei KaiGai wrote:

However, UPDATE / DELETE support is not perfect right now.
In case when we try to update / delete a table with inherited
children and RETURNING clause was added, is loses right
references to the pseudo columns, even though it works fine
without inherited children.

The attached patch fixed this known problem.

This patch no longer applies to git master. Any chance of a rebase?

OK, I'll rebese it.

No chunk failures, its just fuzz.

I must have done something wrong.

I downloaded the patch from the web email archives, then ran "git
apply" on it, and got this:

git apply is much stricter than other tools; if the patch requires a
merge, it doesn't try. Try applying it with "patch -p1 < /path/to/patch"
(this is what I always use)

--
Álvaro Herrera 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

#40Simon Riggs
simon@2ndQuadrant.com
In reply to: Kohei KaiGai (#33)
Re: [v9.3] Row-Level Security

On 15 November 2012 21:07, Kohei KaiGai <kaigai@kaigai.gr.jp> wrote:

The attached patch is a revised version of row-level security
feature.

I've got time to review this patch, so I've added myself as a CF reviewer.

Definitely looks very interesting, well done for getting it this far along.

--
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