pgsql-v9.3-row-level-security.v1.patch

application/octet-stream

Filename: pgsql-v9.3-row-level-security.v1.patch
Type: application/octet-stream
Part: 0
Message: [v9.3] Row-Level Security

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: unified
Series: patch v9
File+
doc/src/sgml/ref/alter_table.sgml 38 0
doc/src/sgml/user-manag.sgml 125 0
src/backend/catalog/dependency.c 22 0
src/backend/catalog/Makefile 1 1
src/backend/commands/explain.c 6 2
src/backend/commands/tablecmds.c 185 0
src/backend/optimizer/plan/planner.c 7 0
src/backend/optimizer/prep/preptlist.c 49 5
src/backend/optimizer/prep/prepunion.c 27 1
src/backend/optimizer/util/Makefile 1 1
src/backend/optimizer/util/rowlvsec.c 524 0
src/backend/parser/gram.y 16 0
src/backend/utils/adt/acl.c 34 0
src/backend/utils/adt/ri_triggers.c 28 5
src/backend/utils/cache/syscache.c 12 0
src/include/catalog/dependency.h 1 0
src/include/catalog/indexing.h 3 0
src/include/catalog/pg_proc.h 7 0
src/include/catalog/pg_rowlvsec.h 39 0
src/include/commands/tablecmds.h 2 0
src/include/nodes/parsenodes.h 4 1
src/include/optimizer/rowlvsec.h 31 0
src/include/utils/syscache.h 1 0
src/test/regress/expected/rowlvsec.out 438 0
src/test/regress/expected/sanity_check.out 2 1
src/test/regress/parallel_schedule 1 1
src/test/regress/serial_schedule 1 0
src/test/regress/sql/rowlvsec.sql 216 0
 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;