Auditing extension for PostgreSQL (Take 2)

Started by David Steelealmost 11 years ago65 messages
#1David Steele
david@pgmasters.net
1 attachment(s)

I've posted a couple of messages over the last few weeks about the work
I've been doing on the pg_audit extension. The lack of response could
be due to either universal acclaim or complete apathy, but in any case I
think this is a very important topic so I want to give it another try.

I've extensively reworked the code that was originally submitted by
2ndQuandrant. This is not an indictment of their work, but rather an
attempt to redress concerns that were expressed by members of the
community. I've used core functions to determine how audit events
should be classified and simplified and tightened the code wherever
possible. I've removed deparse and event triggers and opted for methods
that rely only on existing hooks. In my last message I provided
numerous examples of configuration, usage, and output which I hoped
would alleviate concerns of complexity. I've also written a ton of unit
tests to make sure that the code works as expected.

Auditing has been a concern everywhere I've used or introduced
PostgreSQL. Over time I've developed a reasonably comprehensive (but
complex) system of triggers, tables and functions to provide auditing
for customers. While this has addressed most requirements, there are
always questions of auditing aborted transactions, DDL changes,
configurability, etc. which I have been unable to satisfy.

There has been some discussion of whether or not this module needs to be
in contrib. One reason customers trust contrib is that the modules are
assumed to be held to a higher standard than code found on GitHub. This
has already been pointed out. But I believe another important reason is
that they know packages will be made available for a variety of
platforms, and bugs are more likely to be experienced by many users and
not just a few (or one). For this reason my policy is not to run
custom-compiled code in production. This is especially true for
something as mission critical as a relational database.

I realize this is not an ideal solution. Everybody (including me) wants
something that is in core with real policies and more options. It's
something that I am personally really eager to work on. But the reality
is that's not going to happen for 9.5 and probably not for 9.6 either.
Meanwhile, I believe the lack of some form of auditing is harming
adoption of PostgreSQL, especially in the financial and government sectors.

The patch I've attached satisfies the requirements that I've had from
customers in the past. I'm confident that if it gets out into the wild
it will bring all kinds of criticism and comments which will be valuable
in designing a robust in-core solution.

I'm submitting this patch to the Commitfest. I'll do everything I can
to address the concerns of the community and I'm happy to provide more
examples as needed. I'm hoping the sgml docs I've provided with the
patch will cover any questions, but of course feedback is always
appreciated.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v1.patchtext/plain; charset=UTF-8; name=pg_audit-v1.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index 195d447..d8e75f4 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -29,6 +29,7 @@ SUBDIRS = \
 		pageinspect	\
 		passwordcheck	\
 		pg_archivecleanup \
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..32bc6d9
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,20 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..2eee3b9
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,4 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..b3914ac
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1099 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements.
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_prewarm/pg_prewarm.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * auditRole is the string value of the pgaudit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pgaudit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * String contants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String contants for log classes - used when processing tokens in the
+ * pgaudit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC		    "MISC"
+#define CLASS_READ			"READ"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* SELECT */
+	LOG_READ = (1 << 0),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 1),
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 2),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 4),
+
+	/* Function execution */
+	LOG_MISC = (1 << 5),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String contants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * This module collects AuditEvents from various sources (event triggers, and
+ * executor/utility hooks) and passes them to the log_audit_event() function.
+ *
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If an underlying command affects multiple objects multiple
+ * AuditEvents must be created to represent it.
+ */
+typedef struct
+{
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	bool granted;
+} AuditEvent;
+
+/*
+ * Set if a function below log_utility_command() has logged the event - prevents
+ * more than one function from logging when the event could be logged in
+ * multiple places.
+ */
+bool utilityCommandLogged = false;
+AuditEvent utilityAuditEvent;
+
+/*
+ * Returns the oid of the role specified in pgaudit.role.
+ */
+static Oid
+audit_role_oid()
+{
+	HeapTuple roleTup;
+	Oid oid = InvalidOid;
+
+	roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(auditRole));
+
+	if (HeapTupleIsValid(roleTup))
+	{
+		oid = HeapTupleGetOid(roleTup);
+		ReleaseSysCache(roleTup);
+	}
+
+	return oid;
+}
+
+/*
+ * Takes an AuditEvent and returns true or false depending on whether the event
+ * should be logged according to the pgaudit.roles/log settings. If it returns
+ * true, also fills in the name of the LogClass which it is logged under.
+ */
+static bool
+log_check(AuditEvent *e, const char **classname)
+{
+	enum LogClass class = LOG_NONE;
+
+	/* By default put everything in the MISC class. */
+	*classname = CLASS_MISC;
+	class = LOG_MISC;
+
+	/*
+	 * Look at the type of the command and decide what LogClass needs to be
+	 * enabled for the command to be logged.
+	 */
+	switch (e->logStmtLevel)
+	{
+		case LOGSTMT_MOD:
+			*classname = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		case LOGSTMT_DDL:
+			*classname = CLASS_DDL;
+			class = LOG_DDL;
+
+		case LOGSTMT_ALL:
+			switch (e->commandTag)
+			{
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					*classname = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				case T_VacuumStmt:
+				case T_ReindexStmt:
+					*classname = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				case T_DoStmt:
+					*classname = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * We log audit events under the following conditions:
+	 *
+	 * 1. If the audit role has been explicitly granted permission for
+	 *    an operation.
+	 */
+	if (e->granted)
+	{
+		return true;
+	}
+
+	/* 2. If the event belongs to a class covered by pgaudit.log. */
+	if ((auditLogBitmap & class) == class)
+	{
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * Takes an AuditEvent and, if it log_check(), writes it to the audit log. The
+ * AuditEvent is assumed to be completely filled in by the caller (unknown
+ * values must be set to "" so that they can be logged without error checking).
+ */
+static void
+log_audit_event(AuditEvent *e)
+{
+	const char *classname;
+
+	/* Check that this event should be logged. */
+	if (!log_check(e, &classname))
+		return;
+
+	/* Log via ereport(). */
+	ereport(LOG,
+			(errmsg("AUDIT: %s,%s,%s,%s,%s,%s",
+					e->granted ? AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+					classname, e->command, e->objectType, e->objectName,
+					e->commandText),
+			 errhidestmt(true)));
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+log_acl_check(Datum aclDatum, Oid auditOid, AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem    *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+log_relation_check(Oid relOid,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = log_acl_check(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+log_attribute_check(Oid relOid,
+					AttrNumber attNum,
+					Oid auditOid,
+					AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = log_acl_check(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+log_attribute_check_any(Oid relOid,
+						Oid auditOid,
+						Bitmapset *attributeSet,
+						AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (log_attribute_check(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* Make a copy of the column set */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			log_attribute_check(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for DML operations via executor permissions checks.
+ */
+static void
+log_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+		AuditEvent auditEvent;
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		auditEvent.logStmtLevel = LOGSTMT_MOD;
+
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEvent.commandTag = T_InsertStmt;
+			auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEvent.commandTag = T_UpdateStmt;
+			auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEvent.commandTag = T_DeleteStmt;
+			auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEvent.commandTag = T_SelectStmt;
+			auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEvent.commandTag = T_Invalid;
+			auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEvent.granted = false;
+		auditEvent.commandText = debug_query_string;
+
+		/* If this is the first rte then session log */
+		if (first)
+		{
+			auditEvent.objectName = "";
+			auditEvent.objectType = "";
+
+			log_audit_event(&auditEvent);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEvent.objectType = OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEvent.objectType = OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEvent.objectType = OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEvent.objectType = OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEvent.objectType = OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEvent.objectType = OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEvent.objectType = OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (log_relation_check(relOid, auditOid, auditPerms))
+			{
+				auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEvent.granted =
+						log_attribute_check_any(relOid, auditOid,
+												rte->selectedCols,
+												ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEvent.granted =
+							log_attribute_check_any(relOid, auditOid,
+													rte->modifiedCols,
+													auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Only do relation level logging if a grant was found. */
+		if (auditEvent.granted)
+		{
+			log_audit_event(&auditEvent);
+		}
+
+		pfree(auditEvent.objectName);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		utilityAuditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				utilityAuditEvent.objectType = OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				utilityAuditEvent.objectType = OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				utilityAuditEvent.objectType = OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				utilityAuditEvent.objectType = OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+
+		/* Log the event */
+		log_audit_event(&utilityAuditEvent);
+		utilityCommandLogged = true;
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Generate the fully-qualified function name. */
+	utilityAuditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the event */
+	utilityAuditEvent.logStmtLevel = LOGSTMT_ALL;
+	utilityAuditEvent.commandTag = T_DoStmt;
+	utilityAuditEvent.command = COMMAND_EXECUTE;
+	utilityAuditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	utilityAuditEvent.commandText = debug_query_string;
+
+	log_audit_event(&utilityAuditEvent);
+	utilityCommandLogged = true;
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute. */
+		case OAT_FUNCTION_EXECUTE:
+			log_function_execute(objectId);
+			break;
+
+		/* Log create. */
+		case OAT_POST_CREATE:
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter. */
+		case OAT_POST_ALTER:
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop. */
+		case OAT_DROP:
+		{
+			ObjectAccessDrop *drop = arg;
+
+			if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+				return;
+
+			log_create_alter_drop(classId, objectId);
+		}
+		break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pgaudit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid = audit_role_oid();
+
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_dml(auditOid, rangeTabls);
+
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pgaudit_ProcessUtility_hook(Node *parsetree,
+							const char *queryString,
+							ProcessUtilityContext context,
+							ParamListInfo params,
+							DestReceiver *dest,
+							char *completionTag)
+{
+	/* Create the utility audit event. */
+	utilityCommandLogged = false;
+
+	utilityAuditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+	utilityAuditEvent.commandTag = nodeTag(parsetree);
+	utilityAuditEvent.command = CreateCommandTag(parsetree);
+	utilityAuditEvent.objectName = "";
+	utilityAuditEvent.objectType = "";
+	utilityAuditEvent.commandText = debug_query_string;
+	utilityAuditEvent.granted = false;
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/* Log the utility command if logging is on, the command has not already
+	 * been logged by another hook, and the transaction is not aborted */
+	if (auditLogBitmap != 0 && !utilityCommandLogged &&
+		!IsAbortedTransactionBlockState())
+	{
+		log_audit_event(&utilityAuditEvent);
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pgaudit_object_access_hook(ObjectAccessType access,
+						   Oid classId,
+						   Oid objectId,
+						   int subId,
+						   void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState())
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pgaudit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pgaudit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then then token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pgaudit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pgaudit_log from extra (ignoring newval, which has already been converted
+ * to a bitmap above). Note that extra may not be set if the assignment is to be
+ * suppressed.
+ */
+static void
+assign_pgaudit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("pgaudit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pgaudit.role = "role1"
+	 *
+	 * This variable defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pgaudit.role",
+							   "Enable auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pgaudit.log = "read, write, ddl"
+	 *
+	 * This variables controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pgaudit.log",
+							   "Enable auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pgaudit_log,
+							   assign_pgaudit_log,
+							   NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pgaudit_ExecutorCheckPerms_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pgaudit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pgaudit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..0b39082
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pgaudit'
+relocatable = true
diff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.pl
new file mode 100755
index 0000000..3a89d50
--- /dev/null
+++ b/contrib/pg_audit/test/test.pl
@@ -0,0 +1,1220 @@
+#!/usr/bin/perl
+################################################################################
+# test.pl - pgAudit Unit Tests
+################################################################################
+
+################################################################################
+# Perl includes
+################################################################################
+use strict;
+use warnings;
+use Carp;
+
+use Getopt::Long;
+use Pod::Usage;
+use DBI;
+use Cwd qw(abs_path);
+use IPC::System::Simple qw(capture);
+
+################################################################################
+# Constants
+################################################################################
+use constant
+{
+	true  => 1,
+	false => 0
+};
+
+use constant
+{
+	CONTEXT_GLOBAL   => 'GLOBAL',
+	CONTEXT_DATABASE => 'DATABASE',
+	CONTEXT_ROLE	 => 'ROLE'
+};
+
+use constant
+{
+	CLASS			=> 'CLASS',
+
+	CLASS_DDL		=> 'DDL',
+	CLASS_FUNCTION	=> 'FUNCTION',
+	CLASS_MISC		=> 'MISC',
+	CLASS_READ		=> 'READ',
+	CLASS_WRITE		=> 'WRITE',
+
+	CLASS_ALL		=> 'ALL',
+	CLASS_NONE		=> 'NONE'
+};
+
+use constant
+{
+	COMMAND						=> 'COMMAND',
+	COMMAND_LOG					=> 'COMMAND_LOG',
+
+	COMMAND_ANALYZE				=> 'ANALYZE',
+	COMMAND_ALTER_AGGREGATE		=> 'ALTER AGGREGATE',
+	COMMAND_ALTER_COLLATION		=> 'ALTER COLLATION',
+	COMMAND_ALTER_CONVERSION	=> 'ALTER CONVERSION',
+	COMMAND_ALTER_DATABASE		=> 'ALTER DATABASE',
+	COMMAND_ALTER_ROLE			=> 'ALTER ROLE',
+	COMMAND_ALTER_ROLE_SET		=> 'ALTER ROLE SET',
+	COMMAND_ALTER_TABLE			=> 'ALTER TABLE',
+	COMMAND_ALTER_TABLE_INDEX	=> 'ALTER TABLE INDEX',
+	COMMAND_BEGIN				=> 'BEGIN',
+	COMMAND_CLOSE				=> 'CLOSE CURSOR',
+	COMMAND_COMMIT				=> 'COMMIT',
+	COMMAND_COPY				=> 'COPY',
+	COMMAND_COPY_TO				=> 'COPY TO',
+	COMMAND_COPY_FROM			=> 'COPY FROM',
+	COMMAND_CREATE_AGGREGATE	=> 'CREATE AGGREGATE',
+	COMMAND_CREATE_COLLATION	=> 'CREATE COLLATION',
+	COMMAND_CREATE_CONVERSION	=> 'CREATE CONVERSION',
+	COMMAND_CREATE_DATABASE		=> 'CREATE DATABASE',
+	COMMAND_CREATE_INDEX		=> 'CREATE INDEX',
+	COMMAND_DEALLOCATE			=> 'DEALLOCATE',
+	COMMAND_DECLARE_CURSOR		=> 'DECLARE CURSOR',
+	COMMAND_DO					=> 'DO',
+	COMMAND_DISCARD_ALL			=> 'DISCARD ALL',
+	COMMAND_CREATE_FUNCTION		=> 'CREATE FUNCTION',
+	COMMAND_CREATE_ROLE			=> 'CREATE ROLE',
+	COMMAND_CREATE_SCHEMA		=> 'CREATE SCHEMA',
+	COMMAND_CREATE_TABLE		=> 'CREATE TABLE',
+	COMMAND_CREATE_TABLE_AS		=> 'CREATE TABLE AS',
+	COMMAND_DROP_DATABASE		=> 'DROP DATABASE',
+	COMMAND_DROP_SCHEMA			=> 'DROP SCHEMA',
+	COMMAND_DROP_TABLE			=> 'DROP TABLE',
+	COMMAND_DROP_TABLE_INDEX	=> 'DROP TABLE INDEX',
+	COMMAND_DROP_TABLE_TYPE		=> 'DROP TABLE TYPE',
+	COMMAND_EXECUTE				=> 'EXECUTE',
+	COMMAND_EXECUTE_READ		=> 'EXECUTE READ',
+	COMMAND_EXECUTE_WRITE		=> 'EXECUTE WRITE',
+	COMMAND_EXECUTE_FUNCTION	=> 'EXECUTE FUNCTION',
+	COMMAND_FETCH				=> 'FETCH',
+	COMMAND_GRANT				=> 'GRANT',
+	COMMAND_INSERT				=> 'INSERT',
+	COMMAND_PREPARE				=> 'PREPARE',
+	COMMAND_PREPARE_READ		=> 'PREPARE READ',
+	COMMAND_PREPARE_WRITE		=> 'PREPARE WRITE',
+	COMMAND_REVOKE				=> 'REVOKE',
+	COMMAND_SELECT				=> 'SELECT',
+	COMMAND_SET					=> 'SET',
+	COMMAND_UPDATE				=> 'UPDATE'
+};
+
+use constant
+{
+	TYPE			=> 'TYPE',
+	TYPE_NONE		=> '',
+
+	TYPE_FUNCTION	=> 'FUNCTION',
+	TYPE_INDEX		=> 'INDEX',
+	TYPE_TABLE		=> 'TABLE',
+	TYPE_TYPE		=> 'TYPE'
+};
+
+use constant
+{
+	NAME			=> 'NAME'
+};
+
+################################################################################
+# Command line parameters
+################################################################################
+my $strPgSqlBin = '../../../../bin/bin';	# Path of PG binaries to use for
+											# this test
+my $strTestPath = '../../../../data';		# Path where testing will occur
+my $iDefaultPort = 6000;					# Default port to run Postgres on
+my $bHelp = false;							# Display help
+my $bQuiet = false;							# Supress output except for errors
+my $bNoCleanup = false;						# Cleanup database on exit
+
+GetOptions ('q|quiet' => \$bQuiet,
+			'no-cleanup' => \$bNoCleanup,
+			'help' => \$bHelp,
+			'pgsql-bin=s' => \$strPgSqlBin,
+			'test-path=s' => \$strTestPath)
+	or pod2usage(2);
+
+# Display version and exit if requested
+if ($bHelp)
+{
+	print 'pg_audit unit test\n\n';
+	pod2usage();
+
+	exit 0;
+}
+
+################################################################################
+# Global variables
+################################################################################
+my $hDb;					# Connection to Postgres
+my $strLogExpected = '';	# The expected log compared with grepping AUDIT
+							# entries from the postgres log.
+
+my $strDatabase = 'postgres';	# Connected database (modified by PgSetDatabase)
+my $strUser = 'postgres';		# Connected user (modified by PgSetUser)
+my $strAuditRole = 'audit';		# Role to use for auditing
+
+my %oAuditLogHash;				# Hash to store pgaudit.log GUCS
+my %oAuditGrantHash;			# Hash to store pgaudit grants
+
+my $strCurrentAuditLog;		# pgaudit.log setting that Postgres was started with
+my $strTemporaryAuditLog;	# pgaudit.log setting that was set hot
+
+################################################################################
+# Stores the mapping between commands, classes, and types
+################################################################################
+my %oCommandHash =
+(&COMMAND_ANALYZE => {
+	&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE_SET => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_ALTER_ROLE},
+	&COMMAND_ALTER_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_ALTER_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_BEGIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CLOSE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COMMIT => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COPY_FROM => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_COPY_TO => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_CREATE_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX},
+	&COMMAND_DEALLOCATE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_DECLARE_CURSOR => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_DO => {&CLASS => &CLASS_FUNCTION, &TYPE => &TYPE_NONE},
+	&COMMAND_DISCARD_ALL => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_FUNCTION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_CREATE_TABLE_AS => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TYPE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TYPE,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_EXECUTE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_FUNCTION => {&CLASS => &CLASS_FUNCTION,
+		&TYPE => &TYPE_FUNCTION, &COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_FETCH => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_GRANT => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_PREPARE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_PREPARE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_INSERT => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE},
+	&COMMAND_REVOKE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_SELECT => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_SET => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_UPDATE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE}
+);
+
+################################################################################
+# CommandExecute
+################################################################################
+sub CommandExecute
+{
+	my $strCommand = shift;
+	my $bSuppressError = shift;
+
+	# Set default
+	$bSuppressError = defined($bSuppressError) ? $bSuppressError : false;
+
+	# Run the command
+	my $iResult = system($strCommand);
+
+	if ($iResult != 0 && !$bSuppressError)
+	{
+		confess "command '${strCommand}' failed with error ${iResult}";
+	}
+}
+
+################################################################################
+# log
+################################################################################
+sub log
+{
+	my $strMessage = shift;
+	my $bError = shift;
+
+	# Set default
+	$bError = defined($bError) ? $bError : false;
+
+	if (!$bQuiet)
+	{
+		print "${strMessage}\n";
+	}
+
+	if ($bError)
+	{
+		exit 1;
+	}
+}
+
+################################################################################
+# ArrayToString
+################################################################################
+sub ArrayToString
+{
+	my @stryArray = @_;
+
+	my $strResult = '';
+
+	for (my $iIndex = 0; $iIndex < @stryArray; $iIndex++)
+	{
+		if ($iIndex != 0)
+		{
+			$strResult .= ', ';
+		}
+
+		$strResult .= $stryArray[$iIndex];
+	}
+
+	return $strResult;
+}
+
+################################################################################
+# BuildModule
+################################################################################
+sub BuildModule
+{
+	capture('cd ..;make');
+	CommandExecute("cp ../pg_audit.so" .
+	               " ${strPgSqlBin}/../lib/postgresql");
+	CommandExecute("cp ../pg_audit.control" .
+	               " ${strPgSqlBin}/../share/postgresql/extension");
+	CommandExecute("cp ../pg_audit--1.0.0.sql" .
+	               " ${strPgSqlBin}/../share/postgresql/extension");
+}
+
+################################################################################
+# PgConnect
+################################################################################
+sub PgConnect
+{
+	my $iPort = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+
+	# Log Connection
+	&log("   DB: connect user ${strUser}, database ${strDatabase}");
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# Connect to the db
+	$hDb = DBI->connect("dbi:Pg:dbname=${strDatabase};port=${iPort};host=/tmp",
+						$strUser, undef,
+						{AutoCommit => 1, RaiseError => 1});
+}
+
+################################################################################
+# PgDisconnect
+################################################################################
+sub PgDisconnect
+{
+	# Connect to the db (whether it is local or remote)
+	if (defined($hDb))
+	{
+		$hDb->disconnect;
+		undef($hDb);
+	}
+}
+
+################################################################################
+# PgExecute
+################################################################################
+sub PgExecute
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	my $hStatement = $hDb->prepare($strSql);
+
+	$hStatement->execute();
+	$hStatement->finish();
+}
+
+################################################################################
+# PgExecuteOnly
+################################################################################
+sub PgExecuteOnly
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	$hDb->do($strSql);
+}
+
+################################################################################
+# PgSetDatabase
+################################################################################
+sub PgSetDatabase
+{
+	my $strDatabaseParam = shift;
+
+	# Stop and start the database to reset pgconf entries
+	PgStop();
+	PgStart();
+
+	# Execute the statement
+	$strDatabase = $strDatabaseParam;
+	PgConnect();
+}
+
+################################################################################
+# PgSetUser
+################################################################################
+sub PgSetUser
+{
+	my $strUserParam = shift;
+
+	$strUser = $strUserParam;
+
+	# Stop and start the database to reset pgconf entries
+	if ((defined($strTemporaryAuditLog) && !defined($strCurrentAuditLog)) ||
+		(defined($strCurrentAuditLog) && !defined($strTemporaryAuditLog)) ||
+		$strCurrentAuditLog ne $strTemporaryAuditLog)
+	{
+		$strCurrentAuditLog = $strTemporaryAuditLog;
+
+		PgStop();
+		PgStart();
+	}
+	else
+	{
+		# Execute the statement
+		PgConnect();
+	}
+}
+
+################################################################################
+# SaveString
+################################################################################
+sub SaveString
+{
+	my $strFile = shift;
+	my $strString = shift;
+
+	# Open the file for writing
+	my $hFile;
+
+	open($hFile, '>', $strFile)
+		or confess "unable to open ${strFile}";
+
+	if ($strString ne '')
+	{
+		syswrite($hFile, $strString)
+			or confess "unable to write to ${strFile}: $!";
+	}
+
+	close($hFile);
+}
+
+################################################################################
+# PgLogExecute
+################################################################################
+sub PgLogExecute
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+	my $bExecute = shift;
+	my $bWait = shift;
+	my $bLogSql = shift;
+
+	# Set defaults
+	$bExecute = defined($bExecute) ? $bExecute : true;
+	$bWait = defined($bWait) ? $bWait : true;
+	$bLogSql = defined($bLogSql) ? $bLogSql : true;
+
+	if ($bExecute)
+	{
+		PgExecuteOnly($strSql);
+	}
+
+	PgLogExpect($strCommand, $bLogSql ? $strSql : '', $oData);
+
+	if ($bWait)
+	{
+		PgLogWait();
+	}
+}
+
+################################################################################
+# PgLogExpect
+################################################################################
+sub PgLogExpect
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+
+	# If oData is false then no logging
+	if (defined($oData) && ref($oData) eq '' && !$oData)
+	{
+		return;
+	}
+
+	# Log based on session
+	if (PgShouldLog($strCommand))
+	{
+		# Make sure class is defined
+		my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+		if (!defined($strClass))
+		{
+			confess "class is not defined for command ${strCommand}";
+		}
+
+		# Make sure object type is defined
+		my $strObjectType = $oCommandHash{$strCommand}{&TYPE};
+
+		if (!defined($strObjectType))
+		{
+			confess "object type is not defined for command ${strCommand}";
+		}
+
+		# Check for command override
+		my $strCommandLog = $strCommand;
+
+		if ($oCommandHash{$strCommand}{&COMMAND})
+		{
+			$strCommandLog = $oCommandHash{$strCommand}{&COMMAND};
+		}
+
+		my $strObjectName = '';
+
+		if (defined($oData) && ref($oData) ne 'ARRAY')
+		{
+			$strObjectName = $oData;
+		}
+
+		my $strLog .= "SESSION,${strClass},${strCommandLog}," .
+					  "${strObjectType},${strObjectName},${strSql}";
+		&log("AUDIT: ${strLog}");
+
+		$strLogExpected .= "${strLog}\n";
+	}
+
+	# Log based on grants
+	if (ref($oData) eq 'ARRAY' && ($strCommand eq COMMAND_SELECT ||
+		$oCommandHash{$strCommand}{&CLASS} eq CLASS_WRITE))
+	{
+		foreach my $oTableHash (@{$oData})
+		{
+			my $strObjectName = ${$oTableHash}{&NAME};
+			my $strCommandLog = ${$oTableHash}{&COMMAND};
+
+			if (defined($oAuditGrantHash{$strAuditRole}
+										{$strObjectName}{$strCommandLog}))
+			{
+				my $strCommandLog = defined(${$oTableHash}{&COMMAND_LOG}) ?
+					${$oTableHash}{&COMMAND_LOG} : $strCommandLog;
+				my $strClass = $oCommandHash{$strCommandLog}{&CLASS};
+				my $strObjectType = ${$oTableHash}{&TYPE};
+
+				my $strLog .= "OBJECT,${strClass},${strCommandLog}," .
+							  "${strObjectType},${strObjectName},${strSql}";
+				&log("AUDIT: ${strLog}");
+
+				$strLogExpected .= "${strLog}\n";
+			}
+		}
+
+		$oData = undef;
+	}
+}
+
+################################################################################
+# PgShouldLog
+################################################################################
+sub PgShouldLog
+{
+	my $strCommand = shift;
+
+	# Make sure class is defined
+	my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+	if (!defined($strClass))
+	{
+		confess "class is not defined for command ${strCommand}";
+	}
+
+	# Check logging for the role
+	my $bLog = undef;
+
+	if (defined($oAuditLogHash{&CONTEXT_ROLE}{$strUser}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_ROLE}{$strUser}{$strClass};
+	}
+
+	# Else check logging for the db
+	elsif (defined($oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}{$strClass};
+	}
+
+	# Else check logging for global
+	elsif (defined($oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}{$strClass};
+	}
+
+	return defined($bLog) ? true : false;
+}
+
+################################################################################
+# PgLogWait
+################################################################################
+sub PgLogWait
+{
+	my $strLogActual;
+
+	# Run in an eval block since grep returns 1 when nothing was found
+	eval
+	{
+		$strLogActual = capture("grep 'LOG:  AUDIT: '" .
+								" ${strTestPath}/postgresql.log");
+	};
+
+	# If an error was returned, continue if it was 1, otherwise confess
+	if ($@)
+	{
+		my $iExitStatus = $? >> 8;
+
+		if ($iExitStatus != 1)
+		{
+			confess "grep returned ${iExitStatus}";
+		}
+
+		$strLogActual = '';
+	}
+
+	# Strip the AUDIT and timestamp from the actual log
+	$strLogActual =~ s/prefix LOG:  AUDIT\: //g;
+
+	# Save the logs
+	SaveString("${strTestPath}/audit.actual", $strLogActual);
+	SaveString("${strTestPath}/audit.expected", $strLogExpected);
+
+	CommandExecute("diff ${strTestPath}/audit.expected" .
+				   " ${strTestPath}/audit.actual");
+}
+
+################################################################################
+# PgDrop
+################################################################################
+sub PgDrop
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Stop the cluster
+	PgStop(true, $strPath);
+
+	# Remove the directory
+	CommandExecute("rm -rf ${strTestPath}");
+}
+
+################################################################################
+# PgCreate
+################################################################################
+sub PgCreate
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	CommandExecute("${strPgSqlBin}/initdb -D ${strPath} -U ${strUser}" .
+				   ' -A trust > /dev/null');
+}
+
+################################################################################
+# PgStop
+################################################################################
+sub PgStop
+{
+	my $bImmediate = shift;
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+	$bImmediate = defined($bImmediate) ? $bImmediate : false;
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# If postmaster process is running then stop the cluster
+	if (-e $strPath . '/postmaster.pid')
+	{
+		CommandExecute("${strPgSqlBin}/pg_ctl stop -D ${strPath} -w -s -m " .
+					  ($bImmediate ? 'immediate' : 'fast'));
+	}
+}
+
+################################################################################
+# PgStart
+################################################################################
+sub PgStart
+{
+	my $iPort = shift;
+	my $strPath = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Make sure postgres is not running
+	if (-e $strPath . '/postmaster.pid')
+	{
+		confess "${strPath}/postmaster.pid exists, cannot start";
+	}
+
+	# Start the cluster
+	CommandExecute("${strPgSqlBin}/pg_ctl start -o \"" .
+				   "-c port=${iPort}" .
+				   " -c unix_socket_directories='/tmp'" .
+				   " -c shared_preload_libraries='pg_audit'" .
+				   " -c log_min_messages=debug1" .
+				   " -c log_line_prefix='prefix '" .
+				   # " -c log_destination='stderr,csvlog'" .
+				   # " -c logging_collector=on" .
+				   (defined($strCurrentAuditLog) ?
+					   " -c pgaudit.log='${strCurrentAuditLog}'" : '') .
+				   " -c pgaudit.role='${strAuditRole}'" .
+				   " -c log_connections=on" .
+				   "\" -D ${strPath} -l ${strPath}/postgresql.log -w -s");
+
+	# Connect user session
+	PgConnect();
+}
+
+################################################################################
+# PgAuditLogSet
+################################################################################
+sub PgAuditLogSet
+{
+	my $strContext = shift;
+	my $strName = shift;
+	my @stryClass = @_;
+
+	# Create SQL to set the GUC
+	my $strCommand;
+	my $strSql;
+
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		$strCommand = COMMAND_SET;
+		$strSql = "set pgaudit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+		$strTemporaryAuditLog = ArrayToString(@stryClass);
+	}
+	elsif ($strContext eq CONTEXT_ROLE)
+	{
+		$strCommand = COMMAND_ALTER_ROLE_SET;
+		$strSql = "alter role ${strName} set pgaudit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+	}
+	else
+	{
+		confess "unable to set pgaudit.log for context ${strContext}";
+	}
+
+	# Reset the audit log
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		delete($oAuditLogHash{$strContext});
+		$strName = CONTEXT_GLOBAL;
+	}
+	else
+	{
+		delete($oAuditLogHash{$strContext}{$strName});
+	}
+
+	# Store all the classes in the hash and build the GUC
+	foreach my $strClass (@stryClass)
+	{
+		if ($strClass eq CLASS_ALL)
+		{
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_DDL} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_FUNCTION} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_MISC} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_READ} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_WRITE} = true;
+		}
+
+		if (index($strClass, '-') == 0)
+		{
+			$strClass = substr($strClass, 1);
+
+			delete($oAuditLogHash{$strContext}{$strName}{$strClass});
+		}
+		else
+		{
+			$oAuditLogHash{$strContext}{$strName}{$strClass} = true;
+		}
+	}
+
+	PgLogExecute($strCommand, $strSql);
+}
+
+################################################################################
+# PgAuditGrantSet
+################################################################################
+sub PgAuditGrantSet
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_GRANT, "grant " . lc(${strPrivilege}) .
+								(defined($strColumn) ? " (${strColumn})" : '') .
+								" on ${strObject} to ${strRole}");
+
+	$oAuditGrantHash{$strRole}{$strObject}{$strPrivilege} = true;
+}
+
+################################################################################
+# PgAuditGrantReset
+################################################################################
+sub PgAuditGrantReset
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_REVOKE, "revoke " . lc(${strPrivilege}) .
+				 (defined($strColumn) ? " (${strColumn})" : '') .
+				 " on ${strObject} from ${strRole}");
+
+	delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege});
+}
+
+################################################################################
+# Main
+################################################################################
+my @oyTable; # Store table info for select, insert, update, delete
+
+# Drop the old cluster, build the code, and create a new cluster
+PgDrop();
+BuildModule();
+PgCreate();
+PgStart();
+
+PgExecute("create extension pg_audit");
+
+# Create test users and the audit role
+PgExecute("create user user1");
+PgExecute("create user user2");
+PgExecute("create role ${strAuditRole}");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL));
+
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_READ, CLASS_WRITE));
+
+# User1 follows the global log settings
+PgSetUser('user1');
+PgLogExecute(COMMAND_CREATE_TABLE, 'create table test (id int)', 'public.test');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+PgLogExecute(COMMAND_DROP_TABLE, 'drop table test', 'public.test');
+
+PgSetUser('user2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+             'create table test2 (id int)', 'public.test2');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+             'create table test3 (id int)', 'public.test2');
+
+# Catalog select should not log
+PgLogExecute(COMMAND_SELECT, 'select * from pg_class limit 1',
+							   false);
+
+# Multi-table select
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test3, test2',
+							   \@oyTable);
+
+# Various CTE combinations
+PgAuditGrantSet($strAuditRole, &COMMAND_INSERT, 'public.test3');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (select id from test2)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (insert into test3 values (1) returning id)' .
+			 ' insert into test2 select id from cte',
+			 \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE, 'public.test2');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_INSERT,
+             'with cte as (update test2 set id = 1 returning id)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_INSERT});
+PgLogExecute(COMMAND_UPDATE,
+			 'with cte as (insert into test2 values (1) returning id)' .
+			 ' update test3 set id = cte.id' .
+			 ' from cte where test3.id <> cte.id',
+			 \@oyTable);
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_NONE));
+PgSetUser('user2');
+
+# Column-based audits
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test4 (id int, name text)', 'public.test4');
+PgAuditGrantSet($strAuditRole, COMMAND_SELECT, 'public.test4', 'name');
+PgAuditGrantSet($strAuditRole, COMMAND_UPDATE, 'public.test4', 'id');
+PgAuditGrantSet($strAuditRole, COMMAND_INSERT, 'public.test4', 'name');
+
+# Select
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id from public.test4',
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from public.test4',
+							  \@oyTable);
+
+# Insert
+@oyTable = ();
+PgLogExecute(COMMAND_INSERT, 'insert into public.test4 (id) values (1)',
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT, "insert into public.test4 (name) values ('test')",
+							  \@oyTable);
+
+# Update
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set name = 'foo'",
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set id = 1",
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+            &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update public.test4 set name = 'foo' where name = 'bar'",
+			 \@oyTable);
+
+# Drop test tables
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test2", 'public.test2');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test3", 'public.test3');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test4", 'public.test4');
+
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Now create some email friendly tests.  These first tests are session logging
+# only.
+PgSetUser('postgres');
+
+&log("\nExamples:");
+
+&log("\nSession Audit:\n");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL, CLASS_READ));
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account (id int, name text, password text,' .
+			 ' description text)', 'public.account');
+PgLogExecute(COMMAND_SELECT,
+			 'select * from account');
+PgLogExecute(COMMAND_INSERT,
+			 "insert into account (id, name, password, description)" .
+			 " values (1, 'user1', 'HASH1', 'blah, blah')");
+&log("AUDIT: <nothing logged>");
+
+# Now tests for object logging
+&log("\nObject Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_NONE));
+PgExecute("set pgaudit.role = 'audit'");
+PgSetUser('user1');
+
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account', 'password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id, name from account',
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE,
+                'public.account', 'name, password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Now tests for session/object logging
+&log("\nSession/Object Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user1', (CLASS_READ, CLASS_WRITE));
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account_role_map (account_id int, role_id int)',
+			 'public.account_role_map');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account_role_map');
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.account_role_map', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT,
+			 'select account.password, account_role_map.role_id from account' .
+			 ' inner join account_role_map' .
+			 ' on account.id = account_role_map.account_id',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update account set description = 'yada, yada'" .
+			 " where password = 'HASH2'",
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Test all sql commands
+&log("\nExhaustive Command Tests:\n");
+
+PgSetUser('postgres');
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_ALL));
+PgLogExecute(COMMAND_SET, "set pgaudit.role = 'audit'");
+
+PgLogExecute(COMMAND_DO, "do \$\$\ begin raise notice 'test'; end; \$\$;");
+PgLogExecute(COMMAND_CREATE_SCHEMA, "create schema test");
+
+# Test COPY
+PgLogExecute(COMMAND_COPY_TO,
+			 "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'");
+PgLogExecute(COMMAND_CREATE_TABLE_AS,
+			 "CREATE TABLE test.pg_class as select * from pg_class",
+			 'test.pg_class', true, false);
+PgLogExecute(COMMAND_INSERT,
+			 "CREATE TABLE test.pg_class as select * from pg_class",
+			 undef, false, true);
+PgLogExecute(COMMAND_INSERT,
+			 "COPY test.pg_class from '" . abs_path($strTestPath) .
+			 "/class.out'", undef, true, false);
+PgLogExecute(COMMAND_COPY_FROM,
+			 "COPY test.pg_class from '" . abs_path($strTestPath) .
+			 "/class.out'", undef, false, true);
+
+# Test prepared SELECT
+PgLogExecute(COMMAND_PREPARE_READ,
+			 'PREPARE pgclassstmt (oid) as select *' .
+			 ' from pg_class where oid = $1');
+PgLogExecute(COMMAND_EXECUTE_READ,
+			 'EXECUTE pgclassstmt (1)');
+PgLogExecute(COMMAND_DEALLOCATE,
+			 'DEALLOCATE pgclassstmt');
+
+# Test cursor
+PgLogExecute(COMMAND_BEGIN,
+			 'BEGIN');
+PgLogExecute(COMMAND_DECLARE_CURSOR,
+		     'DECLARE ctest SCROLL CURSOR FOR SELECT * FROM pg_class');
+PgLogExecute(COMMAND_FETCH,
+			 'FETCH NEXT FROM ctest');
+PgLogExecute(COMMAND_CLOSE,
+			 'CLOSE ctest');
+PgLogExecute(COMMAND_COMMIT,
+			 'COMMIT');
+
+# Test prepared INSERT
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test.test_insert (id int)', 'test.test_insert');
+PgLogExecute(COMMAND_PREPARE_WRITE,
+			 'PREPARE pgclassstmt (oid) as insert' .
+			 ' into test.test_insert (id) values ($1)');
+PgLogExecute(COMMAND_INSERT,
+			 'EXECUTE pgclassstmt (1)', undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_WRITE,
+			 'EXECUTE pgclassstmt (1)', undef, false, true);
+
+# Create a table with a primary key
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test (id int primary key, name text,' .
+			 'description text)',
+			 'public.test', true, false);
+PgLogExecute(COMMAND_CREATE_INDEX,
+			 'create table test (id int primary key, name text,' .
+			 'description text)',
+			 'public.test_pkey', false, true);
+PgLogExecute(COMMAND_ANALYZE, 'analyze test');
+
+# Grant select to public - this should have no affect on auditing
+PgLogExecute(COMMAND_GRANT, 'grant select on public.test to public');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+# Now grant select to audit and it should be logged
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test', \@oyTable);
+
+# Check columns granted to public and make sure they do not log
+PgAuditGrantReset($strAuditRole, &COMMAND_SELECT, 'public.test');
+PgLogExecute(COMMAND_GRANT, 'grant select (name) on public.test to public');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+PgLogExecute(COMMAND_SELECT, 'select from test');
+
+# Now set grant to a specific column to audit and make sure it logs
+# Make sure the the converse is true
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test',
+				'name, description');
+PgLogExecute(COMMAND_SELECT, 'select id from test');
+
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from test', \@oyTable);
+
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test drop description', 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable);
+
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test rename to test2', 'public.test');
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test2 set schema test', 'public.test2', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE_INDEX, 'alter table test2 set schema test',
+										'public.test_pkey', false, true);
+PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 add description text',
+								  'test.test2');
+PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 drop description',
+								  'test.test2');
+PgLogExecute(COMMAND_DROP_TABLE_INDEX, 'drop table test.test2',
+									   'test.test_pkey', false, false);
+PgLogExecute(COMMAND_DROP_TABLE, 'drop table test.test2',
+								 'test.test2', true, true);
+
+PgLogExecute(COMMAND_CREATE_FUNCTION, 'CREATE FUNCTION int_add(a int, b int)' .
+									  ' returns int as $$ begin return a + b;' .
+									  ' end $$language plpgsql');
+PgLogExecute(COMMAND_EXECUTE_FUNCTION, "select int_add(1, 1)",
+									   'public.int_add');
+
+PgLogExecute(COMMAND_CREATE_AGGREGATE, "CREATE AGGREGATE sum_test (int)" .
+							" (sfunc = int_add, stype = int, initcond = 0)");
+PgLogExecute(COMMAND_ALTER_AGGREGATE,
+			 "ALTER AGGREGATE sum_test (int) rename to sum_test2");
+
+PgLogExecute(COMMAND_CREATE_COLLATION,
+			 "CREATE COLLATION collation_test FROM \"de_DE\"");
+PgLogExecute(COMMAND_ALTER_COLLATION,
+			 "ALTER COLLATION collation_test rename to collation_test2");
+
+PgLogExecute(COMMAND_CREATE_CONVERSION,
+			 "CREATE CONVERSION conversion_test FOR 'SQL_ASCII' TO".
+			 " 'MULE_INTERNAL' FROM ascii_to_mic");
+PgLogExecute(COMMAND_ALTER_CONVERSION,
+			 "ALTER CONVERSION conversion_test rename to conversion_test2");
+
+PgLogExecute(COMMAND_CREATE_DATABASE, "CREATE DATABASE database_test");
+PgLogExecute(COMMAND_ALTER_DATABASE,
+			 "ALTER DATABASE database_test rename to database_test2");
+PgLogExecute(COMMAND_DROP_DATABASE, "DROP DATABASE database_test2");
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Stop the database
+if (!$bNoCleanup)
+{
+	PgDrop();
+}
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index a698d0f..5b247a9 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index f03b72a..e4f0bdc 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -124,6 +124,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbench         SYSTEM "pgbench.sgml">
 <!ENTITY pgarchivecleanup SYSTEM "pgarchivecleanup.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..f3f4ab9
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,316 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> module provides session and object
+    auditing via the standard logging facility.  Session and object auditing are
+    completely independent and can be combined.
+  </para>
+
+  <sect2>
+    <title>Session Auditing</title>
+
+    <para>
+      Session auditing allows the logging of all commands that are executed by
+      a user in the backend.  Each command is logged with a single entry and
+      includes the audit type (e.g. <literal>SESSION</literal>), command type
+      (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and
+      statement (e.g. <literal>"select * from test"</literal>).
+
+      Fully-qualified names and object types will be logged for
+      <literal>CREATE</literal>, <literal>UPDATE</literal>, and
+      <literal>DROP</literal> commands on <literal>TABLE</literal>,
+      <literal>MATVIEW</literal>, <literal>VIEW</literal>,
+      <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,
+      <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and
+      <literal>SEQUENCE</literal> objects as well as function calls.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is controlled by the <literal>pgaudit.log</literal> GUC.
+        There are five classes of commands that are recognized:
+
+        <itemizedlist>
+          <listitem>
+            <para>
+              <literal>READ</literal> - <literal>SELECT</literal> and
+              <literal>COPY</literal> when the source is a table or query.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>WRITE</literal> - <literal>INSERT</literal>,
+              <literal>UPDATE</literal>, <literal>DELETE</literal>,
+              <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+              destination is a table.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>FUNCTION</literal> - Function calls and
+              <literal>DO</literal> blocks.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>DDL</literal> - DDL, plus <literal>VACUUM</literal>,
+              <literal>REINDEX</literal>, and <literal>ANALYZE</literal>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>MISC</literal> - Miscellaneous commands, e.g.
+              <literal>DISCARD</literal>, <literal>FETCH</literal>,
+              <literal>CHECKPOINT</literal>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </para>
+
+      <para>
+        Enable session logging for all writes and DDL:
+          <programlisting>
+pgaudit.log = 'write, ddl'
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except miscellaneous:
+          <programlisting>
+pgaudit.log = 'all, -misc'
+          </programlisting>
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pgaudit.log = 'read, ddl'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+      <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+select *
+    from account;
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Object Auditing</title>
+
+    <para>
+      Object auditing logs commands that affect a particular object.  Only
+      <literal>SELECT</literal>, <literal>INSERT</literal>,
+      <literal>UPDATE</literal> and <literal>DELETE</literal> commands are
+      supported.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level auditing is implemented via the roles system.  The
+        <literal>pgaudit.role</literal> GUC defines the role that will be used
+        for auditing.  An object will be audited when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.
+      </para>
+
+      <programlisting>
+postresql.conf: pgaudit.role = 'audit'
+
+grant select, delete
+   on public.account;
+      </programlisting>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pgaudit.role = 'audit'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to audit;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to audit;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to audit;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is not in compliant CSV format.  If machine-readability is
+          required then consider setting
+          <literal>log_destination = 'csvlog'</literal>.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</literal> - <literal>SESSION</literal> or
+            <literal>OBJECT</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</literal> - <literal>READ</literal>,
+            <literal>WRITE</literal>, <literal>FUNCTION</literal>,
+            <literal>DDL</literal>, or <literal>MISC</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</literal> - <literal>ALTER TABLE</literal>,
+            <literal>SELECT</literal>, <literal>CREATE INDEX</literal>,
+            <literal>UPDATE</literal>, etc.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</literal> - <literal>TABLE</literal>,
+            <literal>INDEX</literal>, <literal>VIEW</literal>, etc.  Only
+            available for DML and certain DDL commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</literal> - The fully-qualified object name
+            (e.g. public.account).  Only available for DML and certain DDL
+            commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</literal> - Statement execute on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Author</title>
+
+    <para>
+      David Steele <email>david@pgmasters.net</email>
+    </para>
+  </sect2>
+</sect1>
#2Stephen Frost
sfrost@snowman.net
In reply to: David Steele (#1)
Re: Auditing extension for PostgreSQL (Take 2)

David,

I've CC'd Abhijit, the original author of pgaudit, as it seems likely
he'd also be interested in this.

* David Steele (david@pgmasters.net) wrote:

I've posted a couple of messages over the last few weeks about the work
I've been doing on the pg_audit extension. The lack of response could
be due to either universal acclaim or complete apathy, but in any case I
think this is a very important topic so I want to give it another try.

Thanks! It's certainly an important topic to a lot of folks; I imagine
the lack of response is simply because people are busy.

I've extensively reworked the code that was originally submitted by
2ndQuandrant. This is not an indictment of their work, but rather an
attempt to redress concerns that were expressed by members of the
community. I've used core functions to determine how audit events
should be classified and simplified and tightened the code wherever
possible. I've removed deparse and event triggers and opted for methods
that rely only on existing hooks. In my last message I provided
numerous examples of configuration, usage, and output which I hoped
would alleviate concerns of complexity. I've also written a ton of unit
tests to make sure that the code works as expected.

The configuration approach you posted is definitely in-line with what I
was trying to get at previously- thanks for putting some actual code
behind it! While not a big fan of that other big RDBMS, I do like that
this approach ends up being so similar in syntax.

I realize this is not an ideal solution. Everybody (including me) wants
something that is in core with real policies and more options. It's
something that I am personally really eager to work on. But the reality
is that's not going to happen for 9.5 and probably not for 9.6 either.
Meanwhile, I believe the lack of some form of auditing is harming
adoption of PostgreSQL, especially in the financial and government sectors.

Agreed.

The patch I've attached satisfies the requirements that I've had from
customers in the past. I'm confident that if it gets out into the wild
it will bring all kinds of criticism and comments which will be valuable
in designing a robust in-core solution.

This is definitely something that makes sense to me, particularly for
such an important piece. I had argued previously that a contrib based
solution would make it difficult to build an in-core solution, but
others convinced me that it'd not only be possible but would probably be
preferrable as we'd gain experience with the contrib module and, as you
say, we'd be able to build a better in-core solution based on that
experience.

I'm submitting this patch to the Commitfest. I'll do everything I can
to address the concerns of the community and I'm happy to provide more
examples as needed. I'm hoping the sgml docs I've provided with the
patch will cover any questions, but of course feedback is always
appreciated.

Glad you submitted it to the CommitFest. Just glancing through the
code, it certainly looks a lot closer to being something which we could
move forward with. Using existing functions to work out the categories
(instead of massive switch statements) is certainly much cleaner and
removing those large #ifdef blocks has made the code a lot easier to
follow.

Lastly, I really like all the unit tests..

Additional comments in-line follow.

diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..b3914ac
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1099 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements.

It'd be good to quantify what 'many' means above.

+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_prewarm/pg_prewarm.c

Pretty sure this isn't pg_prewarm.c :)

+/*
+ * String contants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */

"String constants"

+/*
+ * String contants for log classes - used when processing tokens in the
+ * pgaudit.log GUC.
+ */

Ditto.

+/* String contants for logging commands */

Ditto. :)

+/*
+ * This module collects AuditEvents from various sources (event triggers, and
+ * executor/utility hooks) and passes them to the log_audit_event() function.

This isn't using event triggers any more, right? Doesn't look like it.
I don't think that's a problem and it certainly seems to have simplified
things quite a bit, but the comment should be updated.

+/*
+ * Returns the oid of the role specified in pgaudit.role.
+ */
+static Oid
+audit_role_oid()

Couldn't you use get_role_oid() instead of having your own function..?

+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */

I'm a bit on the fence about this.. Can you provide a use-case where
doing this makes sense..? Does this mean I could grant admin_role1 to
audit and then get auditing on everything that user1 has access to?
That seems like it might be useful for environments where such roles
already exist though it might end up covering more than is intended..

+ /* Make a copy of the column set */
+ tmpSet = bms_copy(attributeSet);

The comment should probably say why, eg:

/* bms_first_member is destructive, so make a copy before using it. */

+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("pgaudit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pgaudit.role = "role1"

I'd make that example "audit", since that's what is generally expected,
right?

+################################################################################
+# test.pl - pgAudit Unit Tests
+################################################################################

This is definitiely nice..

diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..f3f4ab9
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,316 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>

There seems to be a number of places which are 'pgaudit' and a bunch
that are 'pg_audit'. I'm guessing you were thinking 'pg_audit', but
it'd be good to clean up and make them all consistent.

+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> module provides session and object
+    auditing via the standard logging facility.  Session and object auditing are
+    completely independent and can be combined.
+  </para>
+
+  <sect2>
+    <title>Session Auditing</title>
+
+    <para>
+      Session auditing allows the logging of all commands that are executed by
+      a user in the backend.  Each command is logged with a single entry and
+      includes the audit type (e.g. <literal>SESSION</literal>), command type
+      (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and
+      statement (e.g. <literal>"select * from test"</literal>).
+
+      Fully-qualified names and object types will be logged for
+      <literal>CREATE</literal>, <literal>UPDATE</literal>, and
+      <literal>DROP</literal> commands on <literal>TABLE</literal>,
+      <literal>MATVIEW</literal>, <literal>VIEW</literal>,
+      <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,
+      <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and
+      <literal>SEQUENCE</literal> objects as well as function calls.
+    </para>

Ah, you do have a list of what objects you get fully qualified names
for, nice. Are there obvious omissions from that list..? If so, we
might be able to change what happens with objectAccess to include them..

+      <para>
+        Enable session logging for all commands except miscellaneous:
+          <programlisting>
+pgaudit.log = 'all, -misc'
+          </programlisting>
+      </para>
+    </sect3>

Nice, I like that.

+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pgaudit.log = 'read, ddl'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>

Perhaps I missed it, but it'd be good to point out that GUCs can be set
at various levels. I know we probably say that somewhere else, but it's
particularly relevant for this.

That was just a quick read. Would be great if someone else who is
interested would take it for a spin and provide their own feedback.

Thanks again!

Stephen

#3Simon Riggs
simon@2ndQuadrant.com
In reply to: David Steele (#1)
Re: Auditing extension for PostgreSQL (Take 2)

On 15 February 2015 at 02:34, David Steele <david@pgmasters.net> wrote:

I've posted a couple of messages over the last few weeks about the work
I've been doing on the pg_audit extension. The lack of response could
be due to either universal acclaim or complete apathy, but in any case I
think this is a very important topic so I want to give it another try.

You mentioned you had been following the thread for some time and yet
had not contributed to it. Did that indicate your acclaim for the
earlier patch, or was that apathy? I think neither.

People have been working on this feature for >9 months now, so you
having to wait 9 days for a response is neither universal acclaim, nor
apathy. I've waited much longer than that for Stephen to give the
review he promised, but have not bad mouthed him for that wait, nor do
I do so now. In your first post you had removed the author's email
addresses, so they were likely unaware of your post. I certainly was.

I've extensively reworked the code that was originally submitted by
2ndQuandrant. This is not an indictment of their work, but rather an
attempt to redress concerns that were expressed by members of the
community. I've used core functions to determine how audit events
should be classified and simplified and tightened the code wherever
possible. I've removed deparse and event triggers and opted for methods
that rely only on existing hooks. In my last message I provided
numerous examples of configuration, usage, and output which I hoped
would alleviate concerns of complexity. I've also written a ton of unit
tests to make sure that the code works as expected.

Some people that have contributed ideas to this patch are from
2ndQuadrant, some are not. The main point is that we work together on
things, rather than writing a slightly altered version and then
claiming credit.

If you want to help, please do. We give credit where its due, not to
whoever touched the code last in some kind of bidding war. If we let
this happen, we'd generate a flood of confusing patch versions and
little would ever get committed.

Let's keep to one thread and work to include everybody's ideas then
we'll get something useful committed.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, RemoteDBA, 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

#4David Steele
david@pgmasters.net
In reply to: Simon Riggs (#3)
Re: Auditing extension for PostgreSQL (Take 2)

On 2/18/15 8:25 AM, Simon Riggs wrote:

On 15 February 2015 at 02:34, David Steele <david@pgmasters.net> wrote:

I've posted a couple of messages over the last few weeks about the work
I've been doing on the pg_audit extension. The lack of response could
be due to either universal acclaim or complete apathy, but in any case I
think this is a very important topic so I want to give it another try.

You mentioned you had been following the thread for some time and yet
had not contributed to it. Did that indicate your acclaim for the
earlier patch, or was that apathy? I think neither.

In my case it actually was acclaim. I was happy with the direction
things were going and had nothing in particular to add - and I didn't
think a +1 from me was going to carry any weight with the community.

I can see now that everyone's opinion matters here, so I'll be more
active about weighing in when I think something is valuable.

People have been working on this feature for >9 months now, so you
having to wait 9 days for a response is neither universal acclaim, nor
apathy. I've waited much longer than that for Stephen to give the
review he promised, but have not bad mouthed him for that wait, nor do
I do so now. In your first post you had removed the author's email
addresses, so they were likely unaware of your post. I certainly was.

I understand that, but with the CF closing I felt like I had to act.
Abhijit's last comment on the thread was that he was no longer going to
work on it in relation to 9.5. I felt that it was an important feature
(and one that I have a lot of interest in), so that's when I got involved.

I posted two messages, but I only addressed one of them directly to
Abhijit. As you said, I'm new here and I'm still getting used to the
way things are done.

I've extensively reworked the code that was originally submitted by
2ndQuandrant. This is not an indictment of their work, but rather an
attempt to redress concerns that were expressed by members of the
community. I've used core functions to determine how audit events
should be classified and simplified and tightened the code wherever
possible. I've removed deparse and event triggers and opted for methods
that rely only on existing hooks. In my last message I provided
numerous examples of configuration, usage, and output which I hoped
would alleviate concerns of complexity. I've also written a ton of unit
tests to make sure that the code works as expected.

Some people that have contributed ideas to this patch are from
2ndQuadrant, some are not. The main point is that we work together on
things, rather than writing a slightly altered version and then
claiming credit.

If you want to help, please do. We give credit where its due, not to
whoever touched the code last in some kind of bidding war. If we let
this happen, we'd generate a flood of confusing patch versions and
little would ever get committed.

Agreed, and I apologize if I came off that way. It certainly wasn't my
intention. I was hesitant because I had made so many changes and I
wasn't sure how the authors would feel about it. I wrote to them
privately to get their take on the situation.

Let's keep to one thread and work to include everybody's ideas then
we'll get something useful committed.

I'm a little confused about how to proceed here. I created a new thread
because the other patch had already been rejected. How should I handle
that?

--
- David Steele
david@pgmasters.net

#5David Steele
david@pgmasters.net
In reply to: Stephen Frost (#2)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

Hi Stephen,

Thanks for your review. All fixed except for comments below:

On 2/17/15 10:34 AM, Stephen Frost wrote:

+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */

I'm a bit on the fence about this.. Can you provide a use-case where
doing this makes sense..? Does this mean I could grant admin_role1 to
audit and then get auditing on everything that user1 has access to?
That seems like it might be useful for environments where such roles
already exist though it might end up covering more than is intended..

The idea is that if there are already ready-made roles to be audited
then they don't need to be reconstituted for the audit role. You could
just do:

grant admin_role to audit;
grant user_role to audit;

Of course, we could list multiple roles in the pg_audit.role GUC, but I
thought this would be easier to use and maintain since there was some
worry about GUCs being fragile when they refer to database objects.

+################################################################################
+# test.pl - pgAudit Unit Tests
+################################################################################

This is definitiely nice..

diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..f3f4ab9
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,316 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>

There seems to be a number of places which are 'pgaudit' and a bunch
that are 'pg_audit'. I'm guessing you were thinking 'pg_audit', but
it'd be good to clean up and make them all consistent.

Fixed, though I still left the file name as pgaudit.sgml since all but
one module follow that convention.

+    <para>
+      Session auditing allows the logging of all commands that are executed by
+      a user in the backend.  Each command is logged with a single entry and
+      includes the audit type (e.g. <literal>SESSION</literal>), command type
+      (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and
+      statement (e.g. <literal>"select * from test"</literal>).
+
+      Fully-qualified names and object types will be logged for
+      <literal>CREATE</literal>, <literal>UPDATE</literal>, and
+      <literal>DROP</literal> commands on <literal>TABLE</literal>,
+      <literal>MATVIEW</literal>, <literal>VIEW</literal>,
+      <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,
+      <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and
+      <literal>SEQUENCE</literal> objects as well as function calls.
+    </para>

Ah, you do have a list of what objects you get fully qualified names
for, nice. Are there obvious omissions from that list..? If so, we
might be able to change what happens with objectAccess to include them..

It seems like these are the objects where having a name really matters.
I'm more interested in using the deparse code to handle fully-qualified
names for additional objects rather than adding hooks.

+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pgaudit.log = 'read, ddl'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>

Perhaps I missed it, but it'd be good to point out that GUCs can be set
at various levels. I know we probably say that somewhere else, but it's
particularly relevant for this.

Yes, it's very relevant for this patch. Do you think it's enough to
call out the functionality, or should I provide examples? Maybe a
separate section just for this concept?

Patch v2 is attached for all changes except the doc change above.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v2.patchtext/plain; charset=UTF-8; name=pg_audit-v2.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index 195d447..d8e75f4 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -29,6 +29,7 @@ SUBDIRS = \
 		pageinspect	\
 		passwordcheck	\
 		pg_archivecleanup \
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..32bc6d9
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,20 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..2eee3b9
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,4 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..ead65a8
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1102 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * auditRole is the string value of the pgaudit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pgaudit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pgaudit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC		    "MISC"
+#define CLASS_READ			"READ"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* SELECT */
+	LOG_READ = (1 << 0),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 1),
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 2),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 4),
+
+	/* Function execution */
+	LOG_MISC = (1 << 5),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If an underlying command affects multiple objects multiple
+ * AuditEvents must be created to represent it.
+ */
+typedef struct
+{
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	bool granted;
+} AuditEvent;
+
+/*
+ * Set if a function below log_utility_command() has logged the event - prevents
+ * more than one function from logging when the event could be logged in
+ * multiple places.
+ */
+bool utilityCommandLogged = false;
+bool utilityCommandInProgress = false;
+AuditEvent utilityAuditEvent;
+
+/*
+ * Takes an AuditEvent and returns true or false depending on whether the event
+ * should be logged according to the pgaudit.roles/log settings. If it returns
+ * true, also fills in the name of the LogClass which it is logged under.
+ */
+static bool
+log_check(AuditEvent *e, const char **classname)
+{
+	enum LogClass class = LOG_NONE;
+
+	/* By default put everything in the MISC class. */
+	*classname = CLASS_MISC;
+	class = LOG_MISC;
+
+	/*
+	 * Look at the type of the command and decide what LogClass needs to be
+	 * enabled for the command to be logged.
+	 */
+	switch (e->logStmtLevel)
+	{
+		case LOGSTMT_MOD:
+			*classname = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		case LOGSTMT_DDL:
+			*classname = CLASS_DDL;
+			class = LOG_DDL;
+
+		case LOGSTMT_ALL:
+			switch (e->commandTag)
+			{
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					*classname = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				case T_VacuumStmt:
+				case T_ReindexStmt:
+					*classname = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				case T_DoStmt:
+					*classname = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * We log audit events under the following conditions:
+	 *
+	 * 1. If the audit role has been explicitly granted permission for
+	 *    an operation.
+	 */
+	if (e->granted)
+	{
+		return true;
+	}
+
+	/* 2. If the event belongs to a class covered by pgaudit.log. */
+	if ((auditLogBitmap & class) == class)
+	{
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * Takes an AuditEvent and, if it log_check(), writes it to the audit log. The
+ * AuditEvent is assumed to be completely filled in by the caller (unknown
+ * values must be set to "" so that they can be logged without error checking).
+ */
+static void
+log_audit_event(AuditEvent *e)
+{
+	const char *classname;
+
+	/* Check that this event should be logged. */
+	if (!log_check(e, &classname))
+		return;
+
+	/* Log via ereport(). */
+	ereport(LOG,
+			(errmsg("AUDIT: %s,%s,%s,%s,%s,%s",
+					e->granted ? AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+					classname, e->command, e->objectType, e->objectName,
+					e->commandText),
+			 errhidestmt(true)));
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+log_acl_check(Datum aclDatum, Oid auditOid, AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem    *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+log_relation_check(Oid relOid,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = log_acl_check(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+log_attribute_check(Oid relOid,
+					AttrNumber attNum,
+					Oid auditOid,
+					AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = log_acl_check(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+log_attribute_check_any(Oid relOid,
+						Oid auditOid,
+						Bitmapset *attributeSet,
+						AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (log_attribute_check(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			log_attribute_check(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for DML operations via executor permissions checks.
+ */
+static void
+log_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	AuditEvent auditEvent;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		auditEvent.logStmtLevel = LOGSTMT_MOD;
+
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEvent.commandTag = T_InsertStmt;
+			auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEvent.commandTag = T_UpdateStmt;
+			auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEvent.commandTag = T_DeleteStmt;
+			auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEvent.commandTag = T_SelectStmt;
+			auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEvent.commandTag = T_Invalid;
+			auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEvent.granted = false;
+		auditEvent.commandText = debug_query_string;
+
+		/* If this is the first rte then session log */
+		if (first)
+		{
+			auditEvent.objectName = "";
+			auditEvent.objectType = "";
+
+			log_audit_event(&auditEvent);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEvent.objectType = OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEvent.objectType = OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEvent.objectType = OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEvent.objectType = OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEvent.objectType = OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEvent.objectType = OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEvent.objectType = OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (log_relation_check(relOid, auditOid, auditPerms))
+			{
+				auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEvent.granted =
+						log_attribute_check_any(relOid, auditOid,
+												rte->selectedCols,
+												ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEvent.granted =
+							log_attribute_check_any(relOid, auditOid,
+													rte->modifiedCols,
+													auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Only do relation level logging if a grant was found. */
+		if (auditEvent.granted)
+		{
+			log_audit_event(&auditEvent);
+		}
+
+		pfree(auditEvent.objectName);
+	}
+
+	/*
+	 * If the first flag was never set to false, then rangeTabls was empty. In
+	 * this case log a session select statement.
+	 */
+	if (first && !utilityCommandInProgress)
+	{
+		auditEvent.logStmtLevel = LOGSTMT_ALL;
+		auditEvent.commandTag = T_SelectStmt;
+		auditEvent.command = COMMAND_SELECT;
+		auditEvent.granted = false;
+		auditEvent.commandText = debug_query_string;
+		auditEvent.objectName = "";
+		auditEvent.objectType = "";
+
+		log_audit_event(&auditEvent);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		utilityAuditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				utilityAuditEvent.objectType = OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				utilityAuditEvent.objectType = OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				utilityAuditEvent.objectType = OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				utilityAuditEvent.objectType = OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+
+		/* Log the event */
+		log_audit_event(&utilityAuditEvent);
+		utilityCommandLogged = true;
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Generate the fully-qualified function name. */
+	utilityAuditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the event */
+	utilityAuditEvent.logStmtLevel = LOGSTMT_ALL;
+	utilityAuditEvent.commandTag = T_DoStmt;
+	utilityAuditEvent.command = COMMAND_EXECUTE;
+	utilityAuditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	utilityAuditEvent.commandText = debug_query_string;
+
+	log_audit_event(&utilityAuditEvent);
+	utilityCommandLogged = true;
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute. */
+		case OAT_FUNCTION_EXECUTE:
+			log_function_execute(objectId);
+			break;
+
+		/* Log create. */
+		case OAT_POST_CREATE:
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter. */
+		case OAT_POST_ALTER:
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop. */
+		case OAT_DROP:
+		{
+			ObjectAccessDrop *drop = arg;
+
+			if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+				return;
+
+			log_create_alter_drop(classId, objectId);
+		}
+		break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pgaudit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pgaudit_ProcessUtility_hook(Node *parsetree,
+							const char *queryString,
+							ProcessUtilityContext context,
+							ParamListInfo params,
+							DestReceiver *dest,
+							char *completionTag)
+{
+	/* Create the utility audit event. */
+	utilityCommandLogged = false;
+	utilityCommandInProgress = true;
+
+	utilityAuditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+	utilityAuditEvent.commandTag = nodeTag(parsetree);
+	utilityAuditEvent.command = CreateCommandTag(parsetree);
+	utilityAuditEvent.objectName = "";
+	utilityAuditEvent.objectType = "";
+	utilityAuditEvent.commandText = debug_query_string;
+	utilityAuditEvent.granted = false;
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/* Log the utility command if logging is on, the command has not already
+	 * been logged by another hook, and the transaction is not aborted */
+	if (auditLogBitmap != 0 && !utilityCommandLogged &&
+		!IsAbortedTransactionBlockState())
+	{
+		log_audit_event(&utilityAuditEvent);
+	}
+
+	utilityCommandInProgress = false;
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pgaudit_object_access_hook(ObjectAccessType access,
+						   Oid classId,
+						   Oid objectId,
+						   int subId,
+						   void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState())
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pgaudit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then then token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pgaudit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pgaudit_log from extra (ignoring newval, which has already been converted
+ * to a bitmap above). Note that extra may not be set if the assignment is to be
+ * suppressed.
+ */
+static void
+assign_pgaudit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * This variable defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * This variables controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pgaudit_log,
+							   assign_pgaudit_log,
+							   NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pgaudit_ExecutorCheckPerms_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pgaudit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pgaudit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..0b39082
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pgaudit'
+relocatable = true
diff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.pl
new file mode 100755
index 0000000..dcb9fda
--- /dev/null
+++ b/contrib/pg_audit/test/test.pl
@@ -0,0 +1,1243 @@
+#!/usr/bin/perl
+################################################################################
+# test.pl - pg_audit Unit Tests
+################################################################################
+
+################################################################################
+# Perl includes
+################################################################################
+use strict;
+use warnings;
+use Carp;
+
+use Getopt::Long;
+use Pod::Usage;
+use DBI;
+use Cwd qw(abs_path);
+use IPC::System::Simple qw(capture);
+
+################################################################################
+# Constants
+################################################################################
+use constant
+{
+	true  => 1,
+	false => 0
+};
+
+use constant
+{
+	CONTEXT_GLOBAL   => 'GLOBAL',
+	CONTEXT_DATABASE => 'DATABASE',
+	CONTEXT_ROLE	 => 'ROLE'
+};
+
+use constant
+{
+	CLASS			=> 'CLASS',
+
+	CLASS_DDL		=> 'DDL',
+	CLASS_FUNCTION	=> 'FUNCTION',
+	CLASS_MISC		=> 'MISC',
+	CLASS_READ		=> 'READ',
+	CLASS_WRITE		=> 'WRITE',
+
+	CLASS_ALL		=> 'ALL',
+	CLASS_NONE		=> 'NONE'
+};
+
+use constant
+{
+	COMMAND						=> 'COMMAND',
+	COMMAND_LOG					=> 'COMMAND_LOG',
+
+	COMMAND_ANALYZE				=> 'ANALYZE',
+	COMMAND_ALTER_AGGREGATE		=> 'ALTER AGGREGATE',
+	COMMAND_ALTER_COLLATION		=> 'ALTER COLLATION',
+	COMMAND_ALTER_CONVERSION	=> 'ALTER CONVERSION',
+	COMMAND_ALTER_DATABASE		=> 'ALTER DATABASE',
+	COMMAND_ALTER_ROLE			=> 'ALTER ROLE',
+	COMMAND_ALTER_ROLE_SET		=> 'ALTER ROLE SET',
+	COMMAND_ALTER_TABLE			=> 'ALTER TABLE',
+	COMMAND_ALTER_TABLE_INDEX	=> 'ALTER TABLE INDEX',
+	COMMAND_BEGIN				=> 'BEGIN',
+	COMMAND_CLOSE				=> 'CLOSE CURSOR',
+	COMMAND_COMMIT				=> 'COMMIT',
+	COMMAND_COPY				=> 'COPY',
+	COMMAND_COPY_TO				=> 'COPY TO',
+	COMMAND_COPY_FROM			=> 'COPY FROM',
+	COMMAND_CREATE_AGGREGATE	=> 'CREATE AGGREGATE',
+	COMMAND_CREATE_COLLATION	=> 'CREATE COLLATION',
+	COMMAND_CREATE_CONVERSION	=> 'CREATE CONVERSION',
+	COMMAND_CREATE_DATABASE		=> 'CREATE DATABASE',
+	COMMAND_CREATE_INDEX		=> 'CREATE INDEX',
+	COMMAND_DEALLOCATE			=> 'DEALLOCATE',
+	COMMAND_DECLARE_CURSOR		=> 'DECLARE CURSOR',
+	COMMAND_DO					=> 'DO',
+	COMMAND_DISCARD_ALL			=> 'DISCARD ALL',
+	COMMAND_CREATE_FUNCTION		=> 'CREATE FUNCTION',
+	COMMAND_CREATE_ROLE			=> 'CREATE ROLE',
+	COMMAND_CREATE_SCHEMA		=> 'CREATE SCHEMA',
+	COMMAND_CREATE_TABLE		=> 'CREATE TABLE',
+	COMMAND_CREATE_TABLE_AS		=> 'CREATE TABLE AS',
+	COMMAND_DROP_DATABASE		=> 'DROP DATABASE',
+	COMMAND_DROP_SCHEMA			=> 'DROP SCHEMA',
+	COMMAND_DROP_TABLE			=> 'DROP TABLE',
+	COMMAND_DROP_TABLE_INDEX	=> 'DROP TABLE INDEX',
+	COMMAND_DROP_TABLE_TYPE		=> 'DROP TABLE TYPE',
+	COMMAND_EXECUTE				=> 'EXECUTE',
+	COMMAND_EXECUTE_READ		=> 'EXECUTE READ',
+	COMMAND_EXECUTE_WRITE		=> 'EXECUTE WRITE',
+	COMMAND_EXECUTE_FUNCTION	=> 'EXECUTE FUNCTION',
+	COMMAND_EXPLAIN				=> 'EXPLAIN',
+	COMMAND_FETCH				=> 'FETCH',
+	COMMAND_GRANT				=> 'GRANT',
+	COMMAND_INSERT				=> 'INSERT',
+	COMMAND_PREPARE				=> 'PREPARE',
+	COMMAND_PREPARE_READ		=> 'PREPARE READ',
+	COMMAND_PREPARE_WRITE		=> 'PREPARE WRITE',
+	COMMAND_REVOKE				=> 'REVOKE',
+	COMMAND_SELECT				=> 'SELECT',
+	COMMAND_SET					=> 'SET',
+	COMMAND_UPDATE				=> 'UPDATE'
+};
+
+use constant
+{
+	TYPE			=> 'TYPE',
+	TYPE_NONE		=> '',
+
+	TYPE_FUNCTION	=> 'FUNCTION',
+	TYPE_INDEX		=> 'INDEX',
+	TYPE_TABLE		=> 'TABLE',
+	TYPE_TYPE		=> 'TYPE'
+};
+
+use constant
+{
+	NAME			=> 'NAME'
+};
+
+################################################################################
+# Command line parameters
+################################################################################
+my $strPgSqlBin = '../../../../bin/bin';	# Path of PG binaries to use for
+											# this test
+my $strTestPath = '../../../../data';		# Path where testing will occur
+my $iDefaultPort = 6000;					# Default port to run Postgres on
+my $bHelp = false;							# Display help
+my $bQuiet = false;							# Supress output except for errors
+my $bNoCleanup = false;						# Cleanup database on exit
+
+GetOptions ('q|quiet' => \$bQuiet,
+			'no-cleanup' => \$bNoCleanup,
+			'help' => \$bHelp,
+			'pgsql-bin=s' => \$strPgSqlBin,
+			'test-path=s' => \$strTestPath)
+	or pod2usage(2);
+
+# Display version and exit if requested
+if ($bHelp)
+{
+	print 'pg_audit unit test\n\n';
+	pod2usage();
+
+	exit 0;
+}
+
+################################################################################
+# Global variables
+################################################################################
+my $hDb;					# Connection to Postgres
+my $strLogExpected = '';	# The expected log compared with grepping AUDIT
+							# entries from the postgres log.
+
+my $strDatabase = 'postgres';	# Connected database (modified by PgSetDatabase)
+my $strUser = 'postgres';		# Connected user (modified by PgSetUser)
+my $strAuditRole = 'audit';		# Role to use for auditing
+
+my %oAuditLogHash;				# Hash to store pg_audit.log GUCS
+my %oAuditGrantHash;			# Hash to store pg_audit grants
+
+my $strCurrentAuditLog;		# pg_audit.log setting was Postgres was started with
+my $strTemporaryAuditLog;	# pg_audit.log setting that was set hot
+
+################################################################################
+# Stores the mapping between commands, classes, and types
+################################################################################
+my %oCommandHash =
+(&COMMAND_ANALYZE => {
+	&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE_SET => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_ALTER_ROLE},
+	&COMMAND_ALTER_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_ALTER_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_BEGIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CLOSE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COMMIT => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COPY_FROM => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_COPY_TO => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_CREATE_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX},
+	&COMMAND_DEALLOCATE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_DECLARE_CURSOR => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_DO => {&CLASS => &CLASS_FUNCTION, &TYPE => &TYPE_NONE},
+	&COMMAND_DISCARD_ALL => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_FUNCTION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_CREATE_TABLE_AS => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TYPE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TYPE,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_EXECUTE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_FUNCTION => {&CLASS => &CLASS_FUNCTION,
+		&TYPE => &TYPE_FUNCTION, &COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXPLAIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_FETCH => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_GRANT => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_PREPARE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_PREPARE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_INSERT => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE},
+	&COMMAND_REVOKE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_SELECT => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_SET => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_UPDATE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE}
+);
+
+################################################################################
+# CommandExecute
+################################################################################
+sub CommandExecute
+{
+	my $strCommand = shift;
+	my $bSuppressError = shift;
+
+	# Set default
+	$bSuppressError = defined($bSuppressError) ? $bSuppressError : false;
+
+	# Run the command
+	my $iResult = system($strCommand);
+
+	if ($iResult != 0 && !$bSuppressError)
+	{
+		confess "command '${strCommand}' failed with error ${iResult}";
+	}
+}
+
+################################################################################
+# log
+################################################################################
+sub log
+{
+	my $strMessage = shift;
+	my $bError = shift;
+
+	# Set default
+	$bError = defined($bError) ? $bError : false;
+
+	if (!$bQuiet)
+	{
+		print "${strMessage}\n";
+	}
+
+	if ($bError)
+	{
+		exit 1;
+	}
+}
+
+################################################################################
+# ArrayToString
+################################################################################
+sub ArrayToString
+{
+	my @stryArray = @_;
+
+	my $strResult = '';
+
+	for (my $iIndex = 0; $iIndex < @stryArray; $iIndex++)
+	{
+		if ($iIndex != 0)
+		{
+			$strResult .= ', ';
+		}
+
+		$strResult .= $stryArray[$iIndex];
+	}
+
+	return $strResult;
+}
+
+################################################################################
+# BuildModule
+################################################################################
+sub BuildModule
+{
+	capture('cd ..;make');
+	CommandExecute("cp ../pg_audit.so" .
+	               " ${strPgSqlBin}/../lib/postgresql");
+	CommandExecute("cp ../pg_audit.control" .
+	               " ${strPgSqlBin}/../share/postgresql/extension");
+	CommandExecute("cp ../pg_audit--1.0.0.sql" .
+	               " ${strPgSqlBin}/../share/postgresql/extension");
+}
+
+################################################################################
+# PgConnect
+################################################################################
+sub PgConnect
+{
+	my $iPort = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+
+	# Log Connection
+	&log("   DB: connect user ${strUser}, database ${strDatabase}");
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# Connect to the db
+	$hDb = DBI->connect("dbi:Pg:dbname=${strDatabase};port=${iPort};host=/tmp",
+						$strUser, undef,
+						{AutoCommit => 1, RaiseError => 1});
+}
+
+################################################################################
+# PgDisconnect
+################################################################################
+sub PgDisconnect
+{
+	# Connect to the db (whether it is local or remote)
+	if (defined($hDb))
+	{
+		$hDb->disconnect;
+		undef($hDb);
+	}
+}
+
+################################################################################
+# PgExecute
+################################################################################
+sub PgExecute
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	my $hStatement = $hDb->prepare($strSql);
+
+	$hStatement->execute();
+	$hStatement->finish();
+}
+
+################################################################################
+# PgExecuteOnly
+################################################################################
+sub PgExecuteOnly
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	$hDb->do($strSql);
+}
+
+################################################################################
+# PgSetDatabase
+################################################################################
+sub PgSetDatabase
+{
+	my $strDatabaseParam = shift;
+
+	# Stop and start the database to reset pgconf entries
+	PgStop();
+	PgStart();
+
+	# Execute the statement
+	$strDatabase = $strDatabaseParam;
+	PgConnect();
+}
+
+################################################################################
+# PgSetUser
+################################################################################
+sub PgSetUser
+{
+	my $strUserParam = shift;
+
+	$strUser = $strUserParam;
+
+	# Stop and start the database to reset pgconf entries
+	if ((defined($strTemporaryAuditLog) && !defined($strCurrentAuditLog)) ||
+		(defined($strCurrentAuditLog) && !defined($strTemporaryAuditLog)) ||
+		$strCurrentAuditLog ne $strTemporaryAuditLog)
+	{
+		$strCurrentAuditLog = $strTemporaryAuditLog;
+
+		PgStop();
+		PgStart();
+	}
+	else
+	{
+		# Execute the statement
+		PgConnect();
+	}
+}
+
+################################################################################
+# SaveString
+################################################################################
+sub SaveString
+{
+	my $strFile = shift;
+	my $strString = shift;
+
+	# Open the file for writing
+	my $hFile;
+
+	open($hFile, '>', $strFile)
+		or confess "unable to open ${strFile}";
+
+	if ($strString ne '')
+	{
+		syswrite($hFile, $strString)
+			or confess "unable to write to ${strFile}: $!";
+	}
+
+	close($hFile);
+}
+
+################################################################################
+# PgLogExecute
+################################################################################
+sub PgLogExecute
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+	my $bExecute = shift;
+	my $bWait = shift;
+	my $bLogSql = shift;
+
+	# Set defaults
+	$bExecute = defined($bExecute) ? $bExecute : true;
+	$bWait = defined($bWait) ? $bWait : true;
+	$bLogSql = defined($bLogSql) ? $bLogSql : true;
+
+	if ($bExecute)
+	{
+		PgExecuteOnly($strSql);
+	}
+
+	PgLogExpect($strCommand, $bLogSql ? $strSql : '', $oData);
+
+	if ($bWait)
+	{
+		PgLogWait();
+	}
+}
+
+################################################################################
+# PgLogExpect
+################################################################################
+sub PgLogExpect
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+
+	# If oData is false then no logging
+	if (defined($oData) && ref($oData) eq '' && !$oData)
+	{
+		return;
+	}
+
+	# Log based on session
+	if (PgShouldLog($strCommand))
+	{
+		# Make sure class is defined
+		my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+		if (!defined($strClass))
+		{
+			confess "class is not defined for command ${strCommand}";
+		}
+
+		# Make sure object type is defined
+		my $strObjectType = $oCommandHash{$strCommand}{&TYPE};
+
+		if (!defined($strObjectType))
+		{
+			confess "object type is not defined for command ${strCommand}";
+		}
+
+		# Check for command override
+		my $strCommandLog = $strCommand;
+
+		if ($oCommandHash{$strCommand}{&COMMAND})
+		{
+			$strCommandLog = $oCommandHash{$strCommand}{&COMMAND};
+		}
+
+		my $strObjectName = '';
+
+		if (defined($oData) && ref($oData) ne 'ARRAY')
+		{
+			$strObjectName = $oData;
+		}
+
+		my $strLog .= "SESSION,${strClass},${strCommandLog}," .
+					  "${strObjectType},${strObjectName},${strSql}";
+		&log("AUDIT: ${strLog}");
+
+		$strLogExpected .= "${strLog}\n";
+	}
+
+	# Log based on grants
+	if (ref($oData) eq 'ARRAY' && ($strCommand eq COMMAND_SELECT ||
+		$oCommandHash{$strCommand}{&CLASS} eq CLASS_WRITE))
+	{
+		foreach my $oTableHash (@{$oData})
+		{
+			my $strObjectName = ${$oTableHash}{&NAME};
+			my $strCommandLog = ${$oTableHash}{&COMMAND};
+
+			if (defined($oAuditGrantHash{$strAuditRole}
+										{$strObjectName}{$strCommandLog}))
+			{
+				my $strCommandLog = defined(${$oTableHash}{&COMMAND_LOG}) ?
+					${$oTableHash}{&COMMAND_LOG} : $strCommandLog;
+				my $strClass = $oCommandHash{$strCommandLog}{&CLASS};
+				my $strObjectType = ${$oTableHash}{&TYPE};
+
+				my $strLog .= "OBJECT,${strClass},${strCommandLog}," .
+							  "${strObjectType},${strObjectName},${strSql}";
+				&log("AUDIT: ${strLog}");
+
+				$strLogExpected .= "${strLog}\n";
+			}
+		}
+
+		$oData = undef;
+	}
+}
+
+################################################################################
+# PgShouldLog
+################################################################################
+sub PgShouldLog
+{
+	my $strCommand = shift;
+
+	# Make sure class is defined
+	my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+	if (!defined($strClass))
+	{
+		confess "class is not defined for command ${strCommand}";
+	}
+
+	# Check logging for the role
+	my $bLog = undef;
+
+	if (defined($oAuditLogHash{&CONTEXT_ROLE}{$strUser}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_ROLE}{$strUser}{$strClass};
+	}
+
+	# Else check logging for the db
+	elsif (defined($oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}{$strClass};
+	}
+
+	# Else check logging for global
+	elsif (defined($oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}{$strClass};
+	}
+
+	return defined($bLog) ? true : false;
+}
+
+################################################################################
+# PgLogWait
+################################################################################
+sub PgLogWait
+{
+	my $strLogActual;
+
+	# Run in an eval block since grep returns 1 when nothing was found
+	eval
+	{
+		$strLogActual = capture("grep 'LOG:  AUDIT: '" .
+								" ${strTestPath}/postgresql.log");
+	};
+
+	# If an error was returned, continue if it was 1, otherwise confess
+	if ($@)
+	{
+		my $iExitStatus = $? >> 8;
+
+		if ($iExitStatus != 1)
+		{
+			confess "grep returned ${iExitStatus}";
+		}
+
+		$strLogActual = '';
+	}
+
+	# Strip the AUDIT and timestamp from the actual log
+	$strLogActual =~ s/prefix LOG:  AUDIT\: //g;
+
+	# Save the logs
+	SaveString("${strTestPath}/audit.actual", $strLogActual);
+	SaveString("${strTestPath}/audit.expected", $strLogExpected);
+
+	CommandExecute("diff ${strTestPath}/audit.expected" .
+				   " ${strTestPath}/audit.actual");
+}
+
+################################################################################
+# PgDrop
+################################################################################
+sub PgDrop
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Stop the cluster
+	PgStop(true, $strPath);
+
+	# Remove the directory
+	CommandExecute("rm -rf ${strTestPath}");
+}
+
+################################################################################
+# PgCreate
+################################################################################
+sub PgCreate
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	CommandExecute("${strPgSqlBin}/initdb -D ${strPath} -U ${strUser}" .
+				   ' -A trust > /dev/null');
+}
+
+################################################################################
+# PgStop
+################################################################################
+sub PgStop
+{
+	my $bImmediate = shift;
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+	$bImmediate = defined($bImmediate) ? $bImmediate : false;
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# If postmaster process is running then stop the cluster
+	if (-e $strPath . '/postmaster.pid')
+	{
+		CommandExecute("${strPgSqlBin}/pg_ctl stop -D ${strPath} -w -s -m " .
+					  ($bImmediate ? 'immediate' : 'fast'));
+	}
+}
+
+################################################################################
+# PgStart
+################################################################################
+sub PgStart
+{
+	my $iPort = shift;
+	my $strPath = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Make sure postgres is not running
+	if (-e $strPath . '/postmaster.pid')
+	{
+		confess "${strPath}/postmaster.pid exists, cannot start";
+	}
+
+	# Start the cluster
+	CommandExecute("${strPgSqlBin}/pg_ctl start -o \"" .
+				   "-c port=${iPort}" .
+				   " -c unix_socket_directories='/tmp'" .
+				   " -c shared_preload_libraries='pg_audit'" .
+				   " -c log_min_messages=debug1" .
+				   " -c log_line_prefix='prefix '" .
+				   # " -c log_destination='stderr,csvlog'" .
+				   # " -c logging_collector=on" .
+				   (defined($strCurrentAuditLog) ?
+					   " -c pg_audit.log='${strCurrentAuditLog}'" : '') .
+				   " -c pg_audit.role='${strAuditRole}'" .
+				   " -c log_connections=on" .
+				   "\" -D ${strPath} -l ${strPath}/postgresql.log -w -s");
+
+	# Connect user session
+	PgConnect();
+}
+
+################################################################################
+# PgAuditLogSet
+################################################################################
+sub PgAuditLogSet
+{
+	my $strContext = shift;
+	my $strName = shift;
+	my @stryClass = @_;
+
+	# Create SQL to set the GUC
+	my $strCommand;
+	my $strSql;
+
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		$strCommand = COMMAND_SET;
+		$strSql = "set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+		$strTemporaryAuditLog = ArrayToString(@stryClass);
+	}
+	elsif ($strContext eq CONTEXT_ROLE)
+	{
+		$strCommand = COMMAND_ALTER_ROLE_SET;
+		$strSql = "alter role ${strName} set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+	}
+	else
+	{
+		confess "unable to set pg_audit.log for context ${strContext}";
+	}
+
+	# Reset the audit log
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		delete($oAuditLogHash{$strContext});
+		$strName = CONTEXT_GLOBAL;
+	}
+	else
+	{
+		delete($oAuditLogHash{$strContext}{$strName});
+	}
+
+	# Store all the classes in the hash and build the GUC
+	foreach my $strClass (@stryClass)
+	{
+		if ($strClass eq CLASS_ALL)
+		{
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_DDL} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_FUNCTION} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_MISC} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_READ} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_WRITE} = true;
+		}
+
+		if (index($strClass, '-') == 0)
+		{
+			$strClass = substr($strClass, 1);
+
+			delete($oAuditLogHash{$strContext}{$strName}{$strClass});
+		}
+		else
+		{
+			$oAuditLogHash{$strContext}{$strName}{$strClass} = true;
+		}
+	}
+
+	PgLogExecute($strCommand, $strSql);
+}
+
+################################################################################
+# PgAuditGrantSet
+################################################################################
+sub PgAuditGrantSet
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_GRANT, "grant " . lc(${strPrivilege}) .
+								(defined($strColumn) ? " (${strColumn})" : '') .
+								" on ${strObject} to ${strRole}");
+
+	$oAuditGrantHash{$strRole}{$strObject}{$strPrivilege} = true;
+}
+
+################################################################################
+# PgAuditGrantReset
+################################################################################
+sub PgAuditGrantReset
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_REVOKE, "revoke " . lc(${strPrivilege}) .
+				 (defined($strColumn) ? " (${strColumn})" : '') .
+				 " on ${strObject} from ${strRole}");
+
+	delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege});
+}
+
+################################################################################
+# Main
+################################################################################
+my @oyTable; # Store table info for select, insert, update, delete
+
+# Drop the old cluster, build the code, and create a new cluster
+PgDrop();
+BuildModule();
+PgCreate();
+PgStart();
+
+PgExecute("create extension pg_audit");
+
+# Create test users and the audit role
+PgExecute("create user user1");
+PgExecute("create user user2");
+PgExecute("create role ${strAuditRole}");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL));
+
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_READ, CLASS_WRITE));
+
+# User1 follows the global log settings
+PgSetUser('user1');
+PgLogExecute(COMMAND_CREATE_TABLE, 'create table test (id int)', 'public.test');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+PgLogExecute(COMMAND_DROP_TABLE, 'drop table test', 'public.test');
+
+PgSetUser('user2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+             'create table test2 (id int)', 'public.test2');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+             'create table test3 (id int)', 'public.test2');
+
+# Catalog select should not log
+PgLogExecute(COMMAND_SELECT, 'select * from pg_class limit 1',
+							   false);
+
+# Multi-table select
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test3, test2',
+							   \@oyTable);
+
+# Various CTE combinations
+PgAuditGrantSet($strAuditRole, &COMMAND_INSERT, 'public.test3');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (select id from test2)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (insert into test3 values (1) returning id)' .
+			 ' insert into test2 select id from cte',
+			 \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE, 'public.test2');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_INSERT,
+             'with cte as (update test2 set id = 1 returning id)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_INSERT});
+PgLogExecute(COMMAND_UPDATE,
+			 'with cte as (insert into test2 values (1) returning id)' .
+			 ' update test3 set id = cte.id' .
+			 ' from cte where test3.id <> cte.id',
+			 \@oyTable);
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_NONE));
+PgSetUser('user2');
+
+# Column-based audits
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test4 (id int, name text)', 'public.test4');
+PgAuditGrantSet($strAuditRole, COMMAND_SELECT, 'public.test4', 'name');
+PgAuditGrantSet($strAuditRole, COMMAND_UPDATE, 'public.test4', 'id');
+PgAuditGrantSet($strAuditRole, COMMAND_INSERT, 'public.test4', 'name');
+
+# Select
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id from public.test4',
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from public.test4',
+							  \@oyTable);
+
+# Insert
+@oyTable = ();
+PgLogExecute(COMMAND_INSERT, 'insert into public.test4 (id) values (1)',
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT, "insert into public.test4 (name) values ('test')",
+							  \@oyTable);
+
+# Update
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set name = 'foo'",
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set id = 1",
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+            &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update public.test4 set name = 'foo' where name = 'bar'",
+			 \@oyTable);
+
+# Drop test tables
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test2", 'public.test2');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test3", 'public.test3');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test4", 'public.test4');
+
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Now create some email friendly tests.  These first tests are session logging
+# only.
+PgSetUser('postgres');
+
+&log("\nExamples:");
+
+&log("\nSession Audit:\n");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL, CLASS_READ));
+
+# !!! Trying to build test to exclude function calls (did not work)
+# PgLogExecute(COMMAND_CREATE_ROLE, 'create role func_owner');
+# PgAuditLogSet(CONTEXT_ROLE, 'func_owner');
+# PgLogExecute(COMMAND_SET, 'set role func_owner');
+# PgLogExecute(COMMAND_CREATE_FUNCTION, 'CREATE FUNCTION func_test(a int)' .
+# 									  ' returns int as $$ begin return a + 1;' .
+# 									  ' end $$language plpgsql security definer');
+# PgLogExecute(COMMAND_GRANT, 'grant execute on function func_test(int) to user1');
+
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account (id int, name text, password text,' .
+			 ' description text)', 'public.account');
+PgLogExecute(COMMAND_SELECT,
+			 'select * from account');
+PgLogExecute(COMMAND_INSERT,
+			 "insert into account (id, name, password, description)" .
+			 " values (1, 'user1', 'HASH1', 'blah, blah')");
+&log("AUDIT: <nothing logged>");
+
+# Test function without auditing (did not work)
+# PgLogExecute(COMMAND_SELECT, 'select func_test(1)');
+
+# Now tests for object logging
+&log("\nObject Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_NONE));
+PgExecute("set pg_audit.role = 'audit'");
+PgSetUser('user1');
+
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account', 'password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id, name from account',
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE,
+                'public.account', 'name, password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Now tests for session/object logging
+&log("\nSession/Object Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user1', (CLASS_READ, CLASS_WRITE));
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account_role_map (account_id int, role_id int)',
+			 'public.account_role_map');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account_role_map');
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.account_role_map', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT,
+			 'select account.password, account_role_map.role_id from account' .
+			 ' inner join account_role_map' .
+			 ' on account.id = account_role_map.account_id',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update account set description = 'yada, yada'" .
+			 " where password = 'HASH2'",
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Test all sql commands
+&log("\nExhaustive Command Tests:\n");
+
+PgSetUser('postgres');
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_ALL));
+PgLogExecute(COMMAND_SET, "set pg_audit.role = 'audit'");
+
+PgLogExecute(COMMAND_DO, "do \$\$\ begin raise notice 'test'; end; \$\$;");
+PgLogExecute(COMMAND_CREATE_SCHEMA, "create schema test");
+
+# Test COPY
+PgLogExecute(COMMAND_COPY_TO,
+			 "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'");
+PgLogExecute(COMMAND_CREATE_TABLE_AS,
+			 "CREATE TABLE test.pg_class as select * from pg_class",
+			 'test.pg_class', true, false);
+PgLogExecute(COMMAND_INSERT,
+			 "CREATE TABLE test.pg_class as select * from pg_class",
+			 undef, false, true);
+PgLogExecute(COMMAND_INSERT,
+			 "COPY test.pg_class from '" . abs_path($strTestPath) .
+			 "/class.out'", undef, true, false);
+PgLogExecute(COMMAND_COPY_FROM,
+			 "COPY test.pg_class from '" . abs_path($strTestPath) .
+			 "/class.out'", undef, false, true);
+
+# Test prepared SELECT
+PgLogExecute(COMMAND_PREPARE_READ,
+			 'PREPARE pgclassstmt (oid) as select *' .
+			 ' from pg_class where oid = $1');
+PgLogExecute(COMMAND_EXECUTE_READ,
+			 'EXECUTE pgclassstmt (1)');
+PgLogExecute(COMMAND_DEALLOCATE,
+			 'DEALLOCATE pgclassstmt');
+
+# Test cursor
+PgLogExecute(COMMAND_BEGIN,
+			 'BEGIN');
+PgLogExecute(COMMAND_DECLARE_CURSOR,
+		     'DECLARE ctest SCROLL CURSOR FOR SELECT * FROM pg_class');
+PgLogExecute(COMMAND_FETCH,
+			 'FETCH NEXT FROM ctest');
+PgLogExecute(COMMAND_CLOSE,
+			 'CLOSE ctest');
+PgLogExecute(COMMAND_COMMIT,
+			 'COMMIT');
+
+# Test prepared INSERT
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test.test_insert (id int)', 'test.test_insert');
+PgLogExecute(COMMAND_PREPARE_WRITE,
+			 'PREPARE pgclassstmt (oid) as insert' .
+			 ' into test.test_insert (id) values ($1)');
+PgLogExecute(COMMAND_INSERT,
+			 'EXECUTE pgclassstmt (1)', undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_WRITE,
+			 'EXECUTE pgclassstmt (1)', undef, false, true);
+
+# Create a table with a primary key
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test (id int primary key, name text,' .
+			 'description text)',
+			 'public.test', true, false);
+PgLogExecute(COMMAND_CREATE_INDEX,
+			 'create table test (id int primary key, name text,' .
+			 'description text)',
+			 'public.test_pkey', false, true);
+PgLogExecute(COMMAND_ANALYZE, 'analyze test');
+
+# Grant select to public - this should have no affect on auditing
+PgLogExecute(COMMAND_GRANT, 'grant select on public.test to public');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+# Now grant select to audit and it should be logged
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test', \@oyTable);
+
+# Check columns granted to public and make sure they do not log
+PgAuditGrantReset($strAuditRole, &COMMAND_SELECT, 'public.test');
+PgLogExecute(COMMAND_GRANT, 'grant select (name) on public.test to public');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+PgLogExecute(COMMAND_SELECT, 'select from test');
+
+# Try a select that does not reference any tables
+PgLogExecute(COMMAND_SELECT, 'select 1, current_timestamp');
+
+# Try explain
+PgLogExecute(COMMAND_EXPLAIN, 'explain select 1');
+
+# Now set grant to a specific column to audit and make sure it logs
+# Make sure the the converse is true
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test',
+				'name, description');
+PgLogExecute(COMMAND_SELECT, 'select id from test');
+
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from test', \@oyTable);
+
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test drop description', 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable);
+
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test rename to test2', 'public.test');
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test2 set schema test', 'public.test2', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE_INDEX, 'alter table test2 set schema test',
+										'public.test_pkey', false, true);
+PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 add description text',
+								  'test.test2');
+PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 drop description',
+								  'test.test2');
+PgLogExecute(COMMAND_DROP_TABLE_INDEX, 'drop table test.test2',
+									   'test.test_pkey', false, false);
+PgLogExecute(COMMAND_DROP_TABLE, 'drop table test.test2',
+								 'test.test2', true, true);
+
+PgLogExecute(COMMAND_CREATE_FUNCTION, 'CREATE FUNCTION int_add(a int, b int)' .
+									  ' returns int as $$ begin return a + b;' .
+									  ' end $$language plpgsql');
+PgLogExecute(COMMAND_SELECT, "select int_add(1, 1)",
+							 undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_FUNCTION, "select int_add(1, 1)",
+									   'public.int_add', false, true);
+
+PgLogExecute(COMMAND_CREATE_AGGREGATE, "CREATE AGGREGATE sum_test (int)" .
+							" (sfunc = int_add, stype = int, initcond = 0)");
+PgLogExecute(COMMAND_ALTER_AGGREGATE,
+			 "ALTER AGGREGATE sum_test (int) rename to sum_test2");
+
+PgLogExecute(COMMAND_CREATE_COLLATION,
+			 "CREATE COLLATION collation_test FROM \"de_DE\"");
+PgLogExecute(COMMAND_ALTER_COLLATION,
+			 "ALTER COLLATION collation_test rename to collation_test2");
+
+PgLogExecute(COMMAND_CREATE_CONVERSION,
+			 "CREATE CONVERSION conversion_test FOR 'SQL_ASCII' TO".
+			 " 'MULE_INTERNAL' FROM ascii_to_mic");
+PgLogExecute(COMMAND_ALTER_CONVERSION,
+			 "ALTER CONVERSION conversion_test rename to conversion_test2");
+
+PgLogExecute(COMMAND_CREATE_DATABASE, "CREATE DATABASE database_test");
+PgLogExecute(COMMAND_ALTER_DATABASE,
+			 "ALTER DATABASE database_test rename to database_test2");
+PgLogExecute(COMMAND_DROP_DATABASE, "DROP DATABASE database_test2");
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Stop the database
+if (!$bNoCleanup)
+{
+	PgDrop();
+}
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index a698d0f..5b247a9 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index f03b72a..e4f0bdc 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -124,6 +124,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbench         SYSTEM "pgbench.sgml">
 <!ENTITY pgarchivecleanup SYSTEM "pgarchivecleanup.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..78565de
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,316 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pg_audit" xreflabel="pg_audit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pg_audit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> module provides session and object
+    auditing via the standard logging facility.  Session and object auditing are
+    completely independent and can be combined.
+  </para>
+
+  <sect2>
+    <title>Session Auditing</title>
+
+    <para>
+      Session auditing allows the logging of all commands that are executed by
+      a user in the backend.  Each command is logged with a single entry and
+      includes the audit type (e.g. <literal>SESSION</literal>), command type
+      (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and
+      statement (e.g. <literal>"select * from test"</literal>).
+
+      Fully-qualified names and object types will be logged for
+      <literal>CREATE</literal>, <literal>UPDATE</literal>, and
+      <literal>DROP</literal> commands on <literal>TABLE</literal>,
+      <literal>MATVIEW</literal>, <literal>VIEW</literal>,
+      <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,
+      <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and
+      <literal>SEQUENCE</literal> objects as well as function calls.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is controlled by the <literal>pg_audit.log</literal>
+        GUC. There are five classes of commands that are recognized:
+
+        <itemizedlist>
+          <listitem>
+            <para>
+              <literal>READ</literal> - <literal>SELECT</literal> and
+              <literal>COPY</literal> when the source is a table or query.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>WRITE</literal> - <literal>INSERT</literal>,
+              <literal>UPDATE</literal>, <literal>DELETE</literal>,
+              <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+              destination is a table.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>FUNCTION</literal> - Function calls and
+              <literal>DO</literal> blocks.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>DDL</literal> - DDL, plus <literal>VACUUM</literal>,
+              <literal>REINDEX</literal>, and <literal>ANALYZE</literal>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>MISC</literal> - Miscellaneous commands, e.g.
+              <literal>DISCARD</literal>, <literal>FETCH</literal>,
+              <literal>CHECKPOINT</literal>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </para>
+
+      <para>
+        Enable session logging for all writes and DDL:
+          <programlisting>
+pg_audit.log = 'write, ddl'
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except miscellaneous:
+          <programlisting>
+pg_audit.log = 'all, -misc'
+          </programlisting>
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.log = 'read, ddl'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+      <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+select *
+    from account;
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Object Auditing</title>
+
+    <para>
+      Object auditing logs commands that affect a particular object.  Only
+      <literal>SELECT</literal>, <literal>INSERT</literal>,
+      <literal>UPDATE</literal> and <literal>DELETE</literal> commands are
+      supported.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level auditing is implemented via the roles system.  The
+        <literal>pg_audit.role</literal> GUC defines the role that will be used
+        for auditing.  An object will be audited when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.
+      </para>
+
+      <programlisting>
+postresql.conf: pg_audit.role = 'audit'
+
+grant select, delete
+   on public.account;
+      </programlisting>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.role = 'audit'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to audit;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to audit;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to audit;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is not in compliant CSV format.  If machine-readability is
+          required then consider setting
+          <literal>log_destination = 'csvlog'</literal>.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</literal> - <literal>SESSION</literal> or
+            <literal>OBJECT</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</literal> - <literal>READ</literal>,
+            <literal>WRITE</literal>, <literal>FUNCTION</literal>,
+            <literal>DDL</literal>, or <literal>MISC</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</literal> - <literal>ALTER TABLE</literal>,
+            <literal>SELECT</literal>, <literal>CREATE INDEX</literal>,
+            <literal>UPDATE</literal>, etc.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</literal> - <literal>TABLE</literal>,
+            <literal>INDEX</literal>, <literal>VIEW</literal>, etc.  Only
+            available for DML and certain DDL commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</literal> - The fully-qualified object name
+            (e.g. public.account).  Only available for DML and certain DDL
+            commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</literal> - Statement execute on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Author</title>
+
+    <para>
+      David Steele <email>david@pgmasters.net</email>
+    </para>
+  </sect2>
+</sect1>
#6David Steele
david@pgmasters.net
In reply to: David Steele (#5)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

On 2/23/15 10:59 AM, David Steele wrote:

On 2/17/15 10:34 AM, Stephen Frost wrote:

There seems to be a number of places which are 'pgaudit' and a bunch
that are 'pg_audit'. I'm guessing you were thinking 'pg_audit', but
it'd be good to clean up and make them all consistent.

Fixed, though I still left the file name as pgaudit.sgml since all but
one module follow that convention.

It turns out there are a few places where _ is not allowed. I've
reverted a few places to fix the doc build while maintaining the name as
pg_audit in the visible docs.

Perhaps I missed it, but it'd be good to point out that GUCs can be set
at various levels. I know we probably say that somewhere else, but it's
particularly relevant for this.

I added notes as suggested.

Patch v3 is attached.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v3.patchtext/plain; charset=UTF-8; name=pg_audit-v3.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index 195d447..d8e75f4 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -29,6 +29,7 @@ SUBDIRS = \
 		pageinspect	\
 		passwordcheck	\
 		pg_archivecleanup \
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..32bc6d9
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,20 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..2eee3b9
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,4 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..ead65a8
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1102 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * auditRole is the string value of the pgaudit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pgaudit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pgaudit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC		    "MISC"
+#define CLASS_READ			"READ"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* SELECT */
+	LOG_READ = (1 << 0),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 1),
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 2),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 4),
+
+	/* Function execution */
+	LOG_MISC = (1 << 5),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If an underlying command affects multiple objects multiple
+ * AuditEvents must be created to represent it.
+ */
+typedef struct
+{
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	bool granted;
+} AuditEvent;
+
+/*
+ * Set if a function below log_utility_command() has logged the event - prevents
+ * more than one function from logging when the event could be logged in
+ * multiple places.
+ */
+bool utilityCommandLogged = false;
+bool utilityCommandInProgress = false;
+AuditEvent utilityAuditEvent;
+
+/*
+ * Takes an AuditEvent and returns true or false depending on whether the event
+ * should be logged according to the pgaudit.roles/log settings. If it returns
+ * true, also fills in the name of the LogClass which it is logged under.
+ */
+static bool
+log_check(AuditEvent *e, const char **classname)
+{
+	enum LogClass class = LOG_NONE;
+
+	/* By default put everything in the MISC class. */
+	*classname = CLASS_MISC;
+	class = LOG_MISC;
+
+	/*
+	 * Look at the type of the command and decide what LogClass needs to be
+	 * enabled for the command to be logged.
+	 */
+	switch (e->logStmtLevel)
+	{
+		case LOGSTMT_MOD:
+			*classname = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		case LOGSTMT_DDL:
+			*classname = CLASS_DDL;
+			class = LOG_DDL;
+
+		case LOGSTMT_ALL:
+			switch (e->commandTag)
+			{
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					*classname = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				case T_VacuumStmt:
+				case T_ReindexStmt:
+					*classname = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				case T_DoStmt:
+					*classname = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * We log audit events under the following conditions:
+	 *
+	 * 1. If the audit role has been explicitly granted permission for
+	 *    an operation.
+	 */
+	if (e->granted)
+	{
+		return true;
+	}
+
+	/* 2. If the event belongs to a class covered by pgaudit.log. */
+	if ((auditLogBitmap & class) == class)
+	{
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * Takes an AuditEvent and, if it log_check(), writes it to the audit log. The
+ * AuditEvent is assumed to be completely filled in by the caller (unknown
+ * values must be set to "" so that they can be logged without error checking).
+ */
+static void
+log_audit_event(AuditEvent *e)
+{
+	const char *classname;
+
+	/* Check that this event should be logged. */
+	if (!log_check(e, &classname))
+		return;
+
+	/* Log via ereport(). */
+	ereport(LOG,
+			(errmsg("AUDIT: %s,%s,%s,%s,%s,%s",
+					e->granted ? AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+					classname, e->command, e->objectType, e->objectName,
+					e->commandText),
+			 errhidestmt(true)));
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+log_acl_check(Datum aclDatum, Oid auditOid, AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem    *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+log_relation_check(Oid relOid,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = log_acl_check(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+log_attribute_check(Oid relOid,
+					AttrNumber attNum,
+					Oid auditOid,
+					AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = log_acl_check(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+log_attribute_check_any(Oid relOid,
+						Oid auditOid,
+						Bitmapset *attributeSet,
+						AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (log_attribute_check(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			log_attribute_check(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for DML operations via executor permissions checks.
+ */
+static void
+log_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	AuditEvent auditEvent;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		auditEvent.logStmtLevel = LOGSTMT_MOD;
+
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEvent.commandTag = T_InsertStmt;
+			auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEvent.commandTag = T_UpdateStmt;
+			auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEvent.commandTag = T_DeleteStmt;
+			auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEvent.commandTag = T_SelectStmt;
+			auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEvent.commandTag = T_Invalid;
+			auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEvent.granted = false;
+		auditEvent.commandText = debug_query_string;
+
+		/* If this is the first rte then session log */
+		if (first)
+		{
+			auditEvent.objectName = "";
+			auditEvent.objectType = "";
+
+			log_audit_event(&auditEvent);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEvent.objectType = OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEvent.objectType = OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEvent.objectType = OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEvent.objectType = OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEvent.objectType = OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEvent.objectType = OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEvent.objectType = OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (log_relation_check(relOid, auditOid, auditPerms))
+			{
+				auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEvent.granted =
+						log_attribute_check_any(relOid, auditOid,
+												rte->selectedCols,
+												ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEvent.granted =
+							log_attribute_check_any(relOid, auditOid,
+													rte->modifiedCols,
+													auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Only do relation level logging if a grant was found. */
+		if (auditEvent.granted)
+		{
+			log_audit_event(&auditEvent);
+		}
+
+		pfree(auditEvent.objectName);
+	}
+
+	/*
+	 * If the first flag was never set to false, then rangeTabls was empty. In
+	 * this case log a session select statement.
+	 */
+	if (first && !utilityCommandInProgress)
+	{
+		auditEvent.logStmtLevel = LOGSTMT_ALL;
+		auditEvent.commandTag = T_SelectStmt;
+		auditEvent.command = COMMAND_SELECT;
+		auditEvent.granted = false;
+		auditEvent.commandText = debug_query_string;
+		auditEvent.objectName = "";
+		auditEvent.objectType = "";
+
+		log_audit_event(&auditEvent);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		utilityAuditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				utilityAuditEvent.objectType = OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				utilityAuditEvent.objectType = OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				utilityAuditEvent.objectType = OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				utilityAuditEvent.objectType = OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				utilityAuditEvent.objectType = OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+
+		/* Log the event */
+		log_audit_event(&utilityAuditEvent);
+		utilityCommandLogged = true;
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Generate the fully-qualified function name. */
+	utilityAuditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the event */
+	utilityAuditEvent.logStmtLevel = LOGSTMT_ALL;
+	utilityAuditEvent.commandTag = T_DoStmt;
+	utilityAuditEvent.command = COMMAND_EXECUTE;
+	utilityAuditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	utilityAuditEvent.commandText = debug_query_string;
+
+	log_audit_event(&utilityAuditEvent);
+	utilityCommandLogged = true;
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute. */
+		case OAT_FUNCTION_EXECUTE:
+			log_function_execute(objectId);
+			break;
+
+		/* Log create. */
+		case OAT_POST_CREATE:
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter. */
+		case OAT_POST_ALTER:
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop. */
+		case OAT_DROP:
+		{
+			ObjectAccessDrop *drop = arg;
+
+			if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+				return;
+
+			log_create_alter_drop(classId, objectId);
+		}
+		break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pgaudit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pgaudit_ProcessUtility_hook(Node *parsetree,
+							const char *queryString,
+							ProcessUtilityContext context,
+							ParamListInfo params,
+							DestReceiver *dest,
+							char *completionTag)
+{
+	/* Create the utility audit event. */
+	utilityCommandLogged = false;
+	utilityCommandInProgress = true;
+
+	utilityAuditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+	utilityAuditEvent.commandTag = nodeTag(parsetree);
+	utilityAuditEvent.command = CreateCommandTag(parsetree);
+	utilityAuditEvent.objectName = "";
+	utilityAuditEvent.objectType = "";
+	utilityAuditEvent.commandText = debug_query_string;
+	utilityAuditEvent.granted = false;
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/* Log the utility command if logging is on, the command has not already
+	 * been logged by another hook, and the transaction is not aborted */
+	if (auditLogBitmap != 0 && !utilityCommandLogged &&
+		!IsAbortedTransactionBlockState())
+	{
+		log_audit_event(&utilityAuditEvent);
+	}
+
+	utilityCommandInProgress = false;
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pgaudit_object_access_hook(ObjectAccessType access,
+						   Oid classId,
+						   Oid objectId,
+						   int subId,
+						   void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState())
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pgaudit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then then token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pgaudit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pgaudit_log from extra (ignoring newval, which has already been converted
+ * to a bitmap above). Note that extra may not be set if the assignment is to be
+ * suppressed.
+ */
+static void
+assign_pgaudit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * This variable defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * This variables controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pgaudit_log,
+							   assign_pgaudit_log,
+							   NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pgaudit_ExecutorCheckPerms_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pgaudit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pgaudit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..0b39082
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pgaudit'
+relocatable = true
diff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.pl
new file mode 100755
index 0000000..dcb9fda
--- /dev/null
+++ b/contrib/pg_audit/test/test.pl
@@ -0,0 +1,1243 @@
+#!/usr/bin/perl
+################################################################################
+# test.pl - pg_audit Unit Tests
+################################################################################
+
+################################################################################
+# Perl includes
+################################################################################
+use strict;
+use warnings;
+use Carp;
+
+use Getopt::Long;
+use Pod::Usage;
+use DBI;
+use Cwd qw(abs_path);
+use IPC::System::Simple qw(capture);
+
+################################################################################
+# Constants
+################################################################################
+use constant
+{
+	true  => 1,
+	false => 0
+};
+
+use constant
+{
+	CONTEXT_GLOBAL   => 'GLOBAL',
+	CONTEXT_DATABASE => 'DATABASE',
+	CONTEXT_ROLE	 => 'ROLE'
+};
+
+use constant
+{
+	CLASS			=> 'CLASS',
+
+	CLASS_DDL		=> 'DDL',
+	CLASS_FUNCTION	=> 'FUNCTION',
+	CLASS_MISC		=> 'MISC',
+	CLASS_READ		=> 'READ',
+	CLASS_WRITE		=> 'WRITE',
+
+	CLASS_ALL		=> 'ALL',
+	CLASS_NONE		=> 'NONE'
+};
+
+use constant
+{
+	COMMAND						=> 'COMMAND',
+	COMMAND_LOG					=> 'COMMAND_LOG',
+
+	COMMAND_ANALYZE				=> 'ANALYZE',
+	COMMAND_ALTER_AGGREGATE		=> 'ALTER AGGREGATE',
+	COMMAND_ALTER_COLLATION		=> 'ALTER COLLATION',
+	COMMAND_ALTER_CONVERSION	=> 'ALTER CONVERSION',
+	COMMAND_ALTER_DATABASE		=> 'ALTER DATABASE',
+	COMMAND_ALTER_ROLE			=> 'ALTER ROLE',
+	COMMAND_ALTER_ROLE_SET		=> 'ALTER ROLE SET',
+	COMMAND_ALTER_TABLE			=> 'ALTER TABLE',
+	COMMAND_ALTER_TABLE_INDEX	=> 'ALTER TABLE INDEX',
+	COMMAND_BEGIN				=> 'BEGIN',
+	COMMAND_CLOSE				=> 'CLOSE CURSOR',
+	COMMAND_COMMIT				=> 'COMMIT',
+	COMMAND_COPY				=> 'COPY',
+	COMMAND_COPY_TO				=> 'COPY TO',
+	COMMAND_COPY_FROM			=> 'COPY FROM',
+	COMMAND_CREATE_AGGREGATE	=> 'CREATE AGGREGATE',
+	COMMAND_CREATE_COLLATION	=> 'CREATE COLLATION',
+	COMMAND_CREATE_CONVERSION	=> 'CREATE CONVERSION',
+	COMMAND_CREATE_DATABASE		=> 'CREATE DATABASE',
+	COMMAND_CREATE_INDEX		=> 'CREATE INDEX',
+	COMMAND_DEALLOCATE			=> 'DEALLOCATE',
+	COMMAND_DECLARE_CURSOR		=> 'DECLARE CURSOR',
+	COMMAND_DO					=> 'DO',
+	COMMAND_DISCARD_ALL			=> 'DISCARD ALL',
+	COMMAND_CREATE_FUNCTION		=> 'CREATE FUNCTION',
+	COMMAND_CREATE_ROLE			=> 'CREATE ROLE',
+	COMMAND_CREATE_SCHEMA		=> 'CREATE SCHEMA',
+	COMMAND_CREATE_TABLE		=> 'CREATE TABLE',
+	COMMAND_CREATE_TABLE_AS		=> 'CREATE TABLE AS',
+	COMMAND_DROP_DATABASE		=> 'DROP DATABASE',
+	COMMAND_DROP_SCHEMA			=> 'DROP SCHEMA',
+	COMMAND_DROP_TABLE			=> 'DROP TABLE',
+	COMMAND_DROP_TABLE_INDEX	=> 'DROP TABLE INDEX',
+	COMMAND_DROP_TABLE_TYPE		=> 'DROP TABLE TYPE',
+	COMMAND_EXECUTE				=> 'EXECUTE',
+	COMMAND_EXECUTE_READ		=> 'EXECUTE READ',
+	COMMAND_EXECUTE_WRITE		=> 'EXECUTE WRITE',
+	COMMAND_EXECUTE_FUNCTION	=> 'EXECUTE FUNCTION',
+	COMMAND_EXPLAIN				=> 'EXPLAIN',
+	COMMAND_FETCH				=> 'FETCH',
+	COMMAND_GRANT				=> 'GRANT',
+	COMMAND_INSERT				=> 'INSERT',
+	COMMAND_PREPARE				=> 'PREPARE',
+	COMMAND_PREPARE_READ		=> 'PREPARE READ',
+	COMMAND_PREPARE_WRITE		=> 'PREPARE WRITE',
+	COMMAND_REVOKE				=> 'REVOKE',
+	COMMAND_SELECT				=> 'SELECT',
+	COMMAND_SET					=> 'SET',
+	COMMAND_UPDATE				=> 'UPDATE'
+};
+
+use constant
+{
+	TYPE			=> 'TYPE',
+	TYPE_NONE		=> '',
+
+	TYPE_FUNCTION	=> 'FUNCTION',
+	TYPE_INDEX		=> 'INDEX',
+	TYPE_TABLE		=> 'TABLE',
+	TYPE_TYPE		=> 'TYPE'
+};
+
+use constant
+{
+	NAME			=> 'NAME'
+};
+
+################################################################################
+# Command line parameters
+################################################################################
+my $strPgSqlBin = '../../../../bin/bin';	# Path of PG binaries to use for
+											# this test
+my $strTestPath = '../../../../data';		# Path where testing will occur
+my $iDefaultPort = 6000;					# Default port to run Postgres on
+my $bHelp = false;							# Display help
+my $bQuiet = false;							# Supress output except for errors
+my $bNoCleanup = false;						# Cleanup database on exit
+
+GetOptions ('q|quiet' => \$bQuiet,
+			'no-cleanup' => \$bNoCleanup,
+			'help' => \$bHelp,
+			'pgsql-bin=s' => \$strPgSqlBin,
+			'test-path=s' => \$strTestPath)
+	or pod2usage(2);
+
+# Display version and exit if requested
+if ($bHelp)
+{
+	print 'pg_audit unit test\n\n';
+	pod2usage();
+
+	exit 0;
+}
+
+################################################################################
+# Global variables
+################################################################################
+my $hDb;					# Connection to Postgres
+my $strLogExpected = '';	# The expected log compared with grepping AUDIT
+							# entries from the postgres log.
+
+my $strDatabase = 'postgres';	# Connected database (modified by PgSetDatabase)
+my $strUser = 'postgres';		# Connected user (modified by PgSetUser)
+my $strAuditRole = 'audit';		# Role to use for auditing
+
+my %oAuditLogHash;				# Hash to store pg_audit.log GUCS
+my %oAuditGrantHash;			# Hash to store pg_audit grants
+
+my $strCurrentAuditLog;		# pg_audit.log setting was Postgres was started with
+my $strTemporaryAuditLog;	# pg_audit.log setting that was set hot
+
+################################################################################
+# Stores the mapping between commands, classes, and types
+################################################################################
+my %oCommandHash =
+(&COMMAND_ANALYZE => {
+	&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE_SET => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_ALTER_ROLE},
+	&COMMAND_ALTER_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_ALTER_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_BEGIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CLOSE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COMMIT => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COPY_FROM => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_COPY_TO => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_CREATE_AGGREGATE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX},
+	&COMMAND_DEALLOCATE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_DECLARE_CURSOR => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_DO => {&CLASS => &CLASS_FUNCTION, &TYPE => &TYPE_NONE},
+	&COMMAND_DISCARD_ALL => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_FUNCTION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_CREATE_TABLE_AS => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TYPE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TYPE,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_EXECUTE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_FUNCTION => {&CLASS => &CLASS_FUNCTION,
+		&TYPE => &TYPE_FUNCTION, &COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXPLAIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_FETCH => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_GRANT => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_PREPARE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_PREPARE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_INSERT => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE},
+	&COMMAND_REVOKE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_SELECT => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_SET => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_UPDATE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE}
+);
+
+################################################################################
+# CommandExecute
+################################################################################
+sub CommandExecute
+{
+	my $strCommand = shift;
+	my $bSuppressError = shift;
+
+	# Set default
+	$bSuppressError = defined($bSuppressError) ? $bSuppressError : false;
+
+	# Run the command
+	my $iResult = system($strCommand);
+
+	if ($iResult != 0 && !$bSuppressError)
+	{
+		confess "command '${strCommand}' failed with error ${iResult}";
+	}
+}
+
+################################################################################
+# log
+################################################################################
+sub log
+{
+	my $strMessage = shift;
+	my $bError = shift;
+
+	# Set default
+	$bError = defined($bError) ? $bError : false;
+
+	if (!$bQuiet)
+	{
+		print "${strMessage}\n";
+	}
+
+	if ($bError)
+	{
+		exit 1;
+	}
+}
+
+################################################################################
+# ArrayToString
+################################################################################
+sub ArrayToString
+{
+	my @stryArray = @_;
+
+	my $strResult = '';
+
+	for (my $iIndex = 0; $iIndex < @stryArray; $iIndex++)
+	{
+		if ($iIndex != 0)
+		{
+			$strResult .= ', ';
+		}
+
+		$strResult .= $stryArray[$iIndex];
+	}
+
+	return $strResult;
+}
+
+################################################################################
+# BuildModule
+################################################################################
+sub BuildModule
+{
+	capture('cd ..;make');
+	CommandExecute("cp ../pg_audit.so" .
+	               " ${strPgSqlBin}/../lib/postgresql");
+	CommandExecute("cp ../pg_audit.control" .
+	               " ${strPgSqlBin}/../share/postgresql/extension");
+	CommandExecute("cp ../pg_audit--1.0.0.sql" .
+	               " ${strPgSqlBin}/../share/postgresql/extension");
+}
+
+################################################################################
+# PgConnect
+################################################################################
+sub PgConnect
+{
+	my $iPort = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+
+	# Log Connection
+	&log("   DB: connect user ${strUser}, database ${strDatabase}");
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# Connect to the db
+	$hDb = DBI->connect("dbi:Pg:dbname=${strDatabase};port=${iPort};host=/tmp",
+						$strUser, undef,
+						{AutoCommit => 1, RaiseError => 1});
+}
+
+################################################################################
+# PgDisconnect
+################################################################################
+sub PgDisconnect
+{
+	# Connect to the db (whether it is local or remote)
+	if (defined($hDb))
+	{
+		$hDb->disconnect;
+		undef($hDb);
+	}
+}
+
+################################################################################
+# PgExecute
+################################################################################
+sub PgExecute
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	my $hStatement = $hDb->prepare($strSql);
+
+	$hStatement->execute();
+	$hStatement->finish();
+}
+
+################################################################################
+# PgExecuteOnly
+################################################################################
+sub PgExecuteOnly
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	$hDb->do($strSql);
+}
+
+################################################################################
+# PgSetDatabase
+################################################################################
+sub PgSetDatabase
+{
+	my $strDatabaseParam = shift;
+
+	# Stop and start the database to reset pgconf entries
+	PgStop();
+	PgStart();
+
+	# Execute the statement
+	$strDatabase = $strDatabaseParam;
+	PgConnect();
+}
+
+################################################################################
+# PgSetUser
+################################################################################
+sub PgSetUser
+{
+	my $strUserParam = shift;
+
+	$strUser = $strUserParam;
+
+	# Stop and start the database to reset pgconf entries
+	if ((defined($strTemporaryAuditLog) && !defined($strCurrentAuditLog)) ||
+		(defined($strCurrentAuditLog) && !defined($strTemporaryAuditLog)) ||
+		$strCurrentAuditLog ne $strTemporaryAuditLog)
+	{
+		$strCurrentAuditLog = $strTemporaryAuditLog;
+
+		PgStop();
+		PgStart();
+	}
+	else
+	{
+		# Execute the statement
+		PgConnect();
+	}
+}
+
+################################################################################
+# SaveString
+################################################################################
+sub SaveString
+{
+	my $strFile = shift;
+	my $strString = shift;
+
+	# Open the file for writing
+	my $hFile;
+
+	open($hFile, '>', $strFile)
+		or confess "unable to open ${strFile}";
+
+	if ($strString ne '')
+	{
+		syswrite($hFile, $strString)
+			or confess "unable to write to ${strFile}: $!";
+	}
+
+	close($hFile);
+}
+
+################################################################################
+# PgLogExecute
+################################################################################
+sub PgLogExecute
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+	my $bExecute = shift;
+	my $bWait = shift;
+	my $bLogSql = shift;
+
+	# Set defaults
+	$bExecute = defined($bExecute) ? $bExecute : true;
+	$bWait = defined($bWait) ? $bWait : true;
+	$bLogSql = defined($bLogSql) ? $bLogSql : true;
+
+	if ($bExecute)
+	{
+		PgExecuteOnly($strSql);
+	}
+
+	PgLogExpect($strCommand, $bLogSql ? $strSql : '', $oData);
+
+	if ($bWait)
+	{
+		PgLogWait();
+	}
+}
+
+################################################################################
+# PgLogExpect
+################################################################################
+sub PgLogExpect
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+
+	# If oData is false then no logging
+	if (defined($oData) && ref($oData) eq '' && !$oData)
+	{
+		return;
+	}
+
+	# Log based on session
+	if (PgShouldLog($strCommand))
+	{
+		# Make sure class is defined
+		my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+		if (!defined($strClass))
+		{
+			confess "class is not defined for command ${strCommand}";
+		}
+
+		# Make sure object type is defined
+		my $strObjectType = $oCommandHash{$strCommand}{&TYPE};
+
+		if (!defined($strObjectType))
+		{
+			confess "object type is not defined for command ${strCommand}";
+		}
+
+		# Check for command override
+		my $strCommandLog = $strCommand;
+
+		if ($oCommandHash{$strCommand}{&COMMAND})
+		{
+			$strCommandLog = $oCommandHash{$strCommand}{&COMMAND};
+		}
+
+		my $strObjectName = '';
+
+		if (defined($oData) && ref($oData) ne 'ARRAY')
+		{
+			$strObjectName = $oData;
+		}
+
+		my $strLog .= "SESSION,${strClass},${strCommandLog}," .
+					  "${strObjectType},${strObjectName},${strSql}";
+		&log("AUDIT: ${strLog}");
+
+		$strLogExpected .= "${strLog}\n";
+	}
+
+	# Log based on grants
+	if (ref($oData) eq 'ARRAY' && ($strCommand eq COMMAND_SELECT ||
+		$oCommandHash{$strCommand}{&CLASS} eq CLASS_WRITE))
+	{
+		foreach my $oTableHash (@{$oData})
+		{
+			my $strObjectName = ${$oTableHash}{&NAME};
+			my $strCommandLog = ${$oTableHash}{&COMMAND};
+
+			if (defined($oAuditGrantHash{$strAuditRole}
+										{$strObjectName}{$strCommandLog}))
+			{
+				my $strCommandLog = defined(${$oTableHash}{&COMMAND_LOG}) ?
+					${$oTableHash}{&COMMAND_LOG} : $strCommandLog;
+				my $strClass = $oCommandHash{$strCommandLog}{&CLASS};
+				my $strObjectType = ${$oTableHash}{&TYPE};
+
+				my $strLog .= "OBJECT,${strClass},${strCommandLog}," .
+							  "${strObjectType},${strObjectName},${strSql}";
+				&log("AUDIT: ${strLog}");
+
+				$strLogExpected .= "${strLog}\n";
+			}
+		}
+
+		$oData = undef;
+	}
+}
+
+################################################################################
+# PgShouldLog
+################################################################################
+sub PgShouldLog
+{
+	my $strCommand = shift;
+
+	# Make sure class is defined
+	my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+	if (!defined($strClass))
+	{
+		confess "class is not defined for command ${strCommand}";
+	}
+
+	# Check logging for the role
+	my $bLog = undef;
+
+	if (defined($oAuditLogHash{&CONTEXT_ROLE}{$strUser}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_ROLE}{$strUser}{$strClass};
+	}
+
+	# Else check logging for the db
+	elsif (defined($oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}{$strClass};
+	}
+
+	# Else check logging for global
+	elsif (defined($oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}{$strClass};
+	}
+
+	return defined($bLog) ? true : false;
+}
+
+################################################################################
+# PgLogWait
+################################################################################
+sub PgLogWait
+{
+	my $strLogActual;
+
+	# Run in an eval block since grep returns 1 when nothing was found
+	eval
+	{
+		$strLogActual = capture("grep 'LOG:  AUDIT: '" .
+								" ${strTestPath}/postgresql.log");
+	};
+
+	# If an error was returned, continue if it was 1, otherwise confess
+	if ($@)
+	{
+		my $iExitStatus = $? >> 8;
+
+		if ($iExitStatus != 1)
+		{
+			confess "grep returned ${iExitStatus}";
+		}
+
+		$strLogActual = '';
+	}
+
+	# Strip the AUDIT and timestamp from the actual log
+	$strLogActual =~ s/prefix LOG:  AUDIT\: //g;
+
+	# Save the logs
+	SaveString("${strTestPath}/audit.actual", $strLogActual);
+	SaveString("${strTestPath}/audit.expected", $strLogExpected);
+
+	CommandExecute("diff ${strTestPath}/audit.expected" .
+				   " ${strTestPath}/audit.actual");
+}
+
+################################################################################
+# PgDrop
+################################################################################
+sub PgDrop
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Stop the cluster
+	PgStop(true, $strPath);
+
+	# Remove the directory
+	CommandExecute("rm -rf ${strTestPath}");
+}
+
+################################################################################
+# PgCreate
+################################################################################
+sub PgCreate
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	CommandExecute("${strPgSqlBin}/initdb -D ${strPath} -U ${strUser}" .
+				   ' -A trust > /dev/null');
+}
+
+################################################################################
+# PgStop
+################################################################################
+sub PgStop
+{
+	my $bImmediate = shift;
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+	$bImmediate = defined($bImmediate) ? $bImmediate : false;
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# If postmaster process is running then stop the cluster
+	if (-e $strPath . '/postmaster.pid')
+	{
+		CommandExecute("${strPgSqlBin}/pg_ctl stop -D ${strPath} -w -s -m " .
+					  ($bImmediate ? 'immediate' : 'fast'));
+	}
+}
+
+################################################################################
+# PgStart
+################################################################################
+sub PgStart
+{
+	my $iPort = shift;
+	my $strPath = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Make sure postgres is not running
+	if (-e $strPath . '/postmaster.pid')
+	{
+		confess "${strPath}/postmaster.pid exists, cannot start";
+	}
+
+	# Start the cluster
+	CommandExecute("${strPgSqlBin}/pg_ctl start -o \"" .
+				   "-c port=${iPort}" .
+				   " -c unix_socket_directories='/tmp'" .
+				   " -c shared_preload_libraries='pg_audit'" .
+				   " -c log_min_messages=debug1" .
+				   " -c log_line_prefix='prefix '" .
+				   # " -c log_destination='stderr,csvlog'" .
+				   # " -c logging_collector=on" .
+				   (defined($strCurrentAuditLog) ?
+					   " -c pg_audit.log='${strCurrentAuditLog}'" : '') .
+				   " -c pg_audit.role='${strAuditRole}'" .
+				   " -c log_connections=on" .
+				   "\" -D ${strPath} -l ${strPath}/postgresql.log -w -s");
+
+	# Connect user session
+	PgConnect();
+}
+
+################################################################################
+# PgAuditLogSet
+################################################################################
+sub PgAuditLogSet
+{
+	my $strContext = shift;
+	my $strName = shift;
+	my @stryClass = @_;
+
+	# Create SQL to set the GUC
+	my $strCommand;
+	my $strSql;
+
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		$strCommand = COMMAND_SET;
+		$strSql = "set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+		$strTemporaryAuditLog = ArrayToString(@stryClass);
+	}
+	elsif ($strContext eq CONTEXT_ROLE)
+	{
+		$strCommand = COMMAND_ALTER_ROLE_SET;
+		$strSql = "alter role ${strName} set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+	}
+	else
+	{
+		confess "unable to set pg_audit.log for context ${strContext}";
+	}
+
+	# Reset the audit log
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		delete($oAuditLogHash{$strContext});
+		$strName = CONTEXT_GLOBAL;
+	}
+	else
+	{
+		delete($oAuditLogHash{$strContext}{$strName});
+	}
+
+	# Store all the classes in the hash and build the GUC
+	foreach my $strClass (@stryClass)
+	{
+		if ($strClass eq CLASS_ALL)
+		{
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_DDL} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_FUNCTION} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_MISC} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_READ} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_WRITE} = true;
+		}
+
+		if (index($strClass, '-') == 0)
+		{
+			$strClass = substr($strClass, 1);
+
+			delete($oAuditLogHash{$strContext}{$strName}{$strClass});
+		}
+		else
+		{
+			$oAuditLogHash{$strContext}{$strName}{$strClass} = true;
+		}
+	}
+
+	PgLogExecute($strCommand, $strSql);
+}
+
+################################################################################
+# PgAuditGrantSet
+################################################################################
+sub PgAuditGrantSet
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_GRANT, "grant " . lc(${strPrivilege}) .
+								(defined($strColumn) ? " (${strColumn})" : '') .
+								" on ${strObject} to ${strRole}");
+
+	$oAuditGrantHash{$strRole}{$strObject}{$strPrivilege} = true;
+}
+
+################################################################################
+# PgAuditGrantReset
+################################################################################
+sub PgAuditGrantReset
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_REVOKE, "revoke " . lc(${strPrivilege}) .
+				 (defined($strColumn) ? " (${strColumn})" : '') .
+				 " on ${strObject} from ${strRole}");
+
+	delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege});
+}
+
+################################################################################
+# Main
+################################################################################
+my @oyTable; # Store table info for select, insert, update, delete
+
+# Drop the old cluster, build the code, and create a new cluster
+PgDrop();
+BuildModule();
+PgCreate();
+PgStart();
+
+PgExecute("create extension pg_audit");
+
+# Create test users and the audit role
+PgExecute("create user user1");
+PgExecute("create user user2");
+PgExecute("create role ${strAuditRole}");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL));
+
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_READ, CLASS_WRITE));
+
+# User1 follows the global log settings
+PgSetUser('user1');
+PgLogExecute(COMMAND_CREATE_TABLE, 'create table test (id int)', 'public.test');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+PgLogExecute(COMMAND_DROP_TABLE, 'drop table test', 'public.test');
+
+PgSetUser('user2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+             'create table test2 (id int)', 'public.test2');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+             'create table test3 (id int)', 'public.test2');
+
+# Catalog select should not log
+PgLogExecute(COMMAND_SELECT, 'select * from pg_class limit 1',
+							   false);
+
+# Multi-table select
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test3, test2',
+							   \@oyTable);
+
+# Various CTE combinations
+PgAuditGrantSet($strAuditRole, &COMMAND_INSERT, 'public.test3');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (select id from test2)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (insert into test3 values (1) returning id)' .
+			 ' insert into test2 select id from cte',
+			 \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE, 'public.test2');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_INSERT,
+             'with cte as (update test2 set id = 1 returning id)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_INSERT});
+PgLogExecute(COMMAND_UPDATE,
+			 'with cte as (insert into test2 values (1) returning id)' .
+			 ' update test3 set id = cte.id' .
+			 ' from cte where test3.id <> cte.id',
+			 \@oyTable);
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_NONE));
+PgSetUser('user2');
+
+# Column-based audits
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test4 (id int, name text)', 'public.test4');
+PgAuditGrantSet($strAuditRole, COMMAND_SELECT, 'public.test4', 'name');
+PgAuditGrantSet($strAuditRole, COMMAND_UPDATE, 'public.test4', 'id');
+PgAuditGrantSet($strAuditRole, COMMAND_INSERT, 'public.test4', 'name');
+
+# Select
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id from public.test4',
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from public.test4',
+							  \@oyTable);
+
+# Insert
+@oyTable = ();
+PgLogExecute(COMMAND_INSERT, 'insert into public.test4 (id) values (1)',
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT, "insert into public.test4 (name) values ('test')",
+							  \@oyTable);
+
+# Update
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set name = 'foo'",
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set id = 1",
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+            &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update public.test4 set name = 'foo' where name = 'bar'",
+			 \@oyTable);
+
+# Drop test tables
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test2", 'public.test2');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test3", 'public.test3');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test4", 'public.test4');
+
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Now create some email friendly tests.  These first tests are session logging
+# only.
+PgSetUser('postgres');
+
+&log("\nExamples:");
+
+&log("\nSession Audit:\n");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL, CLASS_READ));
+
+# !!! Trying to build test to exclude function calls (did not work)
+# PgLogExecute(COMMAND_CREATE_ROLE, 'create role func_owner');
+# PgAuditLogSet(CONTEXT_ROLE, 'func_owner');
+# PgLogExecute(COMMAND_SET, 'set role func_owner');
+# PgLogExecute(COMMAND_CREATE_FUNCTION, 'CREATE FUNCTION func_test(a int)' .
+# 									  ' returns int as $$ begin return a + 1;' .
+# 									  ' end $$language plpgsql security definer');
+# PgLogExecute(COMMAND_GRANT, 'grant execute on function func_test(int) to user1');
+
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account (id int, name text, password text,' .
+			 ' description text)', 'public.account');
+PgLogExecute(COMMAND_SELECT,
+			 'select * from account');
+PgLogExecute(COMMAND_INSERT,
+			 "insert into account (id, name, password, description)" .
+			 " values (1, 'user1', 'HASH1', 'blah, blah')");
+&log("AUDIT: <nothing logged>");
+
+# Test function without auditing (did not work)
+# PgLogExecute(COMMAND_SELECT, 'select func_test(1)');
+
+# Now tests for object logging
+&log("\nObject Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_NONE));
+PgExecute("set pg_audit.role = 'audit'");
+PgSetUser('user1');
+
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account', 'password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id, name from account',
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE,
+                'public.account', 'name, password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Now tests for session/object logging
+&log("\nSession/Object Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user1', (CLASS_READ, CLASS_WRITE));
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account_role_map (account_id int, role_id int)',
+			 'public.account_role_map');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account_role_map');
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.account_role_map', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT,
+			 'select account.password, account_role_map.role_id from account' .
+			 ' inner join account_role_map' .
+			 ' on account.id = account_role_map.account_id',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+             &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update account set description = 'yada, yada'" .
+			 " where password = 'HASH2'",
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Test all sql commands
+&log("\nExhaustive Command Tests:\n");
+
+PgSetUser('postgres');
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_ALL));
+PgLogExecute(COMMAND_SET, "set pg_audit.role = 'audit'");
+
+PgLogExecute(COMMAND_DO, "do \$\$\ begin raise notice 'test'; end; \$\$;");
+PgLogExecute(COMMAND_CREATE_SCHEMA, "create schema test");
+
+# Test COPY
+PgLogExecute(COMMAND_COPY_TO,
+			 "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'");
+PgLogExecute(COMMAND_CREATE_TABLE_AS,
+			 "CREATE TABLE test.pg_class as select * from pg_class",
+			 'test.pg_class', true, false);
+PgLogExecute(COMMAND_INSERT,
+			 "CREATE TABLE test.pg_class as select * from pg_class",
+			 undef, false, true);
+PgLogExecute(COMMAND_INSERT,
+			 "COPY test.pg_class from '" . abs_path($strTestPath) .
+			 "/class.out'", undef, true, false);
+PgLogExecute(COMMAND_COPY_FROM,
+			 "COPY test.pg_class from '" . abs_path($strTestPath) .
+			 "/class.out'", undef, false, true);
+
+# Test prepared SELECT
+PgLogExecute(COMMAND_PREPARE_READ,
+			 'PREPARE pgclassstmt (oid) as select *' .
+			 ' from pg_class where oid = $1');
+PgLogExecute(COMMAND_EXECUTE_READ,
+			 'EXECUTE pgclassstmt (1)');
+PgLogExecute(COMMAND_DEALLOCATE,
+			 'DEALLOCATE pgclassstmt');
+
+# Test cursor
+PgLogExecute(COMMAND_BEGIN,
+			 'BEGIN');
+PgLogExecute(COMMAND_DECLARE_CURSOR,
+		     'DECLARE ctest SCROLL CURSOR FOR SELECT * FROM pg_class');
+PgLogExecute(COMMAND_FETCH,
+			 'FETCH NEXT FROM ctest');
+PgLogExecute(COMMAND_CLOSE,
+			 'CLOSE ctest');
+PgLogExecute(COMMAND_COMMIT,
+			 'COMMIT');
+
+# Test prepared INSERT
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test.test_insert (id int)', 'test.test_insert');
+PgLogExecute(COMMAND_PREPARE_WRITE,
+			 'PREPARE pgclassstmt (oid) as insert' .
+			 ' into test.test_insert (id) values ($1)');
+PgLogExecute(COMMAND_INSERT,
+			 'EXECUTE pgclassstmt (1)', undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_WRITE,
+			 'EXECUTE pgclassstmt (1)', undef, false, true);
+
+# Create a table with a primary key
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test (id int primary key, name text,' .
+			 'description text)',
+			 'public.test', true, false);
+PgLogExecute(COMMAND_CREATE_INDEX,
+			 'create table test (id int primary key, name text,' .
+			 'description text)',
+			 'public.test_pkey', false, true);
+PgLogExecute(COMMAND_ANALYZE, 'analyze test');
+
+# Grant select to public - this should have no affect on auditing
+PgLogExecute(COMMAND_GRANT, 'grant select on public.test to public');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+# Now grant select to audit and it should be logged
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test', \@oyTable);
+
+# Check columns granted to public and make sure they do not log
+PgAuditGrantReset($strAuditRole, &COMMAND_SELECT, 'public.test');
+PgLogExecute(COMMAND_GRANT, 'grant select (name) on public.test to public');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+PgLogExecute(COMMAND_SELECT, 'select from test');
+
+# Try a select that does not reference any tables
+PgLogExecute(COMMAND_SELECT, 'select 1, current_timestamp');
+
+# Try explain
+PgLogExecute(COMMAND_EXPLAIN, 'explain select 1');
+
+# Now set grant to a specific column to audit and make sure it logs
+# Make sure the the converse is true
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test',
+				'name, description');
+PgLogExecute(COMMAND_SELECT, 'select id from test');
+
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from test', \@oyTable);
+
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test drop description', 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable);
+
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test rename to test2', 'public.test');
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 'alter table test2 set schema test', 'public.test2', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE_INDEX, 'alter table test2 set schema test',
+										'public.test_pkey', false, true);
+PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 add description text',
+								  'test.test2');
+PgLogExecute(COMMAND_ALTER_TABLE, 'alter table test.test2 drop description',
+								  'test.test2');
+PgLogExecute(COMMAND_DROP_TABLE_INDEX, 'drop table test.test2',
+									   'test.test_pkey', false, false);
+PgLogExecute(COMMAND_DROP_TABLE, 'drop table test.test2',
+								 'test.test2', true, true);
+
+PgLogExecute(COMMAND_CREATE_FUNCTION, 'CREATE FUNCTION int_add(a int, b int)' .
+									  ' returns int as $$ begin return a + b;' .
+									  ' end $$language plpgsql');
+PgLogExecute(COMMAND_SELECT, "select int_add(1, 1)",
+							 undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_FUNCTION, "select int_add(1, 1)",
+									   'public.int_add', false, true);
+
+PgLogExecute(COMMAND_CREATE_AGGREGATE, "CREATE AGGREGATE sum_test (int)" .
+							" (sfunc = int_add, stype = int, initcond = 0)");
+PgLogExecute(COMMAND_ALTER_AGGREGATE,
+			 "ALTER AGGREGATE sum_test (int) rename to sum_test2");
+
+PgLogExecute(COMMAND_CREATE_COLLATION,
+			 "CREATE COLLATION collation_test FROM \"de_DE\"");
+PgLogExecute(COMMAND_ALTER_COLLATION,
+			 "ALTER COLLATION collation_test rename to collation_test2");
+
+PgLogExecute(COMMAND_CREATE_CONVERSION,
+			 "CREATE CONVERSION conversion_test FOR 'SQL_ASCII' TO".
+			 " 'MULE_INTERNAL' FROM ascii_to_mic");
+PgLogExecute(COMMAND_ALTER_CONVERSION,
+			 "ALTER CONVERSION conversion_test rename to conversion_test2");
+
+PgLogExecute(COMMAND_CREATE_DATABASE, "CREATE DATABASE database_test");
+PgLogExecute(COMMAND_ALTER_DATABASE,
+			 "ALTER DATABASE database_test rename to database_test2");
+PgLogExecute(COMMAND_DROP_DATABASE, "DROP DATABASE database_test2");
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Stop the database
+if (!$bNoCleanup)
+{
+	PgDrop();
+}
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index a698d0f..5b247a9 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index f03b72a..e4f0bdc 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -124,6 +124,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbench         SYSTEM "pgbench.sgml">
 <!ENTITY pgarchivecleanup SYSTEM "pgarchivecleanup.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..d62d45f
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,330 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> module provides session and object
+    auditing via the standard logging facility.  Session and object auditing are
+    completely independent and can be combined.
+  </para>
+
+  <sect2>
+    <title>Session Auditing</title>
+
+    <para>
+      Session auditing allows the logging of all commands that are executed by
+      a user in the backend.  Each command is logged with a single entry and
+      includes the audit type (e.g. <literal>SESSION</literal>), command type
+      (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and
+      statement (e.g. <literal>"select * from test"</literal>).
+
+      Fully-qualified names and object types will be logged for
+      <literal>CREATE</literal>, <literal>UPDATE</literal>, and
+      <literal>DROP</literal> commands on <literal>TABLE</literal>,
+      <literal>MATVIEW</literal>, <literal>VIEW</literal>,
+      <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,
+      <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and
+      <literal>SEQUENCE</literal> objects as well as function calls.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is controlled by the <literal>pg_audit.log</literal>
+        GUC. There are five classes of commands that are recognized:
+
+        <itemizedlist>
+          <listitem>
+            <para>
+              <literal>READ</literal> - <literal>SELECT</literal> and
+              <literal>COPY</literal> when the source is a table or query.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>WRITE</literal> - <literal>INSERT</literal>,
+              <literal>UPDATE</literal>, <literal>DELETE</literal>,
+              <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+              destination is a table.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>FUNCTION</literal> - Function calls and
+              <literal>DO</literal> blocks.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>DDL</literal> - DDL, plus <literal>VACUUM</literal>,
+              <literal>REINDEX</literal>, and <literal>ANALYZE</literal>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>MISC</literal> - Miscellaneous commands, e.g.
+              <literal>DISCARD</literal>, <literal>FETCH</literal>,
+              <literal>CHECKPOINT</literal>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </para>
+
+      <para>
+        Enable session logging for all writes and DDL:
+          <programlisting>
+pg_audit.log = 'write, ddl'
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except miscellaneous:
+          <programlisting>
+pg_audit.log = 'all, -misc'
+          </programlisting>
+      </para>
+      
+      <para>
+      Note that <literal>pg_audit.log</literal> can be set globally (in 
+      <filename>postgresql.conf</filename>), at the database level (using
+      <literal>alter database ... set</literal>), or at the role level (using
+      <literal>alter role ... set</literal>).
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.log = 'read, ddl'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+      <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+select *
+    from account;
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Object Auditing</title>
+
+    <para>
+      Object auditing logs commands that affect a particular object.  Only
+      <literal>SELECT</literal>, <literal>INSERT</literal>,
+      <literal>UPDATE</literal> and <literal>DELETE</literal> commands are
+      supported.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level auditing is implemented via the roles system.  The
+        <literal>pg_audit.role</literal> GUC defines the role that will be used
+        for auditing.  An object will be audited when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.
+      </para>
+
+      <programlisting>
+postresql.conf: pg_audit.role = 'audit'
+
+grant select, delete
+   on public.account;
+      </programlisting>
+
+      <para>
+      Note that <literal>pg_audit.role</literal> can be set globally (in 
+      <filename>postgresql.conf</filename>), at the database level (using
+      <literal>alter database ... set</literal>), or at the role level (using
+      <literal>alter role ... set</literal>).
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.role = 'audit'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to audit;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to audit;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to audit;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is not in compliant CSV format.  If machine-readability is
+          required then consider setting
+          <literal>log_destination = 'csvlog'</literal>.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</literal> - <literal>SESSION</literal> or
+            <literal>OBJECT</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</literal> - <literal>READ</literal>,
+            <literal>WRITE</literal>, <literal>FUNCTION</literal>,
+            <literal>DDL</literal>, or <literal>MISC</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</literal> - <literal>ALTER TABLE</literal>,
+            <literal>SELECT</literal>, <literal>CREATE INDEX</literal>,
+            <literal>UPDATE</literal>, etc.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</literal> - <literal>TABLE</literal>,
+            <literal>INDEX</literal>, <literal>VIEW</literal>, etc.  Only
+            available for DML and certain DDL commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</literal> - The fully-qualified object name
+            (e.g. public.account).  Only available for DML and certain DDL
+            commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</literal> - Statement execute on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#7Abhijit Menon-Sen
ams@2ndQuadrant.com
In reply to: David Steele (#6)
Re: Auditing extension for PostgreSQL (Take 2)

At 2015-02-24 11:22:41 -0500, david@pgmasters.net wrote:

Patch v3 is attached.

[…]

+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* SELECT */
+	LOG_READ = (1 << 0),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 1),
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 2),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 4),
+
+	/* Function execution */
+	LOG_MISC = (1 << 5),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};

The comment above LOG_MISC should be changed.

More fundamentally, this classification makes it easy to reuse LOGSTMT_*
(and a nice job you've done of that, with just a few additional special
cases), I don't think this level is quite enough for our needs. I think
it should at least be possible to specifically log commands that affect
privileges and roles.

I'm fond of finer categorisation for DDL as well, but I could live with
all DDL being lumped together.

I'm experimenting with a few approaches to do this without reintroducing
switch statements to test every command. That will require core changes,
but I think we can find an acceptable arrangement. I'll post a proof of
concept in a few days.

+ * Takes an AuditEvent and, if it log_check(), writes it to the audit
log.

I don't think log_check is the most useful name, because this sentence
doesn't tell me what the function may do. Similarly, I would prefer to
have log_acl_check be renamed acl_grants_audit or similar. (These are
all static functions anyway, I don't think a log_ prefix is needed.)

+ /* Free the column set */
+ bms_free(tmpSet);

(An aside, really: there are lots of comments like this, which I don't
think add anything to understanding the code, and should be removed.)

+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		auditEvent.logStmtLevel = LOGSTMT_MOD;

(I am also trying to find a way to avoid having to do this.)

+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				utilityAuditEvent.objectType = OBJECT_TYPE_TABLE;
+				break;

This occurs elsewhere too. But I suppose new relkinds are added less
frequently than new commands.

Again on a larger level, I'm not sure how I feel about _avoiding_ the
use of event triggers for audit logging. Regardless of whether we use
the deparse code (which I personally think is a good idea; Álvaro has
been working on it, and it looks very nice) to log extra information,
using the object access hook inevitably means we have to reimplement
the identification/classification code here.

In "old" pgaudit, I think that extra effort is justified by the need to
be backwards compatible with pre-event trigger releases. In a 9.5-only
version, I am not at all convinced that this makes sense.

Thoughts?

-- Abhijit

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

#8David Steele
david@pgmasters.net
In reply to: Abhijit Menon-Sen (#7)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

Thanks for the review, Abhijit.

On 3/23/15 1:31 AM, Abhijit Menon-Sen wrote:

At 2015-02-24 11:22:41 -0500, david@pgmasters.net wrote:

Patch v3 is attached.
+
+	/* Function execution */
+	LOG_MISC = (1 << 5),

The comment above LOG_MISC should be changed.

Fixed.

More fundamentally, this classification makes it easy to reuse LOGSTMT_*
(and a nice job you've done of that, with just a few additional special
cases), I don't think this level is quite enough for our needs. I think
it should at least be possible to specifically log commands that affect
privileges and roles.

I agree, but this turns out to be easier said than done. In the prior
code for instance, CREATE ROLE was classified as USER, while ALTER ROLE
.. RENAME was classified as DDL. This is because any rename gets the
command tag T_RenameStmt. CreateCommandTag does return "ALTER ROLE",
but now we're in the realm of string-matching again which is not my
favorite thing. Let me see if there is a clean way to get this
accomplished. I've also felt this is the one thing I'd like to see
broken out.

I'm fond of finer categorisation for DDL as well, but I could live with
all DDL being lumped together.

I'm experimenting with a few approaches to do this without reintroducing
switch statements to test every command. That will require core changes,
but I think we can find an acceptable arrangement. I'll post a proof of
concept in a few days.

I also think finer-grained categorization would be best accomplished
with some core changes. It seemed too late to get those in for 9.5 so I
decided to proceed with what I knew could be done reliably with the idea
to improve it with core changes going forward.

I look forward to your proof-of-concept.

+ * Takes an AuditEvent and, if it log_check(), writes it to the audit
log.

I don't think log_check is the most useful name, because this sentence
doesn't tell me what the function may do. Similarly, I would prefer to
have log_acl_check be renamed acl_grants_audit or similar. (These are
all static functions anyway, I don't think a log_ prefix is needed.)

log_check() has become somewhat vestigial at this point since it is only
called from one place - I've been considering removing it and merging
into log_audit_event(). For the moment I've improved the comments.

I like acl_grants_audit() and agree that it's a clearer name. I'll
incorporate that into the next version and apply the same scheme to the
other ACL functionsas well as do a general review of naming.

+ /* Free the column set */
+ bms_free(tmpSet);

(An aside, really: there are lots of comments like this, which I don't
think add anything to understanding the code, and should be removed.)

I generally feel like you can't have too many comments. I think even
the less interesting/helpful comments help break the code into
functional sections for readability.

+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		auditEvent.logStmtLevel = LOGSTMT_MOD;

(I am also trying to find a way to avoid having to do this.)

That would be excellent.

+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				utilityAuditEvent.objectType = OBJECT_TYPE_TABLE;
+				break;

This occurs elsewhere too. But I suppose new relkinds are added less
frequently than new commands.

Well, that's the hope at least. I should mention that ALL statements
will be logged no matter what additional classification happens. The
amount of information returned may not be ideal, but nothing is ever
excluded from logging (depending on the classes selected, of course).

Again on a larger level, I'm not sure how I feel about _avoiding_ the
use of event triggers for audit logging. Regardless of whether we use
the deparse code (which I personally think is a good idea; Álvaro has
been working on it, and it looks very nice) to log extra information,
using the object access hook inevitably means we have to reimplement
the identification/classification code here.

In "old" pgaudit, I think that extra effort is justified by the need to
be backwards compatible with pre-event trigger releases. In a 9.5-only
version, I am not at all convinced that this makes sense.

Thoughts?

I was nervous about basing pg_audit on code that I wasn't sure would be
committed (at the time). Since pg_event_trigger_get_creation_commands()
is tied up with deparse, I honestly didn't feel like the triggers were
bringing much to the table.

That being said, I agree that the deparse code is very useful and now
looks certain to be committed for 9.5.

I have prepared a patch that brings event triggers and deparse back to
pg_audit based on the Alvaro's dev/deparse branch at
git://git.postgresql.org/git/2ndquadrant_bdr.git (commit 0447fc5). I've
updated the unit tests accordingly.

I left in the OAT code for now. It does add detail to one event that
the event triggers do not handle (creating PK indexes) and I feel that
it lends an element of safety in case something happens to the event
triggers. OAT is also required for function calls so the code path
cannot be eliminated entirely. I'm not committed to keeping the
redundant OAT code, but I'd rather not remove it until deparse is
committed to master.

I've been hesitant to post this patch as it will not work in master
(though it will compile), but I don't want to hold on to it any longer
since the end of the CF is nominally just weeks away. If you want to
run the patch in master, you'll need to disable the
pg_audit_ddl_command_end trigger.

I've also addressed Fujii's concerns about logging parameters - this is
now an option. The event stack has been formalized and
MemoryContextRegisterResetCallback() is used to cleanup the stack on errors.

Let me know what you think.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v4.patchtext/plain; charset=UTF-8; name=pg_audit-v4.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index 195d447..d8e75f4 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -29,6 +29,7 @@ SUBDIRS = \
 		pageinspect	\
 		passwordcheck	\
 		pg_archivecleanup \
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..32bc6d9
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,20 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..6c24650
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1716 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pgaudit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pgaudit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pgaudit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_PARAMETER		"PARAMETER"
+#define CLASS_READ			"READ"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* Function execution */
+	LOG_PARAMETER = (1 << 4),
+
+	/* SELECT */
+	LOG_READ = (1 << 5),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 6),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static uint64 statementTotal = 0;
+static uint64 substatementTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack the the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop()
+{
+	/* Error if the stack is already empty */
+	if (auditEventStack == NULL)
+		elog(ERROR, "pg_audit stack is already empty");
+
+	/* Switch the old memory context and delete the audit context */
+	MemoryContextDelete(auditEventStack->contextAudit);
+}
+
+/*
+ * Takes an AuditEvent and returns true or false depending on whether the event
+ * should be logged according to the pgaudit.roles/log settings. If it returns
+ * true, also fills in the name of the LogClass which it is logged under.
+ */
+static bool
+log_check(AuditEvent *e, const char **classname)
+{
+	enum LogClass class = LOG_NONE;
+
+	/* By default put everything in the MISC class. */
+	*classname = CLASS_MISC;
+	class = LOG_MISC;
+
+	/*
+	 * Look at the type of the command and decide what LogClass needs to be
+	 * enabled for the command to be logged.
+	 */
+	switch (e->logStmtLevel)
+	{
+		case LOGSTMT_MOD:
+			*classname = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		case LOGSTMT_DDL:
+			*classname = CLASS_DDL;
+			class = LOG_DDL;
+
+		case LOGSTMT_ALL:
+			switch (e->commandTag)
+			{
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					*classname = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				case T_VacuumStmt:
+				case T_ReindexStmt:
+					*classname = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				case T_DoStmt:
+					*classname = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * We log audit events under the following conditions:
+	 *
+	 * 1. If the audit role has been explicitly granted permission for
+	 *    an operation.
+	 */
+	if (e->granted)
+	{
+		return true;
+	}
+
+	/* 2. If the event belongs to a class covered by pgaudit.log. */
+	if ((auditLogBitmap & class) == class)
+	{
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent and, if it log_check(), writes it to the audit log. The
+ * AuditEvent is assumed to be completely filled in by the caller (unknown
+ * values must be set to "" so that they can be logged without error checking).
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	const char *classname;
+	MemoryContext contextOld;
+
+	/* Check that this event should be logged. */
+	if (!log_check(&stackItem->auditEvent, &classname))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	StringInfoData auditStr;
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogBitmap & LOG_PARAMETER &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	ereport(LOG,
+		(errmsg("AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			classname, auditStr.data),
+		 errhidestmt(true)));
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+log_acl_check(Datum aclDatum, Oid auditOid, AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+log_relation_check(Oid relOid,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = log_acl_check(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+log_attribute_check(Oid relOid,
+					AttrNumber attNum,
+					Oid auditOid,
+					AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = log_acl_check(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+log_attribute_check_any(Oid relOid,
+						Oid auditOid,
+						Bitmapset *attributeSet,
+						AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (log_attribute_check(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			log_attribute_check(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/* If this is the first rte then session log */
+		if (first)
+		{
+			auditEventStack->auditEvent.objectName = "";
+			auditEventStack->auditEvent.objectType = "";
+
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (log_relation_check(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						log_attribute_check_any(relOid, auditOid,
+												rte->selectedCols,
+												ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEventStack->auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEventStack->auditEvent.granted =
+							log_attribute_check_any(relOid, auditOid,
+													rte->modifiedCols,
+													auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Only do relation level logging if a grant was found. */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop();
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute */
+		case OAT_FUNCTION_EXECUTE:
+			if (auditLogBitmap & LOG_FUNCTION)
+				log_function_execute(objectId);
+			break;
+
+		/* Log create */
+		case OAT_POST_CREATE:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter */
+		case OAT_POST_ALTER:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop */
+		case OAT_DROP:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessDrop *drop = arg;
+
+				if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+static ExecutorEnd_hook_type next_ExecutorEnd_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pgaudit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pgaudit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ExecutorEnd to pop statement audit event off the stack.
+ */
+static void
+pgaudit_ExecutorEnd_hook(QueryDesc *queryDesc)
+{
+	/* Call the next hook or standard function */
+	if (next_ExecutorEnd_hook)
+		next_ExecutorEnd_hook(queryDesc);
+	else
+		standard_ExecutorEnd(queryDesc);
+
+	/* Pop the audit event off the stack */
+	if (!internalStatement)
+	{
+		stack_pop();
+	}
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pgaudit_ProcessUtility_hook(Node *parsetree,
+							const char *queryString,
+							ProcessUtilityContext context,
+							ParamListInfo params,
+							DestReceiver *dest,
+							char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	/* Allocate the audit event */
+	if (!IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap != 0 &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/* Process the audit event if there is one. */
+	if (stackItem != NULL)
+	{
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !stackItem->auditEvent.logged &&
+			!IsAbortedTransactionBlockState())
+			log_audit_event(stackItem);
+
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			while (auditEventStack != NULL)
+				stack_pop();
+		}
+		else
+			stack_pop();
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pgaudit_object_access_hook(ObjectAccessType access,
+						   Oid classId,
+						   Oid objectId,
+						   int subId,
+						   void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&
+		auditLogBitmap & (LOG_DDL | LOG_FUNCTION))
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+	/* Continue only if session logging is enabled */
+	if (auditLogBitmap != LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pgaudit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type), schema,\n"
+				"       identity, command\n"
+				"  FROM pg_event_trigger_get_creation_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			bool	   isNull;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply addition data to current audit event */
+			auditEventStack->auditEvent.logStmtLevel =
+				GetCommandLogLevel(eventData->parsetree);
+			auditEventStack->auditEvent.commandTag =
+				nodeTag(eventData->parsetree);
+			auditEventStack->auditEvent.command =
+				CreateCommandTag(eventData->parsetree);
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 6);
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			auditEventStack->auditEvent.commandText =
+				TextDatumGetCString(
+					DirectFunctionCall1(pg_event_trigger_expand_command,
+										SPI_getbinval(spiTuple, spiTupDesc,
+													  7, &isNull)));
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pgaudit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type),\n"
+				"       schema_name, object_name, object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			char *schemaName;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			schemaName = SPI_getvalue(spiTuple, spiTupDesc, 5);
+
+			if (!(pg_strcasecmp(auditEventStack->auditEvent.objectType,
+							"TYPE") == 0 ||
+				  pg_strcasecmp(schemaName, "pg_toast") == 0))
+			{
+				auditEventStack->auditEvent.objectName =
+						SPI_getvalue(spiTuple, spiTupDesc, 7);
+
+				log_audit_event(auditEventStack);
+			}
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pgaudit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then then token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_PARAMETER) == 0)
+			class = LOG_PARAMETER;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pgaudit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pgaudit_log from extra (ignoring newval, which has already been converted
+ * to a bitmap above). Note that extra may not be set if the assignment is to be
+ * suppressed.
+ */
+static void
+assign_pgaudit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * This variable defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * This variables controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pgaudit_log,
+							   assign_pgaudit_log,
+							   NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pgaudit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pgaudit_ExecutorCheckPerms_hook;
+
+	next_ExecutorEnd_hook = ExecutorEnd_hook;
+	ExecutorEnd_hook = pgaudit_ExecutorEnd_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pgaudit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pgaudit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.pl
new file mode 100755
index 0000000..f5cbde2
--- /dev/null
+++ b/contrib/pg_audit/test/test.pl
@@ -0,0 +1,1412 @@
+#!/usr/bin/perl
+################################################################################
+# test.pl - pg_audit Unit Tests
+################################################################################
+
+################################################################################
+# Perl includes
+################################################################################
+use strict;
+use warnings;
+use Carp;
+
+use Getopt::Long;
+use Pod::Usage;
+use DBI;
+use Cwd qw(abs_path);
+use IPC::System::Simple qw(capture);
+
+################################################################################
+# Constants
+################################################################################
+use constant
+{
+	true  => 1,
+	false => 0
+};
+
+use constant
+{
+	CONTEXT_GLOBAL   => 'GLOBAL',
+	CONTEXT_DATABASE => 'DATABASE',
+	CONTEXT_ROLE	 => 'ROLE'
+};
+
+use constant
+{
+	CLASS			=> 'CLASS',
+
+	CLASS_DDL		=> 'DDL',
+	CLASS_FUNCTION	=> 'FUNCTION',
+	CLASS_MISC		=> 'MISC',
+	CLASS_PARAMETER => 'PARAMETER',
+	CLASS_READ		=> 'READ',
+	CLASS_WRITE		=> 'WRITE',
+
+	CLASS_ALL		=> 'ALL',
+	CLASS_NONE		=> 'NONE'
+};
+
+use constant
+{
+	COMMAND						=> 'COMMAND',
+	COMMAND_LOG					=> 'COMMAND_LOG',
+
+	COMMAND_ANALYZE					=> 'ANALYZE',
+	COMMAND_ALTER_AGGREGATE			=> 'ALTER AGGREGATE',
+	COMMAND_ALTER_COLLATION			=> 'ALTER COLLATION',
+	COMMAND_ALTER_CONVERSION		=> 'ALTER CONVERSION',
+	COMMAND_ALTER_DATABASE			=> 'ALTER DATABASE',
+	COMMAND_ALTER_ROLE				=> 'ALTER ROLE',
+	COMMAND_ALTER_ROLE_SET			=> 'ALTER ROLE SET',
+	COMMAND_ALTER_TABLE				=> 'ALTER TABLE',
+	COMMAND_ALTER_TABLE_COLUMN		=> 'ALTER TABLE COLUMN',
+	COMMAND_ALTER_TABLE_INDEX		=> 'ALTER TABLE INDEX',
+	COMMAND_BEGIN					=> 'BEGIN',
+	COMMAND_CLOSE					=> 'CLOSE CURSOR',
+	COMMAND_COMMIT					=> 'COMMIT',
+	COMMAND_COPY					=> 'COPY',
+	COMMAND_COPY_TO					=> 'COPY TO',
+	COMMAND_COPY_FROM				=> 'COPY FROM',
+	COMMAND_CREATE_AGGREGATE		=> 'CREATE AGGREGATE',
+	COMMAND_CREATE_COLLATION		=> 'CREATE COLLATION',
+	COMMAND_CREATE_CONVERSION		=> 'CREATE CONVERSION',
+	COMMAND_CREATE_DATABASE			=> 'CREATE DATABASE',
+	COMMAND_CREATE_INDEX			=> 'CREATE INDEX',
+	COMMAND_DEALLOCATE				=> 'DEALLOCATE',
+	COMMAND_DECLARE_CURSOR			=> 'DECLARE CURSOR',
+	COMMAND_DO						=> 'DO',
+	COMMAND_DISCARD_ALL				=> 'DISCARD ALL',
+	COMMAND_CREATE_FUNCTION			=> 'CREATE FUNCTION',
+	COMMAND_CREATE_ROLE				=> 'CREATE ROLE',
+	COMMAND_CREATE_SCHEMA			=> 'CREATE SCHEMA',
+	COMMAND_CREATE_TABLE			=> 'CREATE TABLE',
+	COMMAND_CREATE_TABLE_AS			=> 'CREATE TABLE AS',
+	COMMAND_DROP_DATABASE			=> 'DROP DATABASE',
+	COMMAND_DROP_SCHEMA				=> 'DROP SCHEMA',
+	COMMAND_DROP_TABLE				=> 'DROP TABLE',
+	COMMAND_DROP_TABLE_CONSTRAINT	=> 'DROP TABLE CONSTRAINT',
+	COMMAND_DROP_TABLE_INDEX		=> 'DROP TABLE INDEX',
+	COMMAND_DROP_TABLE_TOAST		=> 'DROP TABLE TOAST',
+	COMMAND_DROP_TABLE_TYPE			=> 'DROP TABLE TYPE',
+	COMMAND_EXECUTE					=> 'EXECUTE',
+	COMMAND_EXECUTE_READ			=> 'EXECUTE READ',
+	COMMAND_EXECUTE_WRITE			=> 'EXECUTE WRITE',
+	COMMAND_EXECUTE_FUNCTION		=> 'EXECUTE FUNCTION',
+	COMMAND_EXPLAIN					=> 'EXPLAIN',
+	COMMAND_FETCH					=> 'FETCH',
+	COMMAND_GRANT					=> 'GRANT',
+	COMMAND_INSERT					=> 'INSERT',
+	# COMMAND_PARAMETER				=> 'PARAMETER',
+	# COMMAND_PARAMETER_READ			=> 'PARAMETER_READ',
+	# COMMAND_PARAMETER_WRITE			=> 'PARAMETER_WRITE',
+	COMMAND_PREPARE					=> 'PREPARE',
+	COMMAND_PREPARE_READ			=> 'PREPARE READ',
+	COMMAND_PREPARE_WRITE			=> 'PREPARE WRITE',
+	COMMAND_REVOKE					=> 'REVOKE',
+	COMMAND_SELECT					=> 'SELECT',
+	COMMAND_SET						=> 'SET',
+	COMMAND_UPDATE					=> 'UPDATE'
+};
+
+use constant
+{
+	TYPE					=> 'TYPE',
+	TYPE_NONE				=> '',
+
+	TYPE_AGGREGATE			=> 'AGGREGATE',
+	TYPE_COLLATION			=> 'COLLATION',
+	TYPE_CONVERSION			=> 'CONVERSION',
+	TYPE_SCHEMA				=> 'SCHEMA',
+	TYPE_FUNCTION			=> 'FUNCTION',
+	TYPE_INDEX				=> 'INDEX',
+	TYPE_TABLE				=> 'TABLE',
+	TYPE_TABLE_COLUMN		=> 'TABLE COLUMN',
+	TYPE_TABLE_CONSTRAINT	=> 'TABLE CONSTRAINT',
+	TYPE_TABLE_TOAST		=> 'TABLE TOAST',
+	TYPE_TYPE				=> 'TYPE'
+};
+
+use constant
+{
+	NAME			=> 'NAME'
+};
+
+################################################################################
+# Command line parameters
+################################################################################
+my $strPgSqlBin = '../../../../bin/bin';	# Path of PG binaries to use for
+											# this test
+my $strTestPath = '../../../../data';		# Path where testing will occur
+my $iDefaultPort = 6000;					# Default port to run Postgres on
+my $bHelp = false;							# Display help
+my $bQuiet = false;							# Supress output except for errors
+my $bNoCleanup = false;						# Cleanup database on exit
+
+GetOptions ('q|quiet' => \$bQuiet,
+			'no-cleanup' => \$bNoCleanup,
+			'help' => \$bHelp,
+			'pgsql-bin=s' => \$strPgSqlBin,
+			'test-path=s' => \$strTestPath)
+	or pod2usage(2);
+
+# Display version and exit if requested
+if ($bHelp)
+{
+	print 'pg_audit unit test\n\n';
+	pod2usage();
+
+	exit 0;
+}
+
+################################################################################
+# Global variables
+################################################################################
+my $hDb;					# Connection to Postgres
+my $strLogExpected = '';	# The expected log compared with grepping AUDIT
+							# entries from the postgres log.
+
+my $strDatabase = 'postgres';	# Connected database (modified by PgSetDatabase)
+my $strUser = 'postgres';		# Connected user (modified by PgSetUser)
+my $strAuditRole = 'audit';		# Role to use for auditing
+
+my %oAuditLogHash;				# Hash to store pg_audit.log GUCS
+my %oAuditGrantHash;			# Hash to store pg_audit grants
+
+my $strCurrentAuditLog;		# pg_audit.log setting was Postgres was started with
+my $strTemporaryAuditLog;	# pg_audit.log setting that was set hot
+
+################################################################################
+# Stores the mapping between commands, classes, and types
+################################################################################
+my %oCommandHash =
+(&COMMAND_ANALYZE => {
+	&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_AGGREGATE => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_AGGREGATE},
+	&COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_COLLATION},
+	&COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_CONVERSION},
+	&COMMAND_ALTER_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE_SET => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_ALTER_ROLE},
+	&COMMAND_ALTER_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_ALTER_TABLE_COLUMN => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_COLUMN, &COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_ALTER_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_BEGIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CLOSE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COMMIT => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COPY_FROM => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_COPY_TO => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_CREATE_AGGREGATE => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_AGGREGATE},
+	&COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_CONVERSION},
+	&COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_COLLATION},
+	&COMMAND_CREATE_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX},
+	&COMMAND_DEALLOCATE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_DECLARE_CURSOR => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_DO => {&CLASS => &CLASS_FUNCTION, &TYPE => &TYPE_NONE},
+	&COMMAND_DISCARD_ALL => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_FUNCTION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_FUNCTION},
+	&COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_SCHEMA},
+	&COMMAND_CREATE_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_CREATE_TABLE_AS => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_TABLE_CONSTRAINT => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_CONSTRAINT, &COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TOAST => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_TOAST, &COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TYPE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TYPE,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_EXECUTE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_FUNCTION => {&CLASS => &CLASS_FUNCTION,
+		&TYPE => &TYPE_FUNCTION, &COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXPLAIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_FETCH => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_GRANT => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_PREPARE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_PREPARE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_INSERT => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE},
+	&COMMAND_REVOKE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_SELECT => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_SET => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_UPDATE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE}
+);
+
+################################################################################
+# CommandExecute
+################################################################################
+sub CommandExecute
+{
+	my $strCommand = shift;
+	my $bSuppressError = shift;
+
+	# Set default
+	$bSuppressError = defined($bSuppressError) ? $bSuppressError : false;
+
+	# Run the command
+	my $iResult = system($strCommand);
+
+	if ($iResult != 0 && !$bSuppressError)
+	{
+		confess "command '${strCommand}' failed with error ${iResult}";
+	}
+}
+
+################################################################################
+# log
+################################################################################
+sub log
+{
+	my $strMessage = shift;
+	my $bError = shift;
+
+	# Set default
+	$bError = defined($bError) ? $bError : false;
+
+	if (!$bQuiet)
+	{
+		print "${strMessage}\n";
+	}
+
+	if ($bError)
+	{
+		exit 1;
+	}
+}
+
+################################################################################
+# ArrayToString
+################################################################################
+sub ArrayToString
+{
+	my @stryArray = @_;
+
+	my $strResult = '';
+
+	for (my $iIndex = 0; $iIndex < @stryArray; $iIndex++)
+	{
+		if ($iIndex != 0)
+		{
+			$strResult .= ', ';
+		}
+
+		$strResult .= $stryArray[$iIndex];
+	}
+
+	return $strResult;
+}
+
+################################################################################
+# BuildModule
+################################################################################
+sub BuildModule
+{
+	capture('cd ..;make');
+	CommandExecute("cp ../pg_audit.so" .
+				   " ${strPgSqlBin}/../lib/postgresql");
+	CommandExecute("cp ../pg_audit.control" .
+				   " ${strPgSqlBin}/../share/postgresql/extension");
+	CommandExecute("cp ../pg_audit--1.0.0.sql" .
+				   " ${strPgSqlBin}/../share/postgresql/extension");
+}
+
+################################################################################
+# PgConnect
+################################################################################
+sub PgConnect
+{
+	my $iPort = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+
+	# Log Connection
+	&log("   DB: connect user ${strUser}, database ${strDatabase}");
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# Connect to the db
+	$hDb = DBI->connect("dbi:Pg:dbname=${strDatabase};port=${iPort};host=/tmp",
+						$strUser, undef,
+						{AutoCommit => 1, RaiseError => 1});
+}
+
+################################################################################
+# PgDisconnect
+################################################################################
+sub PgDisconnect
+{
+	# Connect to the db (whether it is local or remote)
+	if (defined($hDb))
+	{
+		$hDb->disconnect;
+		undef($hDb);
+	}
+}
+
+################################################################################
+# PgExecute
+################################################################################
+sub PgExecute
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	my $hStatement = $hDb->prepare($strSql);
+
+	$hStatement->execute();
+	$hStatement->finish();
+}
+
+################################################################################
+# PgExecuteOnly
+################################################################################
+sub PgExecuteOnly
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	$hDb->do($strSql);
+}
+
+################################################################################
+# PgSetDatabase
+################################################################################
+sub PgSetDatabase
+{
+	my $strDatabaseParam = shift;
+
+	# Stop and start the database to reset pgconf entries
+	PgStop();
+	PgStart();
+
+	# Execute the statement
+	$strDatabase = $strDatabaseParam;
+	PgConnect();
+}
+
+################################################################################
+# PgSetUser
+################################################################################
+sub PgSetUser
+{
+	my $strUserParam = shift;
+
+	$strUser = $strUserParam;
+
+	# Stop and start the database to reset pgconf entries
+	if ((defined($strTemporaryAuditLog) && !defined($strCurrentAuditLog)) ||
+		(defined($strCurrentAuditLog) && !defined($strTemporaryAuditLog)) ||
+		$strCurrentAuditLog ne $strTemporaryAuditLog)
+	{
+		$strCurrentAuditLog = $strTemporaryAuditLog;
+
+		PgStop();
+		PgStart();
+	}
+	else
+	{
+		# Execute the statement
+		PgConnect();
+	}
+}
+
+################################################################################
+# SaveString
+################################################################################
+sub SaveString
+{
+	my $strFile = shift;
+	my $strString = shift;
+
+	# Open the file for writing
+	my $hFile;
+
+	open($hFile, '>', $strFile)
+		or confess "unable to open ${strFile}";
+
+	if ($strString ne '')
+	{
+		syswrite($hFile, $strString)
+			or confess "unable to write to ${strFile}: $!";
+	}
+
+	close($hFile);
+}
+
+################################################################################
+# PgLogExecute
+################################################################################
+sub PgLogExecute
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+	my $bExecute = shift;
+	my $bWait = shift;
+	my $bLogSql = shift;
+	my $strParameter = shift;
+	my $bExpectError = shift;
+
+	# Set defaults
+	$bExecute = defined($bExecute) ? $bExecute : true;
+	$bWait = defined($bWait) ? $bWait : true;
+	$bLogSql = defined($bLogSql) ? $bLogSql : true;
+
+	if ($bExecute)
+	{
+		eval
+		{
+			PgExecuteOnly($strSql);
+		};
+
+		if ($@ && !$bExpectError)
+		{
+			confess $@;
+		}
+	}
+
+	PgLogExpect($strCommand, $bLogSql ? $strSql : '', $strParameter, $oData);
+
+	if ($bWait)
+	{
+		PgLogWait();
+	}
+}
+
+################################################################################
+# QuoteCSV
+################################################################################
+sub QuoteCSV
+{
+	my $strCSV = shift;
+
+	if (defined($strCSV) &&
+		(index($strCSV, ',') >= 0 || index($strCSV, '"') > 0 ||
+		 index($strCSV, "\n") > 0 || index($strCSV, "\r") >= 0))
+	{
+		$strCSV =~ s/"/""/g;
+		$strCSV = "\"${strCSV}\"";
+	}
+
+	return $strCSV;
+}
+
+################################################################################
+# PgLogExpect
+################################################################################
+sub PgLogExpect
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $strParameter = shift;
+	my $oData = shift;
+
+	# If oData is false then no logging
+	if (defined($oData) && ref($oData) eq '' && !$oData)
+	{
+		return;
+	}
+
+	# Quote SQL if needs to be quoted
+	$strSql = QuoteCSV($strSql);
+
+	if (defined($strParameter))
+	{
+		$strSql .= ",${strParameter}";
+	}
+
+	# Log based on session
+	if (PgShouldLog($strCommand))
+	{
+		# Make sure class is defined
+		my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+		if (!defined($strClass))
+		{
+			confess "class is not defined for command ${strCommand}";
+		}
+
+		# Make sure object type is defined
+		my $strObjectType = $oCommandHash{$strCommand}{&TYPE};
+
+		if (!defined($strObjectType))
+		{
+			confess "object type is not defined for command ${strCommand}";
+		}
+
+		# Check for command override
+		my $strCommandLog = $strCommand;
+
+		if ($oCommandHash{$strCommand}{&COMMAND})
+		{
+			$strCommandLog = $oCommandHash{$strCommand}{&COMMAND};
+		}
+
+		my $strObjectName = '';
+
+		if (defined($oData) && ref($oData) ne 'ARRAY')
+		{
+			$strObjectName = QuoteCSV($oData);
+		}
+
+		my $strLog .= "SESSION,${strClass},${strCommandLog}," .
+					  "${strObjectType},${strObjectName},${strSql}";
+		&log("AUDIT: ${strLog}");
+
+		$strLogExpected .= "${strLog}\n";
+	}
+
+	# Log based on grants
+	if (ref($oData) eq 'ARRAY' && ($strCommand eq COMMAND_SELECT ||
+		$oCommandHash{$strCommand}{&CLASS} eq CLASS_WRITE))
+	{
+		foreach my $oTableHash (@{$oData})
+		{
+			my $strObjectName = QuoteCSV(${$oTableHash}{&NAME});
+			my $strCommandLog = ${$oTableHash}{&COMMAND};
+
+			if (defined($oAuditGrantHash{$strAuditRole}
+										{$strObjectName}{$strCommandLog}))
+			{
+				my $strCommandLog = defined(${$oTableHash}{&COMMAND_LOG}) ?
+					${$oTableHash}{&COMMAND_LOG} : $strCommandLog;
+				my $strClass = $oCommandHash{$strCommandLog}{&CLASS};
+				my $strObjectType = ${$oTableHash}{&TYPE};
+
+				my $strLog .= "OBJECT,${strClass},${strCommandLog}," .
+							  "${strObjectType},${strObjectName},${strSql}";
+				&log("AUDIT: ${strLog}");
+
+				$strLogExpected .= "${strLog}\n";
+			}
+		}
+
+		$oData = undef;
+	}
+}
+
+################################################################################
+# PgShouldLog
+################################################################################
+sub PgShouldLog
+{
+	my $strCommand = shift;
+
+	# Make sure class is defined
+	my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+	if (!defined($strClass))
+	{
+		confess "class is not defined for command ${strCommand}";
+	}
+
+	# Check logging for the role
+	my $bLog = undef;
+
+	if (defined($oAuditLogHash{&CONTEXT_ROLE}{$strUser}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_ROLE}{$strUser}{$strClass};
+	}
+
+	# Else check logging for the db
+	elsif (defined($oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}{$strClass};
+	}
+
+	# Else check logging for global
+	elsif (defined($oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}{$strClass};
+	}
+
+	return defined($bLog) ? true : false;
+}
+
+################################################################################
+# PgLogWait
+################################################################################
+sub PgLogWait
+{
+	my $strLogActual;
+
+	# Run in an eval block since grep returns 1 when nothing was found
+	eval
+	{
+		$strLogActual = capture("grep 'LOG:  AUDIT: '" .
+								" ${strTestPath}/postgresql.log");
+	};
+
+	# If an error was returned, continue if it was 1, otherwise confess
+	if ($@)
+	{
+		my $iExitStatus = $? >> 8;
+
+		if ($iExitStatus != 1)
+		{
+			confess "grep returned ${iExitStatus}";
+		}
+
+		$strLogActual = '';
+	}
+
+	# Strip the AUDIT and timestamp from the actual log
+	$strLogActual =~ s/prefix LOG:  AUDIT\: //g;
+	$strLogActual =~ s/SESSION,[0-9]+,[0-9]+,/SESSION,/g;
+	$strLogActual =~ s/OBJECT,[0-9]+,[0-9]+,/OBJECT,/g;
+
+	# Save the logs
+	SaveString("${strTestPath}/audit.actual", $strLogActual);
+	SaveString("${strTestPath}/audit.expected", $strLogExpected);
+
+	CommandExecute("diff ${strTestPath}/audit.expected" .
+				   " ${strTestPath}/audit.actual");
+}
+
+################################################################################
+# PgDrop
+################################################################################
+sub PgDrop
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Stop the cluster
+	PgStop(true, $strPath);
+
+	# Remove the directory
+	CommandExecute("rm -rf ${strTestPath}");
+}
+
+################################################################################
+# PgCreate
+################################################################################
+sub PgCreate
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	CommandExecute("${strPgSqlBin}/initdb -D ${strPath} -U ${strUser}" .
+				   ' -A trust > /dev/null');
+}
+
+################################################################################
+# PgStop
+################################################################################
+sub PgStop
+{
+	my $bImmediate = shift;
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+	$bImmediate = defined($bImmediate) ? $bImmediate : false;
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# If postmaster process is running then stop the cluster
+	if (-e $strPath . '/postmaster.pid')
+	{
+		CommandExecute("${strPgSqlBin}/pg_ctl stop -D ${strPath} -w -s -m " .
+					  ($bImmediate ? 'immediate' : 'fast'));
+	}
+}
+
+################################################################################
+# PgStart
+################################################################################
+sub PgStart
+{
+	my $iPort = shift;
+	my $strPath = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Make sure postgres is not running
+	if (-e $strPath . '/postmaster.pid')
+	{
+		confess "${strPath}/postmaster.pid exists, cannot start";
+	}
+
+	# Start the cluster
+	CommandExecute("${strPgSqlBin}/pg_ctl start -o \"" .
+				   "-c port=${iPort}" .
+				   " -c unix_socket_directories='/tmp'" .
+				   " -c shared_preload_libraries='pg_audit'" .
+				   " -c log_min_messages=debug1" .
+				   " -c log_line_prefix='prefix '" .
+				   " -c log_statement=all" .
+				   (defined($strCurrentAuditLog) ?
+					   " -c pg_audit.log='${strCurrentAuditLog}'" : '') .
+				   " -c pg_audit.role='${strAuditRole}'" .
+				   " -c log_connections=on" .
+				   "\" -D ${strPath} -l ${strPath}/postgresql.log -w -s");
+
+	# Connect user session
+	PgConnect();
+}
+
+################################################################################
+# PgAuditLogSet
+################################################################################
+sub PgAuditLogSet
+{
+	my $strContext = shift;
+	my $strName = shift;
+	my @stryClass = @_;
+
+	# Create SQL to set the GUC
+	my $strCommand;
+	my $strSql;
+
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		$strCommand = COMMAND_SET;
+		$strSql = "set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+		$strTemporaryAuditLog = ArrayToString(@stryClass);
+	}
+	elsif ($strContext eq CONTEXT_ROLE)
+	{
+		$strCommand = COMMAND_ALTER_ROLE_SET;
+		$strSql = "alter role ${strName} set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+	}
+	else
+	{
+		confess "unable to set pg_audit.log for context ${strContext}";
+	}
+
+	# Reset the audit log
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		delete($oAuditLogHash{$strContext});
+		$strName = CONTEXT_GLOBAL;
+	}
+	else
+	{
+		delete($oAuditLogHash{$strContext}{$strName});
+	}
+
+	# Store all the classes in the hash and build the GUC
+	foreach my $strClass (@stryClass)
+	{
+		if ($strClass eq CLASS_ALL)
+		{
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_DDL} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_FUNCTION} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_MISC} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_READ} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_WRITE} = true;
+		}
+
+		if (index($strClass, '-') == 0)
+		{
+			$strClass = substr($strClass, 1);
+
+			delete($oAuditLogHash{$strContext}{$strName}{$strClass});
+		}
+		else
+		{
+			$oAuditLogHash{$strContext}{$strName}{$strClass} = true;
+		}
+	}
+
+	PgLogExecute($strCommand, $strSql);
+}
+
+################################################################################
+# PgAuditGrantSet
+################################################################################
+sub PgAuditGrantSet
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_GRANT, "GRANT " .
+								(defined($strColumn) ?
+									lc(${strPrivilege}) ." (${strColumn})" :
+									uc(${strPrivilege})) .
+								" ON TABLE ${strObject} TO ${strRole} ");
+
+	$oAuditGrantHash{$strRole}{$strObject}{$strPrivilege} = true;
+}
+
+################################################################################
+# PgAuditGrantReset
+################################################################################
+sub PgAuditGrantReset
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_REVOKE, "REVOKE  " . uc(${strPrivilege}) .
+				 (defined($strColumn) ? " (${strColumn})" : '') .
+				 " ON TABLE ${strObject} FROM ${strRole} ");
+
+	delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege});
+}
+
+################################################################################
+# Main
+################################################################################
+my @oyTable;	   # Store table info for select, insert, update, delete
+my $strSql;		# Hold Sql commands
+
+# Drop the old cluster, build the code, and create a new cluster
+PgDrop();
+BuildModule();
+PgCreate();
+PgStart();
+
+PgExecute("create extension pg_audit");
+
+# Create test users and the audit role
+PgExecute("create user user1");
+PgExecute("create user user2");
+PgExecute("create role ${strAuditRole}");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL));
+
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_READ, CLASS_WRITE));
+
+# User1 follows the global log settings
+PgSetUser('user1');
+
+$strSql = 'CREATE  TABLE  public.test (id pg_catalog.int4   )' .
+		  '  WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+$strSql = 'drop table test';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'public.test');
+
+PgSetUser('user2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test2 (id int)', 'public.test2');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test3 (id int)', 'public.test2');
+
+# Catalog select should not log
+PgLogExecute(COMMAND_SELECT, 'select * from pg_class limit 1',
+							   false);
+
+# Multi-table select
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test3, test2',
+							   \@oyTable);
+
+# Various CTE combinations
+PgAuditGrantSet($strAuditRole, &COMMAND_INSERT, 'public.test3');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (select id from test2)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (insert into test3 values (1) returning id)' .
+			 ' insert into test2 select id from cte',
+			 \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE, 'public.test2');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (update test2 set id = 1 returning id)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_INSERT});
+PgLogExecute(COMMAND_UPDATE,
+			 'with cte as (insert into test2 values (1) returning id)' .
+			 ' update test3 set id = cte.id' .
+			 ' from cte where test3.id <> cte.id',
+			 \@oyTable);
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_NONE));
+PgSetUser('user2');
+
+# Column-based audits
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test4 (id int, name text)', 'public.test4');
+PgAuditGrantSet($strAuditRole, COMMAND_SELECT, 'public.test4', 'name');
+PgAuditGrantSet($strAuditRole, COMMAND_UPDATE, 'public.test4', 'id');
+PgAuditGrantSet($strAuditRole, COMMAND_INSERT, 'public.test4', 'name');
+
+# Select
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id from public.test4',
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from public.test4',
+							  \@oyTable);
+
+# Insert
+@oyTable = ();
+PgLogExecute(COMMAND_INSERT, 'insert into public.test4 (id) values (1)',
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT, "insert into public.test4 (name) values ('test')",
+							  \@oyTable);
+
+# Update
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set name = 'foo'",
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set id = 1",
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			&COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update public.test4 set name = 'foo' where name = 'bar'",
+			 \@oyTable);
+
+# Drop test tables
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test2", 'public.test2');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test3", 'public.test3');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test4", 'public.test4');
+
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Create some email friendly tests.  These first tests are session logging only.
+PgSetUser('postgres');
+
+&log("\nExamples:");
+
+&log("\nSession Audit:\n");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL, CLASS_READ));
+
+PgSetUser('user1');
+
+$strSql = 'CREATE  TABLE  public.account (id pg_catalog.int4   ,' .
+		  ' name pg_catalog.text   COLLATE pg_catalog."default", ' .
+		  'password pg_catalog.text   COLLATE pg_catalog."default", '.
+		  'description pg_catalog.text   COLLATE pg_catalog."default")  '.
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.account');
+PgLogExecute(COMMAND_SELECT,
+			 'select * from account');
+PgLogExecute(COMMAND_INSERT,
+			 "insert into account (id, name, password, description)" .
+			 " values (1, 'user1', 'HASH1', 'blah, blah')");
+&log("AUDIT: <nothing logged>");
+
+# Now tests for object logging
+&log("\nObject Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_NONE));
+PgExecute("set pg_audit.role = 'audit'");
+PgSetUser('user1');
+
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account', 'password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id, name from account',
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE,
+				'public.account', 'name, password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Now tests for session/object logging
+&log("\nSession/Object Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user1', (CLASS_READ, CLASS_WRITE));
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account_role_map (account_id int, role_id int)',
+			 'public.account_role_map');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account_role_map');
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.account_role_map', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT,
+			 'select account.password, account_role_map.role_id from account' .
+			 ' inner join account_role_map' .
+			 ' on account.id = account_role_map.account_id',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update account set description = 'yada, yada'" .
+			 " where password = 'HASH2'",
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Test all sql commands
+&log("\nExhaustive Command Tests:\n");
+
+PgSetUser('postgres');
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_ALL));
+PgLogExecute(COMMAND_SET, "set pg_audit.role = 'audit'");
+
+PgLogExecute(COMMAND_DO, "do \$\$\ begin raise notice 'test'; end; \$\$;");
+
+$strSql = 'CREATE SCHEMA  test ';
+PgLogExecute(COMMAND_CREATE_SCHEMA, $strSql, 'test');
+
+# Test COPY
+PgLogExecute(COMMAND_COPY_TO,
+			 "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'");
+
+$strSql = 'CREATE  TABLE  test.pg_class  WITH (oids=OFF)   AS SELECT relname,' .
+		  ' relnamespace, reltype, reloftype, relowner, relam, relfilenode, ' .
+		  'reltablespace, relpages, reltuples, relallvisible, reltoastrelid, ' .
+		  'relhasindex, relisshared, relpersistence, relkind, relnatts, ' .
+		  'relchecks, relhasoids, relhaspkey, relhasrules, relhastriggers, ' .
+		  'relhassubclass, relrowsecurity, relispopulated, relreplident, ' .
+		  'relfrozenxid, relminmxid, relacl, reloptions ' .
+		  'FROM pg_catalog.pg_class ';
+PgLogExecute(COMMAND_INSERT, $strSql, undef, true, false);
+PgLogExecute(COMMAND_CREATE_TABLE_AS, $strSql, 'test.pg_class', false, true);
+
+$strSql = "COPY test.pg_class from '" . abs_path($strTestPath) . "/class.out'";
+PgLogExecute(COMMAND_INSERT, $strSql);
+#PgLogExecute(COMMAND_COPY_FROM, $strSql, undef, false, true);
+
+# Test prepared SELECT
+PgLogExecute(COMMAND_PREPARE_READ,
+			 'PREPARE pgclassstmt (oid) as select *' .
+			 ' from pg_class where oid = $1');
+PgLogExecute(COMMAND_EXECUTE_READ,
+			 'EXECUTE pgclassstmt (1)');
+PgLogExecute(COMMAND_DEALLOCATE,
+			 'DEALLOCATE pgclassstmt');
+
+# Test cursor
+PgLogExecute(COMMAND_BEGIN,
+			 'BEGIN');
+PgLogExecute(COMMAND_DECLARE_CURSOR,
+			 'DECLARE ctest SCROLL CURSOR FOR SELECT * FROM pg_class');
+PgLogExecute(COMMAND_FETCH,
+			 'FETCH NEXT FROM ctest');
+PgLogExecute(COMMAND_CLOSE,
+			 'CLOSE ctest');
+PgLogExecute(COMMAND_COMMIT,
+			 'COMMIT');
+
+# Test prepared INSERT
+$strSql = 'CREATE  TABLE  test.test_insert (id pg_catalog.int4   )  ' .
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'test.test_insert');
+
+$strSql = 'PREPARE pgclassstmt (oid) as insert into test.test_insert (id) ' .
+		  'values ($1)';
+PgLogExecute(COMMAND_PREPARE_WRITE, $strSql);
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, "1");
+
+$strSql = 'EXECUTE pgclassstmt (1)';
+PgLogExecute(COMMAND_EXECUTE_WRITE, $strSql, undef, true, true);
+
+# Create a table with a primary key
+$strSql = 'CREATE  TABLE  public.test (id pg_catalog.int4   , ' .
+		  'name pg_catalog.text   COLLATE pg_catalog."default", description ' .
+		  'pg_catalog.text   COLLATE pg_catalog."default", CONSTRAINT ' .
+		  'test_pkey PRIMARY KEY (id))  WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_INDEX, $strSql, 'public.test_pkey', true, false);
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test', false, true);
+
+PgLogExecute(COMMAND_ANALYZE, 'analyze test');
+
+# Grant select to public - this should have no affect on auditing
+$strSql = 'GRANT SELECT ON TABLE public.test TO PUBLIC ';
+PgLogExecute(COMMAND_GRANT, $strSql);
+
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+# Now grant select to audit and it should be logged
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test', \@oyTable);
+
+# Check columns granted to public and make sure they do not log
+PgAuditGrantReset($strAuditRole, &COMMAND_SELECT, 'public.test');
+
+$strSql = 'GRANT select (name) ON TABLE public.test TO PUBLIC ';
+PgLogExecute(COMMAND_GRANT, $strSql);
+
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+PgLogExecute(COMMAND_SELECT, 'select from test');
+
+# Try a select that does not reference any tables
+PgLogExecute(COMMAND_SELECT, 'select 1, current_timestamp');
+
+# Now try the same in a do block
+$strSql = 'do $$ declare test int; begin select 1 into test; end $$';
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'select 1';
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, true);
+
+# Insert some data into test and try a loop in a do block
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (1)');
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (2)');
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (3)');
+
+$strSql = 'do $$ ' .
+		  'declare ' .
+		  '	result record;' .
+		  'begin ' .
+		  '	for result in select id from test loop ' .
+		  '		insert into test (id) values (result.id + 100); ' .
+		  '	end loop; ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'select id from test';
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false);
+
+$strSql = 'insert into test (id) values (result.id + 100)';
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+# Test EXECUTE with bind
+$strSql = "select * from test where id = ?";
+my $hStatement = $hDb->prepare($strSql);
+
+$strSql = "select * from test where id = \$1";
+$hStatement->bind_param(1, 101);
+$hStatement->execute();
+
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false, undef, "101");
+
+$hStatement->bind_param(1, 103);
+$hStatement->execute();
+
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false, undef, "103");
+
+$hStatement->finish();
+
+# Now try some DDL in a do block
+$strSql = 'do $$ ' .
+		  'begin ' .
+		  '	create table test_block (id int); ' .
+		  '	drop table test_block; ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'CREATE  TABLE  public.test_block (id pg_catalog.int4   )  ' .
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test_block', false, false);
+
+$strSql = 'drop table test_block';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'public.test_block', false, false);
+
+# Generate an error in a do block and make sure the stack gets cleaned up
+$strSql = 'do $$ ' .
+		  'begin ' .
+		  '	create table bobus.test_block (id int); ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, undef, undef, undef, undef, true);
+# PgLogExecute(COMMAND_SELECT, 'select 1');
+# exit 0;
+
+# Try explain
+PgLogExecute(COMMAND_SELECT, 'explain select 1', undef, true, false);
+PgLogExecute(COMMAND_EXPLAIN, 'explain select 1', undef, false, true);
+
+# Now set grant to a specific column to audit and make sure it logs
+# Make sure the the converse is true
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test',
+				'name, description');
+PgLogExecute(COMMAND_SELECT, 'select id from test');
+
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from test', \@oyTable);
+
+# Test alter and drop table statements
+$strSql = 'ALTER TABLE public.test DROP COLUMN description ';
+PgLogExecute(COMMAND_ALTER_TABLE_COLUMN,
+			 $strSql, 'public.test.description', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 $strSql, 'public.test', false, true);
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable);
+
+$strSql = 'ALTER TABLE  public.test RENAME TO test2';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'public.test2');
+
+$strSql = 'ALTER TABLE public.test2 SET SCHEMA test';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'test.test2');
+
+$strSql = 'ALTER TABLE test.test2 ADD COLUMN description pg_catalog.text   ' .
+		  'COLLATE pg_catalog."default"';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'test.test2');
+
+$strSql = 'ALTER TABLE test.test2 DROP COLUMN description ';
+PgLogExecute(COMMAND_ALTER_TABLE_COLUMN, $strSql,
+			 'test.test2.description', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql,
+			 'test.test2', false, true);
+
+$strSql = 'drop table test.test2';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'test.test2', true, false);
+PgLogExecute(COMMAND_DROP_TABLE_CONSTRAINT, $strSql, 'test_pkey on test.test2',
+			 false, false);
+PgLogExecute(COMMAND_DROP_TABLE_INDEX, $strSql, 'test.test_pkey', false, true);
+
+$strSql = "CREATE  FUNCTION public.int_add(IN a pg_catalog.int4 , IN b " .
+		  "pg_catalog.int4 ) RETURNS  pg_catalog.int4 LANGUAGE plpgsql  " .
+		  "VOLATILE  CALLED ON NULL INPUT SECURITY INVOKER COST 100   AS '" .
+		  " begin return a + b; end '";
+PgLogExecute(COMMAND_CREATE_FUNCTION, $strSql,
+			 'public.int_add(integer,integer)');
+PgLogExecute(COMMAND_SELECT, "select int_add(1, 1)",
+							 undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_FUNCTION, "select int_add(1, 1)",
+									   'public.int_add', false, true);
+
+$strSql = "CREATE AGGREGATE public.sum_test(  pg_catalog.int4) " .
+		  "(SFUNC=public.int_add, STYPE=pg_catalog.int4, INITCOND='0')";
+PgLogExecute(COMMAND_CREATE_AGGREGATE, $strSql, 'public.sum_test(integer)');
+
+# There's a bug here in deparse:
+$strSql = "ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2";
+PgLogExecute(COMMAND_ALTER_AGGREGATE, $strSql, 'public.sum_test2(integer)');
+
+$strSql = "CREATE COLLATION public.collation_test (LC_COLLATE = 'de_DE', " .
+		  "LC_CTYPE = 'de_DE')";
+PgLogExecute(COMMAND_CREATE_COLLATION, $strSql, 'public.collation_test');
+
+$strSql =  "ALTER COLLATION public.collation_test RENAME TO collation_test2";
+PgLogExecute(COMMAND_ALTER_COLLATION, $strSql, 'public.collation_test2');
+
+$strSql = "CREATE  CONVERSION public.conversion_test FOR 'SQL_ASCII' " .
+		  "TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic";
+PgLogExecute(COMMAND_CREATE_CONVERSION, $strSql, 'public.conversion_test');
+
+$strSql = "ALTER CONVERSION public.conversion_test RENAME TO conversion_test2";
+PgLogExecute(COMMAND_ALTER_CONVERSION, $strSql, 'public.conversion_test2');
+
+PgLogExecute(COMMAND_CREATE_DATABASE, "CREATE DATABASE database_test");
+PgLogExecute(COMMAND_ALTER_DATABASE,
+			 "ALTER DATABASE database_test rename to database_test2");
+PgLogExecute(COMMAND_DROP_DATABASE, "DROP DATABASE database_test2");
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Stop the database
+if (!$bNoCleanup)
+{
+	PgDrop();
+}
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index a698d0f..5b247a9 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 89fff77..6b0b407 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -125,6 +125,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbench         SYSTEM "pgbench.sgml">
 <!ENTITY pgarchivecleanup SYSTEM "pgarchivecleanup.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..f9152cd
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,335 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> module provides session and object
+    auditing via the standard logging facility.  Session and object auditing are
+    completely independent and can be combined.
+  </para>
+
+  <sect2>
+    <title>Session Auditing</title>
+
+    <para>
+      Session auditing allows the logging of all commands that are executed by
+      a user in the backend.  Each command is logged with a single entry and
+      includes the audit type (e.g. <literal>SESSION</literal>), command type
+      (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and
+      statement (e.g. <literal>"select * from test"</literal>).
+
+      Fully-qualified names and object types will be logged for
+      <literal>CREATE</literal>, <literal>UPDATE</literal>, and
+      <literal>DROP</literal> commands on <literal>TABLE</literal>,
+      <literal>MATVIEW</literal>, <literal>VIEW</literal>,
+      <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,
+      <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and
+      <literal>SEQUENCE</literal> objects as well as function calls.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is controlled by the <literal>pg_audit.log</literal>
+        GUC. There are six classes of commands that are recognized:
+
+        <itemizedlist>
+          <listitem>
+            <para>
+              <literal>READ</literal> - <literal>SELECT</literal> and
+              <literal>COPY</literal> when the source is a table or query.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>WRITE</literal> - <literal>INSERT</literal>,
+              <literal>UPDATE</literal>, <literal>DELETE</literal>,
+              <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+              destination is a table.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>FUNCTION</literal> - Function calls and
+              <literal>DO</literal> blocks.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>DDL</literal> - DDL, plus <literal>VACUUM</literal>,
+              <literal>REINDEX</literal>, and <literal>ANALYZE</literal>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>PARAMETER</literal> - Parameters that were passed for the statement.  Parameters immediately follow the statement text. 
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>MISC</literal> - Miscellaneous commands, e.g.
+              <literal>DISCARD</literal>, <literal>FETCH</literal>,
+              <literal>CHECKPOINT</literal>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </para>
+
+      <para>
+        Enable session logging for all writes and DDL:
+          <programlisting>
+pg_audit.log = 'write, ddl'
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except miscellaneous:
+          <programlisting>
+pg_audit.log = 'all, -misc'
+          </programlisting>
+      </para>
+      
+      <para>
+      Note that <literal>pg_audit.log</literal> can be set globally (in 
+      <filename>postgresql.conf</filename>), at the database level (using
+      <literal>alter database ... set</literal>), or at the role level (using
+      <literal>alter role ... set</literal>).
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.log = 'read, ddl'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+      <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+select *
+    from account;
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Object Auditing</title>
+
+    <para>
+      Object auditing logs commands that affect a particular object.  Only
+      <literal>SELECT</literal>, <literal>INSERT</literal>,
+      <literal>UPDATE</literal> and <literal>DELETE</literal> commands are
+      supported.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level auditing is implemented via the roles system.  The
+        <literal>pg_audit.role</literal> GUC defines the role that will be used
+        for auditing.  An object will be audited when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.
+      </para>
+
+      <programlisting>
+postresql.conf: pg_audit.role = 'audit'
+
+grant select, delete
+   on public.account;
+      </programlisting>
+
+      <para>
+      Note that <literal>pg_audit.role</literal> can be set globally (in 
+      <filename>postgresql.conf</filename>), at the database level (using
+      <literal>alter database ... set</literal>), or at the role level (using
+      <literal>alter role ... set</literal>).
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.role = 'audit'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to audit;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to audit;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to audit;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is not in compliant CSV format.  If machine-readability is
+          required then consider setting
+          <literal>log_destination = 'csvlog'</literal>.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</literal> - <literal>SESSION</literal> or
+            <literal>OBJECT</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</literal> - <literal>READ</literal>,
+            <literal>WRITE</literal>, <literal>FUNCTION</literal>,
+            <literal>DDL</literal>, or <literal>MISC</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</literal> - <literal>ALTER TABLE</literal>,
+            <literal>SELECT</literal>, <literal>CREATE INDEX</literal>,
+            <literal>UPDATE</literal>, etc.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</literal> - <literal>TABLE</literal>,
+            <literal>INDEX</literal>, <literal>VIEW</literal>, etc.  Only
+            available for DML and certain DDL commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</literal> - The fully-qualified object name
+            (e.g. public.account).  Only available for DML and certain DDL
+            commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</literal> - Statement execute on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#9Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#8)
Re: Auditing extension for PostgreSQL (Take 2)

On Tue, Mar 24, 2015 at 1:40 AM, David Steele <david@pgmasters.net> wrote:

Thanks for the review, Abhijit.

On 3/23/15 1:31 AM, Abhijit Menon-Sen wrote:

At 2015-02-24 11:22:41 -0500, david@pgmasters.net wrote:

Patch v3 is attached.
+
+    /* Function execution */
+    LOG_MISC = (1 << 5),

The comment above LOG_MISC should be changed.

Fixed.

More fundamentally, this classification makes it easy to reuse LOGSTMT_*
(and a nice job you've done of that, with just a few additional special
cases), I don't think this level is quite enough for our needs. I think
it should at least be possible to specifically log commands that affect
privileges and roles.

I agree, but this turns out to be easier said than done. In the prior
code for instance, CREATE ROLE was classified as USER, while ALTER ROLE
.. RENAME was classified as DDL. This is because any rename gets the
command tag T_RenameStmt. CreateCommandTag does return "ALTER ROLE",
but now we're in the realm of string-matching again which is not my
favorite thing. Let me see if there is a clean way to get this
accomplished. I've also felt this is the one thing I'd like to see
broken out.

I'm fond of finer categorisation for DDL as well, but I could live with
all DDL being lumped together.

I'm experimenting with a few approaches to do this without reintroducing
switch statements to test every command. That will require core changes,
but I think we can find an acceptable arrangement. I'll post a proof of
concept in a few days.

I also think finer-grained categorization would be best accomplished
with some core changes. It seemed too late to get those in for 9.5 so I
decided to proceed with what I knew could be done reliably with the idea
to improve it with core changes going forward.

I look forward to your proof-of-concept.

+ * Takes an AuditEvent and, if it log_check(), writes it to the audit
log.

I don't think log_check is the most useful name, because this sentence
doesn't tell me what the function may do. Similarly, I would prefer to
have log_acl_check be renamed acl_grants_audit or similar. (These are
all static functions anyway, I don't think a log_ prefix is needed.)

log_check() has become somewhat vestigial at this point since it is only
called from one place - I've been considering removing it and merging
into log_audit_event(). For the moment I've improved the comments.

I like acl_grants_audit() and agree that it's a clearer name. I'll
incorporate that into the next version and apply the same scheme to the
other ACL functionsas well as do a general review of naming.

+ /* Free the column set */
+ bms_free(tmpSet);

(An aside, really: there are lots of comments like this, which I don't
think add anything to understanding the code, and should be removed.)

I generally feel like you can't have too many comments. I think even
the less interesting/helpful comments help break the code into
functional sections for readability.

+            /*
+             * We don't have access to the parsetree here, so we have to generate
+             * the node type, object type, and command tag by decoding
+             * rte->requiredPerms and rte->relkind.
+             */
+            auditEvent.logStmtLevel = LOGSTMT_MOD;

(I am also trying to find a way to avoid having to do this.)

That would be excellent.

+            /* Set object type based on relkind */
+            switch (class->relkind)
+            {
+                    case RELKIND_RELATION:
+                            utilityAuditEvent.objectType = OBJECT_TYPE_TABLE;
+                            break;

This occurs elsewhere too. But I suppose new relkinds are added less
frequently than new commands.

Well, that's the hope at least. I should mention that ALL statements
will be logged no matter what additional classification happens. The
amount of information returned may not be ideal, but nothing is ever
excluded from logging (depending on the classes selected, of course).

Again on a larger level, I'm not sure how I feel about _avoiding_ the
use of event triggers for audit logging. Regardless of whether we use
the deparse code (which I personally think is a good idea; Álvaro has
been working on it, and it looks very nice) to log extra information,
using the object access hook inevitably means we have to reimplement
the identification/classification code here.

In "old" pgaudit, I think that extra effort is justified by the need to
be backwards compatible with pre-event trigger releases. In a 9.5-only
version, I am not at all convinced that this makes sense.

Thoughts?

I was nervous about basing pg_audit on code that I wasn't sure would be
committed (at the time). Since pg_event_trigger_get_creation_commands()
is tied up with deparse, I honestly didn't feel like the triggers were
bringing much to the table.

That being said, I agree that the deparse code is very useful and now
looks certain to be committed for 9.5.

I have prepared a patch that brings event triggers and deparse back to
pg_audit based on the Alvaro's dev/deparse branch at
git://git.postgresql.org/git/2ndquadrant_bdr.git (commit 0447fc5). I've
updated the unit tests accordingly.

I left in the OAT code for now. It does add detail to one event that
the event triggers do not handle (creating PK indexes) and I feel that
it lends an element of safety in case something happens to the event
triggers. OAT is also required for function calls so the code path
cannot be eliminated entirely. I'm not committed to keeping the
redundant OAT code, but I'd rather not remove it until deparse is
committed to master.

I've been hesitant to post this patch as it will not work in master
(though it will compile), but I don't want to hold on to it any longer
since the end of the CF is nominally just weeks away. If you want to
run the patch in master, you'll need to disable the
pg_audit_ddl_command_end trigger.

I've also addressed Fujii's concerns about logging parameters - this is
now an option. The event stack has been formalized and
MemoryContextRegisterResetCallback() is used to cleanup the stack on errors.

Let me know what you think.

Hi,

I tied to look into latest patch, but got following error.

masahiko [pg_audit] $ LANG=C make
gcc -Wall -Wmissing-prototypes -Wpointer-arith
-Wdeclaration-after-statement -Wendif-labels
-Wmissing-format-attribute -Wformat-security -fno-strict-aliasing
-fwrapv -g -fpic -I. -I. -I../../src/include -D_GNU_SOURCE -c -o
pg_audit.o pg_audit.c
pg_audit.c: In function 'log_audit_event':
pg_audit.c:456: warning: ISO C90 forbids mixed declarations and code
pg_audit.c: In function 'pg_audit_ddl_command_end':
pg_audit.c:1436: error: 'pg_event_trigger_expand_command' undeclared
(first use in this function)
pg_audit.c:1436: error: (Each undeclared identifier is reported only once
pg_audit.c:1436: error: for each function it appears in.)
make: *** [pg_audit.o] Error 1

Am I missing something?

Regards,

-------
Sawada Masahiko

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

#10Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Sawada Masahiko (#9)
Re: Auditing extension for PostgreSQL (Take 2)

Sawada Masahiko wrote:

I tied to look into latest patch, but got following error.

masahiko [pg_audit] $ LANG=C make
gcc -Wall -Wmissing-prototypes -Wpointer-arith
-Wdeclaration-after-statement -Wendif-labels
-Wmissing-format-attribute -Wformat-security -fno-strict-aliasing
-fwrapv -g -fpic -I. -I. -I../../src/include -D_GNU_SOURCE -c -o
pg_audit.o pg_audit.c
pg_audit.c: In function 'log_audit_event':
pg_audit.c:456: warning: ISO C90 forbids mixed declarations and code
pg_audit.c: In function 'pg_audit_ddl_command_end':
pg_audit.c:1436: error: 'pg_event_trigger_expand_command' undeclared
(first use in this function)

You need to apply my deparsing patch first, last version of which I
posted here:
/messages/by-id/20150316234406.GH3636@alvh.no-ip.org

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, 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

#11David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#9)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

On 3/23/15 1:39 PM, Sawada Masahiko wrote:

On Tue, Mar 24, 2015 at 1:40 AM, David Steele <david@pgmasters.net> wrote:

I have prepared a patch that brings event triggers and deparse back to
pg_audit based on the Alvaro's dev/deparse branch at
git://git.postgresql.org/git/2ndquadrant_bdr.git (commit 0447fc5). I've
updated the unit tests accordingly.

I've been hesitant to post this patch as it will not work in master
(though it will compile), but I don't want to hold on to it any longer
since the end of the CF is nominally just weeks away. If you want to
run the patch in master, you'll need to disable the
pg_audit_ddl_command_end trigger.

Hi,

I tied to look into latest patch, but got following error.

masahiko [pg_audit] $ LANG=C make
gcc -Wall -Wmissing-prototypes -Wpointer-arith
-Wdeclaration-after-statement -Wendif-labels
-Wmissing-format-attribute -Wformat-security -fno-strict-aliasing
-fwrapv -g -fpic -I. -I. -I../../src/include -D_GNU_SOURCE -c -o
pg_audit.o pg_audit.c
pg_audit.c: In function 'log_audit_event':
pg_audit.c:456: warning: ISO C90 forbids mixed declarations and code
pg_audit.c: In function 'pg_audit_ddl_command_end':
pg_audit.c:1436: error: 'pg_event_trigger_expand_command' undeclared
(first use in this function)
pg_audit.c:1436: error: (Each undeclared identifier is reported only once
pg_audit.c:1436: error: for each function it appears in.)
make: *** [pg_audit.o] Error 1

Am I missing something?

It's my mistake. I indicated that this would compile under master - but
that turns out not to be true because of this function. It will only
compile cleanly in Alvaro's branch mentioned above.

My apologies - this is why I have been hesitant to post this patch
before. You are welcome to try with Alvaro's deparse branch or wait
until it has been committed to master.

I've attached patch v5 only to cleanup the warnings you saw.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v5.patchtext/plain; charset=UTF-8; name=pg_audit-v5.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index 195d447..d8e75f4 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -29,6 +29,7 @@ SUBDIRS = \
 		pageinspect	\
 		passwordcheck	\
 		pg_archivecleanup \
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..32bc6d9
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,20 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..65c8ed2
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1712 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pgaudit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pgaudit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pgaudit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_PARAMETER		"PARAMETER"
+#define CLASS_READ			"READ"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* Function execution */
+	LOG_PARAMETER = (1 << 4),
+
+	/* SELECT */
+	LOG_READ = (1 << 5),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 6),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static uint64 statementTotal = 0;
+static uint64 substatementTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack the the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop()
+{
+	/* Error if the stack is already empty */
+	if (auditEventStack == NULL)
+		elog(ERROR, "pg_audit stack is already empty");
+
+	/* Switch the old memory context and delete the audit context */
+	MemoryContextDelete(auditEventStack->contextAudit);
+}
+
+/*
+ * Takes an AuditEvent and returns true or false depending on whether the event
+ * should be logged according to the pgaudit.roles/log settings. If it returns
+ * true, also fills in the name of the LogClass which it is logged under.
+ */
+static bool
+log_check(AuditEvent *e, const char **classname)
+{
+	enum LogClass class = LOG_NONE;
+
+	/* By default put everything in the MISC class. */
+	*classname = CLASS_MISC;
+	class = LOG_MISC;
+
+	/*
+	 * Look at the type of the command and decide what LogClass needs to be
+	 * enabled for the command to be logged.
+	 */
+	switch (e->logStmtLevel)
+	{
+		case LOGSTMT_MOD:
+			*classname = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		case LOGSTMT_DDL:
+			*classname = CLASS_DDL;
+			class = LOG_DDL;
+
+		case LOGSTMT_ALL:
+			switch (e->commandTag)
+			{
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					*classname = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				case T_VacuumStmt:
+				case T_ReindexStmt:
+					*classname = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				case T_DoStmt:
+					*classname = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * We log audit events under the following conditions:
+	 *
+	 * 1. If the audit role has been explicitly granted permission for
+	 *    an operation.
+	 */
+	if (e->granted)
+	{
+		return true;
+	}
+
+	/* 2. If the event belongs to a class covered by pgaudit.log. */
+	if ((auditLogBitmap & class) == class)
+	{
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent and, if it log_check(), writes it to the audit log. The
+ * AuditEvent is assumed to be completely filled in by the caller (unknown
+ * values must be set to "" so that they can be logged without error checking).
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	const char *classname;
+	MemoryContext contextOld;
+	StringInfoData auditStr;
+
+	/* Check that this event should be logged. */
+	if (!log_check(&stackItem->auditEvent, &classname))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogBitmap & LOG_PARAMETER &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	ereport(LOG,
+		(errmsg("AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			classname, auditStr.data),
+		 errhidestmt(true)));
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+log_acl_check(Datum aclDatum, Oid auditOid, AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+log_relation_check(Oid relOid,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = log_acl_check(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+log_attribute_check(Oid relOid,
+					AttrNumber attNum,
+					Oid auditOid,
+					AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = log_acl_check(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+log_attribute_check_any(Oid relOid,
+						Oid auditOid,
+						Bitmapset *attributeSet,
+						AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (log_attribute_check(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			log_attribute_check(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/* If this is the first rte then session log */
+		if (first)
+		{
+			auditEventStack->auditEvent.objectName = "";
+			auditEventStack->auditEvent.objectType = "";
+
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (log_relation_check(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						log_attribute_check_any(relOid, auditOid,
+												rte->selectedCols,
+												ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEventStack->auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEventStack->auditEvent.granted =
+							log_attribute_check_any(relOid, auditOid,
+													rte->modifiedCols,
+													auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Only do relation level logging if a grant was found. */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop();
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute */
+		case OAT_FUNCTION_EXECUTE:
+			if (auditLogBitmap & LOG_FUNCTION)
+				log_function_execute(objectId);
+			break;
+
+		/* Log create */
+		case OAT_POST_CREATE:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter */
+		case OAT_POST_ALTER:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop */
+		case OAT_DROP:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessDrop *drop = arg;
+
+				if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+static ExecutorEnd_hook_type next_ExecutorEnd_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pgaudit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pgaudit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ExecutorEnd to pop statement audit event off the stack.
+ */
+static void
+pgaudit_ExecutorEnd_hook(QueryDesc *queryDesc)
+{
+	/* Call the next hook or standard function */
+	if (next_ExecutorEnd_hook)
+		next_ExecutorEnd_hook(queryDesc);
+	else
+		standard_ExecutorEnd(queryDesc);
+
+	/* Pop the audit event off the stack */
+	if (!internalStatement)
+	{
+		stack_pop();
+	}
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pgaudit_ProcessUtility_hook(Node *parsetree,
+							const char *queryString,
+							ProcessUtilityContext context,
+							ParamListInfo params,
+							DestReceiver *dest,
+							char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	/* Allocate the audit event */
+	if (!IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap != 0 &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/* Process the audit event if there is one. */
+	if (stackItem != NULL)
+	{
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !stackItem->auditEvent.logged &&
+			!IsAbortedTransactionBlockState())
+			log_audit_event(stackItem);
+
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			while (auditEventStack != NULL)
+				stack_pop();
+		}
+		else
+			stack_pop();
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pgaudit_object_access_hook(ObjectAccessType access,
+						   Oid classId,
+						   Oid objectId,
+						   int subId,
+						   void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&
+		auditLogBitmap & (LOG_DDL | LOG_FUNCTION))
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+	/* Continue only if session logging is enabled */
+	if (auditLogBitmap != LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pgaudit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type), schema,\n"
+				"       identity, command\n"
+				"  FROM pg_event_trigger_get_creation_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			bool	   isNull;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply addition data to current audit event */
+			auditEventStack->auditEvent.logStmtLevel =
+				GetCommandLogLevel(eventData->parsetree);
+			auditEventStack->auditEvent.commandTag =
+				nodeTag(eventData->parsetree);
+			auditEventStack->auditEvent.command =
+				CreateCommandTag(eventData->parsetree);
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 6);
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			auditEventStack->auditEvent.commandText =
+				TextDatumGetCString(
+					DirectFunctionCall1(pg_event_trigger_expand_command,
+										SPI_getbinval(spiTuple, spiTupDesc,
+													  7, &isNull)));
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pgaudit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type),\n"
+				"       schema_name, object_name, object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			char *schemaName;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			schemaName = SPI_getvalue(spiTuple, spiTupDesc, 5);
+
+			if (!(pg_strcasecmp(auditEventStack->auditEvent.objectType,
+							"TYPE") == 0 ||
+				  pg_strcasecmp(schemaName, "pg_toast") == 0))
+			{
+				auditEventStack->auditEvent.objectName =
+						SPI_getvalue(spiTuple, spiTupDesc, 7);
+
+				log_audit_event(auditEventStack);
+			}
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pgaudit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then then token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_PARAMETER) == 0)
+			class = LOG_PARAMETER;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pgaudit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pgaudit_log from extra (ignoring newval, which has already been converted
+ * to a bitmap above). Note that extra may not be set if the assignment is to be
+ * suppressed.
+ */
+static void
+assign_pgaudit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * This variable defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * This variables controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pgaudit_log,
+							   assign_pgaudit_log,
+							   NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pgaudit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pgaudit_ExecutorCheckPerms_hook;
+
+	next_ExecutorEnd_hook = ExecutorEnd_hook;
+	ExecutorEnd_hook = pgaudit_ExecutorEnd_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pgaudit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pgaudit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.pl
new file mode 100755
index 0000000..f5cbde2
--- /dev/null
+++ b/contrib/pg_audit/test/test.pl
@@ -0,0 +1,1412 @@
+#!/usr/bin/perl
+################################################################################
+# test.pl - pg_audit Unit Tests
+################################################################################
+
+################################################################################
+# Perl includes
+################################################################################
+use strict;
+use warnings;
+use Carp;
+
+use Getopt::Long;
+use Pod::Usage;
+use DBI;
+use Cwd qw(abs_path);
+use IPC::System::Simple qw(capture);
+
+################################################################################
+# Constants
+################################################################################
+use constant
+{
+	true  => 1,
+	false => 0
+};
+
+use constant
+{
+	CONTEXT_GLOBAL   => 'GLOBAL',
+	CONTEXT_DATABASE => 'DATABASE',
+	CONTEXT_ROLE	 => 'ROLE'
+};
+
+use constant
+{
+	CLASS			=> 'CLASS',
+
+	CLASS_DDL		=> 'DDL',
+	CLASS_FUNCTION	=> 'FUNCTION',
+	CLASS_MISC		=> 'MISC',
+	CLASS_PARAMETER => 'PARAMETER',
+	CLASS_READ		=> 'READ',
+	CLASS_WRITE		=> 'WRITE',
+
+	CLASS_ALL		=> 'ALL',
+	CLASS_NONE		=> 'NONE'
+};
+
+use constant
+{
+	COMMAND						=> 'COMMAND',
+	COMMAND_LOG					=> 'COMMAND_LOG',
+
+	COMMAND_ANALYZE					=> 'ANALYZE',
+	COMMAND_ALTER_AGGREGATE			=> 'ALTER AGGREGATE',
+	COMMAND_ALTER_COLLATION			=> 'ALTER COLLATION',
+	COMMAND_ALTER_CONVERSION		=> 'ALTER CONVERSION',
+	COMMAND_ALTER_DATABASE			=> 'ALTER DATABASE',
+	COMMAND_ALTER_ROLE				=> 'ALTER ROLE',
+	COMMAND_ALTER_ROLE_SET			=> 'ALTER ROLE SET',
+	COMMAND_ALTER_TABLE				=> 'ALTER TABLE',
+	COMMAND_ALTER_TABLE_COLUMN		=> 'ALTER TABLE COLUMN',
+	COMMAND_ALTER_TABLE_INDEX		=> 'ALTER TABLE INDEX',
+	COMMAND_BEGIN					=> 'BEGIN',
+	COMMAND_CLOSE					=> 'CLOSE CURSOR',
+	COMMAND_COMMIT					=> 'COMMIT',
+	COMMAND_COPY					=> 'COPY',
+	COMMAND_COPY_TO					=> 'COPY TO',
+	COMMAND_COPY_FROM				=> 'COPY FROM',
+	COMMAND_CREATE_AGGREGATE		=> 'CREATE AGGREGATE',
+	COMMAND_CREATE_COLLATION		=> 'CREATE COLLATION',
+	COMMAND_CREATE_CONVERSION		=> 'CREATE CONVERSION',
+	COMMAND_CREATE_DATABASE			=> 'CREATE DATABASE',
+	COMMAND_CREATE_INDEX			=> 'CREATE INDEX',
+	COMMAND_DEALLOCATE				=> 'DEALLOCATE',
+	COMMAND_DECLARE_CURSOR			=> 'DECLARE CURSOR',
+	COMMAND_DO						=> 'DO',
+	COMMAND_DISCARD_ALL				=> 'DISCARD ALL',
+	COMMAND_CREATE_FUNCTION			=> 'CREATE FUNCTION',
+	COMMAND_CREATE_ROLE				=> 'CREATE ROLE',
+	COMMAND_CREATE_SCHEMA			=> 'CREATE SCHEMA',
+	COMMAND_CREATE_TABLE			=> 'CREATE TABLE',
+	COMMAND_CREATE_TABLE_AS			=> 'CREATE TABLE AS',
+	COMMAND_DROP_DATABASE			=> 'DROP DATABASE',
+	COMMAND_DROP_SCHEMA				=> 'DROP SCHEMA',
+	COMMAND_DROP_TABLE				=> 'DROP TABLE',
+	COMMAND_DROP_TABLE_CONSTRAINT	=> 'DROP TABLE CONSTRAINT',
+	COMMAND_DROP_TABLE_INDEX		=> 'DROP TABLE INDEX',
+	COMMAND_DROP_TABLE_TOAST		=> 'DROP TABLE TOAST',
+	COMMAND_DROP_TABLE_TYPE			=> 'DROP TABLE TYPE',
+	COMMAND_EXECUTE					=> 'EXECUTE',
+	COMMAND_EXECUTE_READ			=> 'EXECUTE READ',
+	COMMAND_EXECUTE_WRITE			=> 'EXECUTE WRITE',
+	COMMAND_EXECUTE_FUNCTION		=> 'EXECUTE FUNCTION',
+	COMMAND_EXPLAIN					=> 'EXPLAIN',
+	COMMAND_FETCH					=> 'FETCH',
+	COMMAND_GRANT					=> 'GRANT',
+	COMMAND_INSERT					=> 'INSERT',
+	# COMMAND_PARAMETER				=> 'PARAMETER',
+	# COMMAND_PARAMETER_READ			=> 'PARAMETER_READ',
+	# COMMAND_PARAMETER_WRITE			=> 'PARAMETER_WRITE',
+	COMMAND_PREPARE					=> 'PREPARE',
+	COMMAND_PREPARE_READ			=> 'PREPARE READ',
+	COMMAND_PREPARE_WRITE			=> 'PREPARE WRITE',
+	COMMAND_REVOKE					=> 'REVOKE',
+	COMMAND_SELECT					=> 'SELECT',
+	COMMAND_SET						=> 'SET',
+	COMMAND_UPDATE					=> 'UPDATE'
+};
+
+use constant
+{
+	TYPE					=> 'TYPE',
+	TYPE_NONE				=> '',
+
+	TYPE_AGGREGATE			=> 'AGGREGATE',
+	TYPE_COLLATION			=> 'COLLATION',
+	TYPE_CONVERSION			=> 'CONVERSION',
+	TYPE_SCHEMA				=> 'SCHEMA',
+	TYPE_FUNCTION			=> 'FUNCTION',
+	TYPE_INDEX				=> 'INDEX',
+	TYPE_TABLE				=> 'TABLE',
+	TYPE_TABLE_COLUMN		=> 'TABLE COLUMN',
+	TYPE_TABLE_CONSTRAINT	=> 'TABLE CONSTRAINT',
+	TYPE_TABLE_TOAST		=> 'TABLE TOAST',
+	TYPE_TYPE				=> 'TYPE'
+};
+
+use constant
+{
+	NAME			=> 'NAME'
+};
+
+################################################################################
+# Command line parameters
+################################################################################
+my $strPgSqlBin = '../../../../bin/bin';	# Path of PG binaries to use for
+											# this test
+my $strTestPath = '../../../../data';		# Path where testing will occur
+my $iDefaultPort = 6000;					# Default port to run Postgres on
+my $bHelp = false;							# Display help
+my $bQuiet = false;							# Supress output except for errors
+my $bNoCleanup = false;						# Cleanup database on exit
+
+GetOptions ('q|quiet' => \$bQuiet,
+			'no-cleanup' => \$bNoCleanup,
+			'help' => \$bHelp,
+			'pgsql-bin=s' => \$strPgSqlBin,
+			'test-path=s' => \$strTestPath)
+	or pod2usage(2);
+
+# Display version and exit if requested
+if ($bHelp)
+{
+	print 'pg_audit unit test\n\n';
+	pod2usage();
+
+	exit 0;
+}
+
+################################################################################
+# Global variables
+################################################################################
+my $hDb;					# Connection to Postgres
+my $strLogExpected = '';	# The expected log compared with grepping AUDIT
+							# entries from the postgres log.
+
+my $strDatabase = 'postgres';	# Connected database (modified by PgSetDatabase)
+my $strUser = 'postgres';		# Connected user (modified by PgSetUser)
+my $strAuditRole = 'audit';		# Role to use for auditing
+
+my %oAuditLogHash;				# Hash to store pg_audit.log GUCS
+my %oAuditGrantHash;			# Hash to store pg_audit grants
+
+my $strCurrentAuditLog;		# pg_audit.log setting was Postgres was started with
+my $strTemporaryAuditLog;	# pg_audit.log setting that was set hot
+
+################################################################################
+# Stores the mapping between commands, classes, and types
+################################################################################
+my %oCommandHash =
+(&COMMAND_ANALYZE => {
+	&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_AGGREGATE => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_AGGREGATE},
+	&COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_COLLATION},
+	&COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_CONVERSION},
+	&COMMAND_ALTER_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE_SET => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_ALTER_ROLE},
+	&COMMAND_ALTER_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_ALTER_TABLE_COLUMN => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_COLUMN, &COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_ALTER_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_BEGIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CLOSE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COMMIT => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COPY_FROM => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_COPY_TO => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_CREATE_AGGREGATE => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_AGGREGATE},
+	&COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_CONVERSION},
+	&COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_COLLATION},
+	&COMMAND_CREATE_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX},
+	&COMMAND_DEALLOCATE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_DECLARE_CURSOR => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_DO => {&CLASS => &CLASS_FUNCTION, &TYPE => &TYPE_NONE},
+	&COMMAND_DISCARD_ALL => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_FUNCTION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_FUNCTION},
+	&COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_SCHEMA},
+	&COMMAND_CREATE_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_CREATE_TABLE_AS => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_TABLE_CONSTRAINT => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_CONSTRAINT, &COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TOAST => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_TOAST, &COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TYPE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TYPE,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_EXECUTE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_FUNCTION => {&CLASS => &CLASS_FUNCTION,
+		&TYPE => &TYPE_FUNCTION, &COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXPLAIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_FETCH => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_GRANT => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_PREPARE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_PREPARE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_INSERT => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE},
+	&COMMAND_REVOKE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_SELECT => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_SET => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_UPDATE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE}
+);
+
+################################################################################
+# CommandExecute
+################################################################################
+sub CommandExecute
+{
+	my $strCommand = shift;
+	my $bSuppressError = shift;
+
+	# Set default
+	$bSuppressError = defined($bSuppressError) ? $bSuppressError : false;
+
+	# Run the command
+	my $iResult = system($strCommand);
+
+	if ($iResult != 0 && !$bSuppressError)
+	{
+		confess "command '${strCommand}' failed with error ${iResult}";
+	}
+}
+
+################################################################################
+# log
+################################################################################
+sub log
+{
+	my $strMessage = shift;
+	my $bError = shift;
+
+	# Set default
+	$bError = defined($bError) ? $bError : false;
+
+	if (!$bQuiet)
+	{
+		print "${strMessage}\n";
+	}
+
+	if ($bError)
+	{
+		exit 1;
+	}
+}
+
+################################################################################
+# ArrayToString
+################################################################################
+sub ArrayToString
+{
+	my @stryArray = @_;
+
+	my $strResult = '';
+
+	for (my $iIndex = 0; $iIndex < @stryArray; $iIndex++)
+	{
+		if ($iIndex != 0)
+		{
+			$strResult .= ', ';
+		}
+
+		$strResult .= $stryArray[$iIndex];
+	}
+
+	return $strResult;
+}
+
+################################################################################
+# BuildModule
+################################################################################
+sub BuildModule
+{
+	capture('cd ..;make');
+	CommandExecute("cp ../pg_audit.so" .
+				   " ${strPgSqlBin}/../lib/postgresql");
+	CommandExecute("cp ../pg_audit.control" .
+				   " ${strPgSqlBin}/../share/postgresql/extension");
+	CommandExecute("cp ../pg_audit--1.0.0.sql" .
+				   " ${strPgSqlBin}/../share/postgresql/extension");
+}
+
+################################################################################
+# PgConnect
+################################################################################
+sub PgConnect
+{
+	my $iPort = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+
+	# Log Connection
+	&log("   DB: connect user ${strUser}, database ${strDatabase}");
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# Connect to the db
+	$hDb = DBI->connect("dbi:Pg:dbname=${strDatabase};port=${iPort};host=/tmp",
+						$strUser, undef,
+						{AutoCommit => 1, RaiseError => 1});
+}
+
+################################################################################
+# PgDisconnect
+################################################################################
+sub PgDisconnect
+{
+	# Connect to the db (whether it is local or remote)
+	if (defined($hDb))
+	{
+		$hDb->disconnect;
+		undef($hDb);
+	}
+}
+
+################################################################################
+# PgExecute
+################################################################################
+sub PgExecute
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	my $hStatement = $hDb->prepare($strSql);
+
+	$hStatement->execute();
+	$hStatement->finish();
+}
+
+################################################################################
+# PgExecuteOnly
+################################################################################
+sub PgExecuteOnly
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	$hDb->do($strSql);
+}
+
+################################################################################
+# PgSetDatabase
+################################################################################
+sub PgSetDatabase
+{
+	my $strDatabaseParam = shift;
+
+	# Stop and start the database to reset pgconf entries
+	PgStop();
+	PgStart();
+
+	# Execute the statement
+	$strDatabase = $strDatabaseParam;
+	PgConnect();
+}
+
+################################################################################
+# PgSetUser
+################################################################################
+sub PgSetUser
+{
+	my $strUserParam = shift;
+
+	$strUser = $strUserParam;
+
+	# Stop and start the database to reset pgconf entries
+	if ((defined($strTemporaryAuditLog) && !defined($strCurrentAuditLog)) ||
+		(defined($strCurrentAuditLog) && !defined($strTemporaryAuditLog)) ||
+		$strCurrentAuditLog ne $strTemporaryAuditLog)
+	{
+		$strCurrentAuditLog = $strTemporaryAuditLog;
+
+		PgStop();
+		PgStart();
+	}
+	else
+	{
+		# Execute the statement
+		PgConnect();
+	}
+}
+
+################################################################################
+# SaveString
+################################################################################
+sub SaveString
+{
+	my $strFile = shift;
+	my $strString = shift;
+
+	# Open the file for writing
+	my $hFile;
+
+	open($hFile, '>', $strFile)
+		or confess "unable to open ${strFile}";
+
+	if ($strString ne '')
+	{
+		syswrite($hFile, $strString)
+			or confess "unable to write to ${strFile}: $!";
+	}
+
+	close($hFile);
+}
+
+################################################################################
+# PgLogExecute
+################################################################################
+sub PgLogExecute
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+	my $bExecute = shift;
+	my $bWait = shift;
+	my $bLogSql = shift;
+	my $strParameter = shift;
+	my $bExpectError = shift;
+
+	# Set defaults
+	$bExecute = defined($bExecute) ? $bExecute : true;
+	$bWait = defined($bWait) ? $bWait : true;
+	$bLogSql = defined($bLogSql) ? $bLogSql : true;
+
+	if ($bExecute)
+	{
+		eval
+		{
+			PgExecuteOnly($strSql);
+		};
+
+		if ($@ && !$bExpectError)
+		{
+			confess $@;
+		}
+	}
+
+	PgLogExpect($strCommand, $bLogSql ? $strSql : '', $strParameter, $oData);
+
+	if ($bWait)
+	{
+		PgLogWait();
+	}
+}
+
+################################################################################
+# QuoteCSV
+################################################################################
+sub QuoteCSV
+{
+	my $strCSV = shift;
+
+	if (defined($strCSV) &&
+		(index($strCSV, ',') >= 0 || index($strCSV, '"') > 0 ||
+		 index($strCSV, "\n") > 0 || index($strCSV, "\r") >= 0))
+	{
+		$strCSV =~ s/"/""/g;
+		$strCSV = "\"${strCSV}\"";
+	}
+
+	return $strCSV;
+}
+
+################################################################################
+# PgLogExpect
+################################################################################
+sub PgLogExpect
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $strParameter = shift;
+	my $oData = shift;
+
+	# If oData is false then no logging
+	if (defined($oData) && ref($oData) eq '' && !$oData)
+	{
+		return;
+	}
+
+	# Quote SQL if needs to be quoted
+	$strSql = QuoteCSV($strSql);
+
+	if (defined($strParameter))
+	{
+		$strSql .= ",${strParameter}";
+	}
+
+	# Log based on session
+	if (PgShouldLog($strCommand))
+	{
+		# Make sure class is defined
+		my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+		if (!defined($strClass))
+		{
+			confess "class is not defined for command ${strCommand}";
+		}
+
+		# Make sure object type is defined
+		my $strObjectType = $oCommandHash{$strCommand}{&TYPE};
+
+		if (!defined($strObjectType))
+		{
+			confess "object type is not defined for command ${strCommand}";
+		}
+
+		# Check for command override
+		my $strCommandLog = $strCommand;
+
+		if ($oCommandHash{$strCommand}{&COMMAND})
+		{
+			$strCommandLog = $oCommandHash{$strCommand}{&COMMAND};
+		}
+
+		my $strObjectName = '';
+
+		if (defined($oData) && ref($oData) ne 'ARRAY')
+		{
+			$strObjectName = QuoteCSV($oData);
+		}
+
+		my $strLog .= "SESSION,${strClass},${strCommandLog}," .
+					  "${strObjectType},${strObjectName},${strSql}";
+		&log("AUDIT: ${strLog}");
+
+		$strLogExpected .= "${strLog}\n";
+	}
+
+	# Log based on grants
+	if (ref($oData) eq 'ARRAY' && ($strCommand eq COMMAND_SELECT ||
+		$oCommandHash{$strCommand}{&CLASS} eq CLASS_WRITE))
+	{
+		foreach my $oTableHash (@{$oData})
+		{
+			my $strObjectName = QuoteCSV(${$oTableHash}{&NAME});
+			my $strCommandLog = ${$oTableHash}{&COMMAND};
+
+			if (defined($oAuditGrantHash{$strAuditRole}
+										{$strObjectName}{$strCommandLog}))
+			{
+				my $strCommandLog = defined(${$oTableHash}{&COMMAND_LOG}) ?
+					${$oTableHash}{&COMMAND_LOG} : $strCommandLog;
+				my $strClass = $oCommandHash{$strCommandLog}{&CLASS};
+				my $strObjectType = ${$oTableHash}{&TYPE};
+
+				my $strLog .= "OBJECT,${strClass},${strCommandLog}," .
+							  "${strObjectType},${strObjectName},${strSql}";
+				&log("AUDIT: ${strLog}");
+
+				$strLogExpected .= "${strLog}\n";
+			}
+		}
+
+		$oData = undef;
+	}
+}
+
+################################################################################
+# PgShouldLog
+################################################################################
+sub PgShouldLog
+{
+	my $strCommand = shift;
+
+	# Make sure class is defined
+	my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+	if (!defined($strClass))
+	{
+		confess "class is not defined for command ${strCommand}";
+	}
+
+	# Check logging for the role
+	my $bLog = undef;
+
+	if (defined($oAuditLogHash{&CONTEXT_ROLE}{$strUser}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_ROLE}{$strUser}{$strClass};
+	}
+
+	# Else check logging for the db
+	elsif (defined($oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}{$strClass};
+	}
+
+	# Else check logging for global
+	elsif (defined($oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}{$strClass};
+	}
+
+	return defined($bLog) ? true : false;
+}
+
+################################################################################
+# PgLogWait
+################################################################################
+sub PgLogWait
+{
+	my $strLogActual;
+
+	# Run in an eval block since grep returns 1 when nothing was found
+	eval
+	{
+		$strLogActual = capture("grep 'LOG:  AUDIT: '" .
+								" ${strTestPath}/postgresql.log");
+	};
+
+	# If an error was returned, continue if it was 1, otherwise confess
+	if ($@)
+	{
+		my $iExitStatus = $? >> 8;
+
+		if ($iExitStatus != 1)
+		{
+			confess "grep returned ${iExitStatus}";
+		}
+
+		$strLogActual = '';
+	}
+
+	# Strip the AUDIT and timestamp from the actual log
+	$strLogActual =~ s/prefix LOG:  AUDIT\: //g;
+	$strLogActual =~ s/SESSION,[0-9]+,[0-9]+,/SESSION,/g;
+	$strLogActual =~ s/OBJECT,[0-9]+,[0-9]+,/OBJECT,/g;
+
+	# Save the logs
+	SaveString("${strTestPath}/audit.actual", $strLogActual);
+	SaveString("${strTestPath}/audit.expected", $strLogExpected);
+
+	CommandExecute("diff ${strTestPath}/audit.expected" .
+				   " ${strTestPath}/audit.actual");
+}
+
+################################################################################
+# PgDrop
+################################################################################
+sub PgDrop
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Stop the cluster
+	PgStop(true, $strPath);
+
+	# Remove the directory
+	CommandExecute("rm -rf ${strTestPath}");
+}
+
+################################################################################
+# PgCreate
+################################################################################
+sub PgCreate
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	CommandExecute("${strPgSqlBin}/initdb -D ${strPath} -U ${strUser}" .
+				   ' -A trust > /dev/null');
+}
+
+################################################################################
+# PgStop
+################################################################################
+sub PgStop
+{
+	my $bImmediate = shift;
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+	$bImmediate = defined($bImmediate) ? $bImmediate : false;
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# If postmaster process is running then stop the cluster
+	if (-e $strPath . '/postmaster.pid')
+	{
+		CommandExecute("${strPgSqlBin}/pg_ctl stop -D ${strPath} -w -s -m " .
+					  ($bImmediate ? 'immediate' : 'fast'));
+	}
+}
+
+################################################################################
+# PgStart
+################################################################################
+sub PgStart
+{
+	my $iPort = shift;
+	my $strPath = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Make sure postgres is not running
+	if (-e $strPath . '/postmaster.pid')
+	{
+		confess "${strPath}/postmaster.pid exists, cannot start";
+	}
+
+	# Start the cluster
+	CommandExecute("${strPgSqlBin}/pg_ctl start -o \"" .
+				   "-c port=${iPort}" .
+				   " -c unix_socket_directories='/tmp'" .
+				   " -c shared_preload_libraries='pg_audit'" .
+				   " -c log_min_messages=debug1" .
+				   " -c log_line_prefix='prefix '" .
+				   " -c log_statement=all" .
+				   (defined($strCurrentAuditLog) ?
+					   " -c pg_audit.log='${strCurrentAuditLog}'" : '') .
+				   " -c pg_audit.role='${strAuditRole}'" .
+				   " -c log_connections=on" .
+				   "\" -D ${strPath} -l ${strPath}/postgresql.log -w -s");
+
+	# Connect user session
+	PgConnect();
+}
+
+################################################################################
+# PgAuditLogSet
+################################################################################
+sub PgAuditLogSet
+{
+	my $strContext = shift;
+	my $strName = shift;
+	my @stryClass = @_;
+
+	# Create SQL to set the GUC
+	my $strCommand;
+	my $strSql;
+
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		$strCommand = COMMAND_SET;
+		$strSql = "set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+		$strTemporaryAuditLog = ArrayToString(@stryClass);
+	}
+	elsif ($strContext eq CONTEXT_ROLE)
+	{
+		$strCommand = COMMAND_ALTER_ROLE_SET;
+		$strSql = "alter role ${strName} set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+	}
+	else
+	{
+		confess "unable to set pg_audit.log for context ${strContext}";
+	}
+
+	# Reset the audit log
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		delete($oAuditLogHash{$strContext});
+		$strName = CONTEXT_GLOBAL;
+	}
+	else
+	{
+		delete($oAuditLogHash{$strContext}{$strName});
+	}
+
+	# Store all the classes in the hash and build the GUC
+	foreach my $strClass (@stryClass)
+	{
+		if ($strClass eq CLASS_ALL)
+		{
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_DDL} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_FUNCTION} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_MISC} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_READ} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_WRITE} = true;
+		}
+
+		if (index($strClass, '-') == 0)
+		{
+			$strClass = substr($strClass, 1);
+
+			delete($oAuditLogHash{$strContext}{$strName}{$strClass});
+		}
+		else
+		{
+			$oAuditLogHash{$strContext}{$strName}{$strClass} = true;
+		}
+	}
+
+	PgLogExecute($strCommand, $strSql);
+}
+
+################################################################################
+# PgAuditGrantSet
+################################################################################
+sub PgAuditGrantSet
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_GRANT, "GRANT " .
+								(defined($strColumn) ?
+									lc(${strPrivilege}) ." (${strColumn})" :
+									uc(${strPrivilege})) .
+								" ON TABLE ${strObject} TO ${strRole} ");
+
+	$oAuditGrantHash{$strRole}{$strObject}{$strPrivilege} = true;
+}
+
+################################################################################
+# PgAuditGrantReset
+################################################################################
+sub PgAuditGrantReset
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_REVOKE, "REVOKE  " . uc(${strPrivilege}) .
+				 (defined($strColumn) ? " (${strColumn})" : '') .
+				 " ON TABLE ${strObject} FROM ${strRole} ");
+
+	delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege});
+}
+
+################################################################################
+# Main
+################################################################################
+my @oyTable;	   # Store table info for select, insert, update, delete
+my $strSql;		# Hold Sql commands
+
+# Drop the old cluster, build the code, and create a new cluster
+PgDrop();
+BuildModule();
+PgCreate();
+PgStart();
+
+PgExecute("create extension pg_audit");
+
+# Create test users and the audit role
+PgExecute("create user user1");
+PgExecute("create user user2");
+PgExecute("create role ${strAuditRole}");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL));
+
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_READ, CLASS_WRITE));
+
+# User1 follows the global log settings
+PgSetUser('user1');
+
+$strSql = 'CREATE  TABLE  public.test (id pg_catalog.int4   )' .
+		  '  WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+$strSql = 'drop table test';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'public.test');
+
+PgSetUser('user2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test2 (id int)', 'public.test2');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test3 (id int)', 'public.test2');
+
+# Catalog select should not log
+PgLogExecute(COMMAND_SELECT, 'select * from pg_class limit 1',
+							   false);
+
+# Multi-table select
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test3, test2',
+							   \@oyTable);
+
+# Various CTE combinations
+PgAuditGrantSet($strAuditRole, &COMMAND_INSERT, 'public.test3');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (select id from test2)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (insert into test3 values (1) returning id)' .
+			 ' insert into test2 select id from cte',
+			 \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE, 'public.test2');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (update test2 set id = 1 returning id)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_INSERT});
+PgLogExecute(COMMAND_UPDATE,
+			 'with cte as (insert into test2 values (1) returning id)' .
+			 ' update test3 set id = cte.id' .
+			 ' from cte where test3.id <> cte.id',
+			 \@oyTable);
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_NONE));
+PgSetUser('user2');
+
+# Column-based audits
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test4 (id int, name text)', 'public.test4');
+PgAuditGrantSet($strAuditRole, COMMAND_SELECT, 'public.test4', 'name');
+PgAuditGrantSet($strAuditRole, COMMAND_UPDATE, 'public.test4', 'id');
+PgAuditGrantSet($strAuditRole, COMMAND_INSERT, 'public.test4', 'name');
+
+# Select
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id from public.test4',
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from public.test4',
+							  \@oyTable);
+
+# Insert
+@oyTable = ();
+PgLogExecute(COMMAND_INSERT, 'insert into public.test4 (id) values (1)',
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT, "insert into public.test4 (name) values ('test')",
+							  \@oyTable);
+
+# Update
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set name = 'foo'",
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set id = 1",
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			&COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update public.test4 set name = 'foo' where name = 'bar'",
+			 \@oyTable);
+
+# Drop test tables
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test2", 'public.test2');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test3", 'public.test3');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test4", 'public.test4');
+
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Create some email friendly tests.  These first tests are session logging only.
+PgSetUser('postgres');
+
+&log("\nExamples:");
+
+&log("\nSession Audit:\n");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL, CLASS_READ));
+
+PgSetUser('user1');
+
+$strSql = 'CREATE  TABLE  public.account (id pg_catalog.int4   ,' .
+		  ' name pg_catalog.text   COLLATE pg_catalog."default", ' .
+		  'password pg_catalog.text   COLLATE pg_catalog."default", '.
+		  'description pg_catalog.text   COLLATE pg_catalog."default")  '.
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.account');
+PgLogExecute(COMMAND_SELECT,
+			 'select * from account');
+PgLogExecute(COMMAND_INSERT,
+			 "insert into account (id, name, password, description)" .
+			 " values (1, 'user1', 'HASH1', 'blah, blah')");
+&log("AUDIT: <nothing logged>");
+
+# Now tests for object logging
+&log("\nObject Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_NONE));
+PgExecute("set pg_audit.role = 'audit'");
+PgSetUser('user1');
+
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account', 'password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id, name from account',
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE,
+				'public.account', 'name, password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Now tests for session/object logging
+&log("\nSession/Object Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user1', (CLASS_READ, CLASS_WRITE));
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account_role_map (account_id int, role_id int)',
+			 'public.account_role_map');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account_role_map');
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.account_role_map', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT,
+			 'select account.password, account_role_map.role_id from account' .
+			 ' inner join account_role_map' .
+			 ' on account.id = account_role_map.account_id',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update account set description = 'yada, yada'" .
+			 " where password = 'HASH2'",
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Test all sql commands
+&log("\nExhaustive Command Tests:\n");
+
+PgSetUser('postgres');
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_ALL));
+PgLogExecute(COMMAND_SET, "set pg_audit.role = 'audit'");
+
+PgLogExecute(COMMAND_DO, "do \$\$\ begin raise notice 'test'; end; \$\$;");
+
+$strSql = 'CREATE SCHEMA  test ';
+PgLogExecute(COMMAND_CREATE_SCHEMA, $strSql, 'test');
+
+# Test COPY
+PgLogExecute(COMMAND_COPY_TO,
+			 "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'");
+
+$strSql = 'CREATE  TABLE  test.pg_class  WITH (oids=OFF)   AS SELECT relname,' .
+		  ' relnamespace, reltype, reloftype, relowner, relam, relfilenode, ' .
+		  'reltablespace, relpages, reltuples, relallvisible, reltoastrelid, ' .
+		  'relhasindex, relisshared, relpersistence, relkind, relnatts, ' .
+		  'relchecks, relhasoids, relhaspkey, relhasrules, relhastriggers, ' .
+		  'relhassubclass, relrowsecurity, relispopulated, relreplident, ' .
+		  'relfrozenxid, relminmxid, relacl, reloptions ' .
+		  'FROM pg_catalog.pg_class ';
+PgLogExecute(COMMAND_INSERT, $strSql, undef, true, false);
+PgLogExecute(COMMAND_CREATE_TABLE_AS, $strSql, 'test.pg_class', false, true);
+
+$strSql = "COPY test.pg_class from '" . abs_path($strTestPath) . "/class.out'";
+PgLogExecute(COMMAND_INSERT, $strSql);
+#PgLogExecute(COMMAND_COPY_FROM, $strSql, undef, false, true);
+
+# Test prepared SELECT
+PgLogExecute(COMMAND_PREPARE_READ,
+			 'PREPARE pgclassstmt (oid) as select *' .
+			 ' from pg_class where oid = $1');
+PgLogExecute(COMMAND_EXECUTE_READ,
+			 'EXECUTE pgclassstmt (1)');
+PgLogExecute(COMMAND_DEALLOCATE,
+			 'DEALLOCATE pgclassstmt');
+
+# Test cursor
+PgLogExecute(COMMAND_BEGIN,
+			 'BEGIN');
+PgLogExecute(COMMAND_DECLARE_CURSOR,
+			 'DECLARE ctest SCROLL CURSOR FOR SELECT * FROM pg_class');
+PgLogExecute(COMMAND_FETCH,
+			 'FETCH NEXT FROM ctest');
+PgLogExecute(COMMAND_CLOSE,
+			 'CLOSE ctest');
+PgLogExecute(COMMAND_COMMIT,
+			 'COMMIT');
+
+# Test prepared INSERT
+$strSql = 'CREATE  TABLE  test.test_insert (id pg_catalog.int4   )  ' .
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'test.test_insert');
+
+$strSql = 'PREPARE pgclassstmt (oid) as insert into test.test_insert (id) ' .
+		  'values ($1)';
+PgLogExecute(COMMAND_PREPARE_WRITE, $strSql);
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, "1");
+
+$strSql = 'EXECUTE pgclassstmt (1)';
+PgLogExecute(COMMAND_EXECUTE_WRITE, $strSql, undef, true, true);
+
+# Create a table with a primary key
+$strSql = 'CREATE  TABLE  public.test (id pg_catalog.int4   , ' .
+		  'name pg_catalog.text   COLLATE pg_catalog."default", description ' .
+		  'pg_catalog.text   COLLATE pg_catalog."default", CONSTRAINT ' .
+		  'test_pkey PRIMARY KEY (id))  WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_INDEX, $strSql, 'public.test_pkey', true, false);
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test', false, true);
+
+PgLogExecute(COMMAND_ANALYZE, 'analyze test');
+
+# Grant select to public - this should have no affect on auditing
+$strSql = 'GRANT SELECT ON TABLE public.test TO PUBLIC ';
+PgLogExecute(COMMAND_GRANT, $strSql);
+
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+# Now grant select to audit and it should be logged
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test', \@oyTable);
+
+# Check columns granted to public and make sure they do not log
+PgAuditGrantReset($strAuditRole, &COMMAND_SELECT, 'public.test');
+
+$strSql = 'GRANT select (name) ON TABLE public.test TO PUBLIC ';
+PgLogExecute(COMMAND_GRANT, $strSql);
+
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+PgLogExecute(COMMAND_SELECT, 'select from test');
+
+# Try a select that does not reference any tables
+PgLogExecute(COMMAND_SELECT, 'select 1, current_timestamp');
+
+# Now try the same in a do block
+$strSql = 'do $$ declare test int; begin select 1 into test; end $$';
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'select 1';
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, true);
+
+# Insert some data into test and try a loop in a do block
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (1)');
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (2)');
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (3)');
+
+$strSql = 'do $$ ' .
+		  'declare ' .
+		  '	result record;' .
+		  'begin ' .
+		  '	for result in select id from test loop ' .
+		  '		insert into test (id) values (result.id + 100); ' .
+		  '	end loop; ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'select id from test';
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false);
+
+$strSql = 'insert into test (id) values (result.id + 100)';
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+# Test EXECUTE with bind
+$strSql = "select * from test where id = ?";
+my $hStatement = $hDb->prepare($strSql);
+
+$strSql = "select * from test where id = \$1";
+$hStatement->bind_param(1, 101);
+$hStatement->execute();
+
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false, undef, "101");
+
+$hStatement->bind_param(1, 103);
+$hStatement->execute();
+
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false, undef, "103");
+
+$hStatement->finish();
+
+# Now try some DDL in a do block
+$strSql = 'do $$ ' .
+		  'begin ' .
+		  '	create table test_block (id int); ' .
+		  '	drop table test_block; ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'CREATE  TABLE  public.test_block (id pg_catalog.int4   )  ' .
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test_block', false, false);
+
+$strSql = 'drop table test_block';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'public.test_block', false, false);
+
+# Generate an error in a do block and make sure the stack gets cleaned up
+$strSql = 'do $$ ' .
+		  'begin ' .
+		  '	create table bobus.test_block (id int); ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, undef, undef, undef, undef, true);
+# PgLogExecute(COMMAND_SELECT, 'select 1');
+# exit 0;
+
+# Try explain
+PgLogExecute(COMMAND_SELECT, 'explain select 1', undef, true, false);
+PgLogExecute(COMMAND_EXPLAIN, 'explain select 1', undef, false, true);
+
+# Now set grant to a specific column to audit and make sure it logs
+# Make sure the the converse is true
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test',
+				'name, description');
+PgLogExecute(COMMAND_SELECT, 'select id from test');
+
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from test', \@oyTable);
+
+# Test alter and drop table statements
+$strSql = 'ALTER TABLE public.test DROP COLUMN description ';
+PgLogExecute(COMMAND_ALTER_TABLE_COLUMN,
+			 $strSql, 'public.test.description', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 $strSql, 'public.test', false, true);
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable);
+
+$strSql = 'ALTER TABLE  public.test RENAME TO test2';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'public.test2');
+
+$strSql = 'ALTER TABLE public.test2 SET SCHEMA test';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'test.test2');
+
+$strSql = 'ALTER TABLE test.test2 ADD COLUMN description pg_catalog.text   ' .
+		  'COLLATE pg_catalog."default"';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'test.test2');
+
+$strSql = 'ALTER TABLE test.test2 DROP COLUMN description ';
+PgLogExecute(COMMAND_ALTER_TABLE_COLUMN, $strSql,
+			 'test.test2.description', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql,
+			 'test.test2', false, true);
+
+$strSql = 'drop table test.test2';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'test.test2', true, false);
+PgLogExecute(COMMAND_DROP_TABLE_CONSTRAINT, $strSql, 'test_pkey on test.test2',
+			 false, false);
+PgLogExecute(COMMAND_DROP_TABLE_INDEX, $strSql, 'test.test_pkey', false, true);
+
+$strSql = "CREATE  FUNCTION public.int_add(IN a pg_catalog.int4 , IN b " .
+		  "pg_catalog.int4 ) RETURNS  pg_catalog.int4 LANGUAGE plpgsql  " .
+		  "VOLATILE  CALLED ON NULL INPUT SECURITY INVOKER COST 100   AS '" .
+		  " begin return a + b; end '";
+PgLogExecute(COMMAND_CREATE_FUNCTION, $strSql,
+			 'public.int_add(integer,integer)');
+PgLogExecute(COMMAND_SELECT, "select int_add(1, 1)",
+							 undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_FUNCTION, "select int_add(1, 1)",
+									   'public.int_add', false, true);
+
+$strSql = "CREATE AGGREGATE public.sum_test(  pg_catalog.int4) " .
+		  "(SFUNC=public.int_add, STYPE=pg_catalog.int4, INITCOND='0')";
+PgLogExecute(COMMAND_CREATE_AGGREGATE, $strSql, 'public.sum_test(integer)');
+
+# There's a bug here in deparse:
+$strSql = "ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2";
+PgLogExecute(COMMAND_ALTER_AGGREGATE, $strSql, 'public.sum_test2(integer)');
+
+$strSql = "CREATE COLLATION public.collation_test (LC_COLLATE = 'de_DE', " .
+		  "LC_CTYPE = 'de_DE')";
+PgLogExecute(COMMAND_CREATE_COLLATION, $strSql, 'public.collation_test');
+
+$strSql =  "ALTER COLLATION public.collation_test RENAME TO collation_test2";
+PgLogExecute(COMMAND_ALTER_COLLATION, $strSql, 'public.collation_test2');
+
+$strSql = "CREATE  CONVERSION public.conversion_test FOR 'SQL_ASCII' " .
+		  "TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic";
+PgLogExecute(COMMAND_CREATE_CONVERSION, $strSql, 'public.conversion_test');
+
+$strSql = "ALTER CONVERSION public.conversion_test RENAME TO conversion_test2";
+PgLogExecute(COMMAND_ALTER_CONVERSION, $strSql, 'public.conversion_test2');
+
+PgLogExecute(COMMAND_CREATE_DATABASE, "CREATE DATABASE database_test");
+PgLogExecute(COMMAND_ALTER_DATABASE,
+			 "ALTER DATABASE database_test rename to database_test2");
+PgLogExecute(COMMAND_DROP_DATABASE, "DROP DATABASE database_test2");
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Stop the database
+if (!$bNoCleanup)
+{
+	PgDrop();
+}
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index a698d0f..5b247a9 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 89fff77..6b0b407 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -125,6 +125,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbench         SYSTEM "pgbench.sgml">
 <!ENTITY pgarchivecleanup SYSTEM "pgarchivecleanup.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..f9152cd
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,335 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> module provides session and object
+    auditing via the standard logging facility.  Session and object auditing are
+    completely independent and can be combined.
+  </para>
+
+  <sect2>
+    <title>Session Auditing</title>
+
+    <para>
+      Session auditing allows the logging of all commands that are executed by
+      a user in the backend.  Each command is logged with a single entry and
+      includes the audit type (e.g. <literal>SESSION</literal>), command type
+      (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and
+      statement (e.g. <literal>"select * from test"</literal>).
+
+      Fully-qualified names and object types will be logged for
+      <literal>CREATE</literal>, <literal>UPDATE</literal>, and
+      <literal>DROP</literal> commands on <literal>TABLE</literal>,
+      <literal>MATVIEW</literal>, <literal>VIEW</literal>,
+      <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,
+      <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and
+      <literal>SEQUENCE</literal> objects as well as function calls.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is controlled by the <literal>pg_audit.log</literal>
+        GUC. There are six classes of commands that are recognized:
+
+        <itemizedlist>
+          <listitem>
+            <para>
+              <literal>READ</literal> - <literal>SELECT</literal> and
+              <literal>COPY</literal> when the source is a table or query.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>WRITE</literal> - <literal>INSERT</literal>,
+              <literal>UPDATE</literal>, <literal>DELETE</literal>,
+              <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+              destination is a table.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>FUNCTION</literal> - Function calls and
+              <literal>DO</literal> blocks.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>DDL</literal> - DDL, plus <literal>VACUUM</literal>,
+              <literal>REINDEX</literal>, and <literal>ANALYZE</literal>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>PARAMETER</literal> - Parameters that were passed for the statement.  Parameters immediately follow the statement text. 
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>MISC</literal> - Miscellaneous commands, e.g.
+              <literal>DISCARD</literal>, <literal>FETCH</literal>,
+              <literal>CHECKPOINT</literal>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </para>
+
+      <para>
+        Enable session logging for all writes and DDL:
+          <programlisting>
+pg_audit.log = 'write, ddl'
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except miscellaneous:
+          <programlisting>
+pg_audit.log = 'all, -misc'
+          </programlisting>
+      </para>
+      
+      <para>
+      Note that <literal>pg_audit.log</literal> can be set globally (in 
+      <filename>postgresql.conf</filename>), at the database level (using
+      <literal>alter database ... set</literal>), or at the role level (using
+      <literal>alter role ... set</literal>).
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.log = 'read, ddl'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+      <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+select *
+    from account;
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Object Auditing</title>
+
+    <para>
+      Object auditing logs commands that affect a particular object.  Only
+      <literal>SELECT</literal>, <literal>INSERT</literal>,
+      <literal>UPDATE</literal> and <literal>DELETE</literal> commands are
+      supported.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level auditing is implemented via the roles system.  The
+        <literal>pg_audit.role</literal> GUC defines the role that will be used
+        for auditing.  An object will be audited when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.
+      </para>
+
+      <programlisting>
+postresql.conf: pg_audit.role = 'audit'
+
+grant select, delete
+   on public.account;
+      </programlisting>
+
+      <para>
+      Note that <literal>pg_audit.role</literal> can be set globally (in 
+      <filename>postgresql.conf</filename>), at the database level (using
+      <literal>alter database ... set</literal>), or at the role level (using
+      <literal>alter role ... set</literal>).
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.role = 'audit'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to audit;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to audit;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to audit;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is not in compliant CSV format.  If machine-readability is
+          required then consider setting
+          <literal>log_destination = 'csvlog'</literal>.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</literal> - <literal>SESSION</literal> or
+            <literal>OBJECT</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</literal> - <literal>READ</literal>,
+            <literal>WRITE</literal>, <literal>FUNCTION</literal>,
+            <literal>DDL</literal>, or <literal>MISC</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</literal> - <literal>ALTER TABLE</literal>,
+            <literal>SELECT</literal>, <literal>CREATE INDEX</literal>,
+            <literal>UPDATE</literal>, etc.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</literal> - <literal>TABLE</literal>,
+            <literal>INDEX</literal>, <literal>VIEW</literal>, etc.  Only
+            available for DML and certain DDL commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</literal> - The fully-qualified object name
+            (e.g. public.account).  Only available for DML and certain DDL
+            commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</literal> - Statement execute on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#12Sawada Masahiko
sawada.mshk@gmail.com
In reply to: Alvaro Herrera (#10)
Re: Auditing extension for PostgreSQL (Take 2)

On Tue, Mar 24, 2015 at 3:17 AM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Sawada Masahiko wrote:

I tied to look into latest patch, but got following error.

masahiko [pg_audit] $ LANG=C make
gcc -Wall -Wmissing-prototypes -Wpointer-arith
-Wdeclaration-after-statement -Wendif-labels
-Wmissing-format-attribute -Wformat-security -fno-strict-aliasing
-fwrapv -g -fpic -I. -I. -I../../src/include -D_GNU_SOURCE -c -o
pg_audit.o pg_audit.c
pg_audit.c: In function 'log_audit_event':
pg_audit.c:456: warning: ISO C90 forbids mixed declarations and code
pg_audit.c: In function 'pg_audit_ddl_command_end':
pg_audit.c:1436: error: 'pg_event_trigger_expand_command' undeclared
(first use in this function)

You need to apply my deparsing patch first, last version of which I
posted here:
/messages/by-id/20150316234406.GH3636@alvh.no-ip.org

Thank you for the info.
I've applied these patchese successfully.

I looked into this module, and had a few comments as follows.
1. pg_audit audits only one role currently.
In currently code, we can not multiple user as auditing user. Why?
(Sorry if this topic already has been discussed.)

2. OBJECT auditing does not work before adding acl info to pg_class.rel_acl.
In following situation, pg_audit can not audit OBJECT log.
$ cat postgresql.conf | grep audit
shared_preload_libraries = 'pg_audit'
pg_audit.role = 'hoge_user'
pg_audit.log = 'read, write'
$ psql -d postgres -U hoge_user
=# create table hoge(col int);
=# select * from hoge;
LOG: AUDIT: SESSION,3,1,READ,SELECT,,,select * from hoge;

OBJECT audit log is not logged here since pg_class.rel_acl is empty
yet. (Only logged SESSION log)
So after creating another unconcerned role and grant any privilege to that user,
OBJECT audit is logged successfully.

=# create role bar_user;
=# grant select on hoge to bar_user;
=# select * from hoge;
LOG: AUDIT: SESSION,4,1,READ,SELECT,,,select * from hoge;
LOG: AUDIT: OBJECT,4,1,READ,SELECT,TABLE,public.hoge,select * from hoge;

The both OBJCET and SESSION log are logged.

3. pg_audit logged OBJECT log even EXPLAIN command.
EXPLAIN command does not touch the table actually, but pg_audit writes
audit OBJECT log.
I'm not sure we need to log it. Is it intentional?

Regards,

-------
Sawada Masahiko

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

#13David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#12)
Re: Auditing extension for PostgreSQL (Take 2)

Hi Sawada,

Thank you for taking the time to look at the patch.

On 3/24/15 10:28 AM, Sawada Masahiko wrote:

I've applied these patchese successfully.

I looked into this module, and had a few comments as follows.
1. pg_audit audits only one role currently.
In currently code, we can not multiple user as auditing user. Why?
(Sorry if this topic already has been discussed.)

There is only one master audit role in a bid for simplicity. However,
there are two ways you can practically have multiple audit roles (both
are mentioned in the docs):

1) The audit role honors inheritance so you can grant all your audit
roles to the "master" role set in pg_audit.role and all the roles will
be audited.

2) Since pg_audit.role is a GUC, you can set a different audit role per
database by using ALTER DATABASE ... SET. You can set the GUC per logon
role as well though that would probably make things very complicated.
The GUC is SUSET so normal users cannot tamper with it.

2. OBJECT auditing does not work before adding acl info to pg_class.rel_acl.
In following situation, pg_audit can not audit OBJECT log.
$ cat postgresql.conf | grep audit
shared_preload_libraries = 'pg_audit'
pg_audit.role = 'hoge_user'
pg_audit.log = 'read, write'
$ psql -d postgres -U hoge_user
=# create table hoge(col int);
=# select * from hoge;
LOG: AUDIT: SESSION,3,1,READ,SELECT,,,select * from hoge;

OBJECT audit log is not logged here since pg_class.rel_acl is empty
yet. (Only logged SESSION log)
So after creating another unconcerned role and grant any privilege to that user,
OBJECT audit is logged successfully.

Yes, object auditing does not work until some grants have been made to
the audit role.

=# create role bar_user;
=# grant select on hoge to bar_user;
=# select * from hoge;
LOG: AUDIT: SESSION,4,1,READ,SELECT,,,select * from hoge;
LOG: AUDIT: OBJECT,4,1,READ,SELECT,TABLE,public.hoge,select * from hoge;

The both OBJCET and SESSION log are logged.

Looks right to me. If you don't want the session logging then disable
pg_audit.log.

Session and object logging are completely independent from each other:
one or the other, or both, or neither can be enabled at any time.

3. pg_audit logged OBJECT log even EXPLAIN command.
EXPLAIN command does not touch the table actually, but pg_audit writes
audit OBJECT log.
I'm not sure we need to log it. Is it intentional?

This is intentional. They are treated as queries since in production
they should be relatively rare (that is, not part of a normal function
or process) and it's good information to have because EXPLAIN can be
used to determine the number of rows in a table, and could also be used
to figure out when data is added or removed from a table. In essence,
it is a query even if it does not return row data.

If that sounds paranoid, well, auditing is all about paranoia!

--
- David Steele
david@pgmasters.net

#14Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#13)
Re: Auditing extension for PostgreSQL (Take 2)

Hi David,

Thank you for your answer!

On Wed, Mar 25, 2015 at 12:38 AM, David Steele <david@pgmasters.net> wrote:

Hi Sawada,

Thank you for taking the time to look at the patch.

On 3/24/15 10:28 AM, Sawada Masahiko wrote:

I've applied these patchese successfully.

I looked into this module, and had a few comments as follows.
1. pg_audit audits only one role currently.
In currently code, we can not multiple user as auditing user. Why?
(Sorry if this topic already has been discussed.)

There is only one master audit role in a bid for simplicity. However,
there are two ways you can practically have multiple audit roles (both
are mentioned in the docs):

1) The audit role honors inheritance so you can grant all your audit
roles to the "master" role set in pg_audit.role and all the roles will
be audited.

2) Since pg_audit.role is a GUC, you can set a different audit role per
database by using ALTER DATABASE ... SET. You can set the GUC per logon
role as well though that would probably make things very complicated.
The GUC is SUSET so normal users cannot tamper with it.

I understood.

2. OBJECT auditing does not work before adding acl info to pg_class.rel_acl.
In following situation, pg_audit can not audit OBJECT log.
$ cat postgresql.conf | grep audit
shared_preload_libraries = 'pg_audit'
pg_audit.role = 'hoge_user'
pg_audit.log = 'read, write'
$ psql -d postgres -U hoge_user
=# create table hoge(col int);
=# select * from hoge;
LOG: AUDIT: SESSION,3,1,READ,SELECT,,,select * from hoge;

OBJECT audit log is not logged here since pg_class.rel_acl is empty
yet. (Only logged SESSION log)
So after creating another unconcerned role and grant any privilege to that user,
OBJECT audit is logged successfully.

Yes, object auditing does not work until some grants have been made to
the audit role.

=# create role bar_user;
=# grant select on hoge to bar_user;
=# select * from hoge;
LOG: AUDIT: SESSION,4,1,READ,SELECT,,,select * from hoge;
LOG: AUDIT: OBJECT,4,1,READ,SELECT,TABLE,public.hoge,select * from hoge;

The both OBJCET and SESSION log are logged.

Looks right to me. If you don't want the session logging then disable
pg_audit.log.

Session and object logging are completely independent from each other:
one or the other, or both, or neither can be enabled at any time.

It means that OBJECT log is not logged just after creating table, even
if that table is touched by its owner.
To write OBJECT log, we need to grant privilege to role at least. right?

3. pg_audit logged OBJECT log even EXPLAIN command.
EXPLAIN command does not touch the table actually, but pg_audit writes
audit OBJECT log.
I'm not sure we need to log it. Is it intentional?

This is intentional. They are treated as queries since in production
they should be relatively rare (that is, not part of a normal function
or process) and it's good information to have because EXPLAIN can be
used to determine the number of rows in a table, and could also be used
to figure out when data is added or removed from a table. In essence,
it is a query even if it does not return row data.

Okay, I understood.

Regards,

-------
Sawada Masahiko

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

#15David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#14)
Re: Auditing extension for PostgreSQL (Take 2)

On Wed, Mar 25, 2015 at 12:38 AM, David Steele <david@pgmasters.net> wrote:

2. OBJECT auditing does not work before adding acl info to pg_class.rel_acl.
In following situation, pg_audit can not audit OBJECT log.
$ cat postgresql.conf | grep audit
shared_preload_libraries = 'pg_audit'
pg_audit.role = 'hoge_user'
pg_audit.log = 'read, write'
$ psql -d postgres -U hoge_user
=# create table hoge(col int);
=# select * from hoge;
LOG: AUDIT: SESSION,3,1,READ,SELECT,,,select * from hoge;

OBJECT audit log is not logged here since pg_class.rel_acl is empty
yet. (Only logged SESSION log)
So after creating another unconcerned role and grant any privilege to that user,
OBJECT audit is logged successfully.

Yes, object auditing does not work until some grants have been made to
the audit role.

=# create role bar_user;
=# grant select on hoge to bar_user;
=# select * from hoge;
LOG: AUDIT: SESSION,4,1,READ,SELECT,,,select * from hoge;
LOG: AUDIT: OBJECT,4,1,READ,SELECT,TABLE,public.hoge,select * from hoge;

The both OBJCET and SESSION log are logged.

Looks right to me. If you don't want the session logging then disable
pg_audit.log.

Session and object logging are completely independent from each other:
one or the other, or both, or neither can be enabled at any time.

It means that OBJECT log is not logged just after creating table, even
if that table is touched by its owner.
To write OBJECT log, we need to grant privilege to role at least. right?

Exactly. Privileges must be granted to the audit role in order for
object auditing to work.

--
- David Steele
david@pgmasters.net

#16Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#15)
Re: Auditing extension for PostgreSQL (Take 2)

On Wed, Mar 25, 2015 at 12:23 PM, David Steele <david@pgmasters.net> wrote:

On Wed, Mar 25, 2015 at 12:38 AM, David Steele <david@pgmasters.net> wrote:

2. OBJECT auditing does not work before adding acl info to pg_class.rel_acl.
In following situation, pg_audit can not audit OBJECT log.
$ cat postgresql.conf | grep audit
shared_preload_libraries = 'pg_audit'
pg_audit.role = 'hoge_user'
pg_audit.log = 'read, write'
$ psql -d postgres -U hoge_user
=# create table hoge(col int);
=# select * from hoge;
LOG: AUDIT: SESSION,3,1,READ,SELECT,,,select * from hoge;

OBJECT audit log is not logged here since pg_class.rel_acl is empty
yet. (Only logged SESSION log)
So after creating another unconcerned role and grant any privilege to that user,
OBJECT audit is logged successfully.

Yes, object auditing does not work until some grants have been made to
the audit role.

=# create role bar_user;
=# grant select on hoge to bar_user;
=# select * from hoge;
LOG: AUDIT: SESSION,4,1,READ,SELECT,,,select * from hoge;
LOG: AUDIT: OBJECT,4,1,READ,SELECT,TABLE,public.hoge,select * from hoge;

The both OBJCET and SESSION log are logged.

Looks right to me. If you don't want the session logging then disable
pg_audit.log.

Session and object logging are completely independent from each other:
one or the other, or both, or neither can be enabled at any time.

It means that OBJECT log is not logged just after creating table, even
if that table is touched by its owner.
To write OBJECT log, we need to grant privilege to role at least. right?

Exactly. Privileges must be granted to the audit role in order for
object auditing to work.

The table owner always can touch its table.
So does it lead that table owner can get its table information while
hiding OBJECT logging?

Also I looked into latest patch again.
Here are two review comment.

1.

typedef struct
{
int64 statementId;
int64 substatementId;

Both statementId and substatementId could be negative number.
I think these should be uint64 instead.

2.
I got ERROR when executing function uses cursor.

1) create empty table (hoge table)
2) create test function as follows.

create function test() returns int as $$
declare
cur1 cursor for select * from hoge;
tmp int;
begin
open cur1;
fetch cur1 into tmp;
return tmp;
end$$
language plpgsql ;

3) execute test function (got ERROR)
=# select test();
LOG: AUDIT: SESSION,6,1,READ,SELECT,,,selecT test();
LOG: AUDIT: SESSION,6,2,FUNCTION,EXECUTE,FUNCTION,public.test,selecT test();
LOG: AUDIT: SESSION,6,3,READ,SELECT,,,select * from hoge
CONTEXT: PL/pgSQL function test() line 6 at OPEN
ERROR: pg_audit stack is already empty
STATEMENT: selecT test();

It seems like that the item in stack is already freed by deleting
pg_audit memory context (in MemoryContextDelete()),
before calling stack_pop in dropping of top-level Portal.

Regards,

-------
Sawada Masahiko

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

#17David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#16)
Re: Auditing extension for PostgreSQL (Take 2)

On 3/25/15 7:46 AM, Sawada Masahiko wrote:

On Wed, Mar 25, 2015 at 12:23 PM, David Steele <david@pgmasters.net> wrote:

On Wed, Mar 25, 2015 at 12:38 AM, David Steele <david@pgmasters.net> wrote:

2. OBJECT auditing does not work before adding acl info to pg_class.rel_acl.
In following situation, pg_audit can not audit OBJECT log.
$ cat postgresql.conf | grep audit
shared_preload_libraries = 'pg_audit'
pg_audit.role = 'hoge_user'
pg_audit.log = 'read, write'
$ psql -d postgres -U hoge_user
=# create table hoge(col int);
=# select * from hoge;
LOG: AUDIT: SESSION,3,1,READ,SELECT,,,select * from hoge;

OBJECT audit log is not logged here since pg_class.rel_acl is empty
yet. (Only logged SESSION log)
So after creating another unconcerned role and grant any privilege to that user,
OBJECT audit is logged successfully.

Yes, object auditing does not work until some grants have been made to
the audit role.

=# create role bar_user;
=# grant select on hoge to bar_user;
=# select * from hoge;
LOG: AUDIT: SESSION,4,1,READ,SELECT,,,select * from hoge;
LOG: AUDIT: OBJECT,4,1,READ,SELECT,TABLE,public.hoge,select * from hoge;

The both OBJCET and SESSION log are logged.

Looks right to me. If you don't want the session logging then disable
pg_audit.log.

Session and object logging are completely independent from each other:
one or the other, or both, or neither can be enabled at any time.

It means that OBJECT log is not logged just after creating table, even
if that table is touched by its owner.
To write OBJECT log, we need to grant privilege to role at least. right?

Exactly. Privileges must be granted to the audit role in order for
object auditing to work.

The table owner always can touch its table.
So does it lead that table owner can get its table information while
hiding OBJECT logging?

Yes, the table owner would be able to access the table without object
logging if grants to that table were not made to the audit role. That
would also be true for any other user that had grants on the table.

The purpose of object auditing is to allow more fine-grained control and
is intended to be used in situations where you only want to audit some
things, rather than all things. Logging everything is better done with
the session logging.

However, object logging does yield more information since it lists every
table that was touched by the statement, so there may be cases where
you'd like to object log everything. In that case I'd recommend writing
a bit of plpgsql code to create the grants.

Also I looked into latest patch again.
Here are two review comment.

1.

typedef struct
{
int64 statementId;
int64 substatementId;

Both statementId and substatementId could be negative number.
I think these should be uint64 instead.

True. I did this because printf formatting for uint64 seems to be vary
across platforms. int64 formatting is more standard and still gives
more than enough IDs.

I could change it back to uint64 if you have a portable way to modify
the sprintf at line 507.

2.
I got ERROR when executing function uses cursor.

1) create empty table (hoge table)
2) create test function as follows.

create function test() returns int as $$
declare
cur1 cursor for select * from hoge;
tmp int;
begin
open cur1;
fetch cur1 into tmp;
return tmp;
end$$
language plpgsql ;

3) execute test function (got ERROR)
=# select test();
LOG: AUDIT: SESSION,6,1,READ,SELECT,,,selecT test();
LOG: AUDIT: SESSION,6,2,FUNCTION,EXECUTE,FUNCTION,public.test,selecT test();
LOG: AUDIT: SESSION,6,3,READ,SELECT,,,select * from hoge
CONTEXT: PL/pgSQL function test() line 6 at OPEN
ERROR: pg_audit stack is already empty
STATEMENT: selecT test();

It seems like that the item in stack is already freed by deleting
pg_audit memory context (in MemoryContextDelete()),
before calling stack_pop in dropping of top-level Portal.

Good catch, I'll add this to my test cases and work on a fix. I think I
see a good way to approach it.

--
- David Steele
david@pgmasters.net

#18David Steele
david@pgmasters.net
In reply to: David Steele (#17)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

Hi Sawada,

On 3/25/15 9:24 AM, David Steele wrote:

On 3/25/15 7:46 AM, Sawada Masahiko wrote:

2.
I got ERROR when executing function uses cursor.

1) create empty table (hoge table)
2) create test function as follows.

create function test() returns int as $$
declare
cur1 cursor for select * from hoge;
tmp int;
begin
open cur1;
fetch cur1 into tmp;
return tmp;
end$$
language plpgsql ;

3) execute test function (got ERROR)
=# select test();
LOG: AUDIT: SESSION,6,1,READ,SELECT,,,selecT test();
LOG: AUDIT: SESSION,6,2,FUNCTION,EXECUTE,FUNCTION,public.test,selecT test();
LOG: AUDIT: SESSION,6,3,READ,SELECT,,,select * from hoge
CONTEXT: PL/pgSQL function test() line 6 at OPEN
ERROR: pg_audit stack is already empty
STATEMENT: selecT test();

It seems like that the item in stack is already freed by deleting
pg_audit memory context (in MemoryContextDelete()),
before calling stack_pop in dropping of top-level Portal.

This has been fixed and I have attached a new patch. I've seen this
with cursors before where the parent MemoryContext is freed before
control is returned to ProcessUtility. I think that's strange behavior
but there's not a lot I can do about it.

The code I put in to deal with this situation was not quite robust
enough so I had to harden it a bit more.

Let me know if you see any other issues.

Thanks,
--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v6.patchtext/plain; charset=UTF-8; name=pg_audit-v6.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index 195d447..d8e75f4 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -29,6 +29,7 @@ SUBDIRS = \
 		pageinspect	\
 		passwordcheck	\
 		pg_archivecleanup \
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..32bc6d9
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,20 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..b34df5a
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1688 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pg_audit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pg_audit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pg_audit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_PARAMETER		"PARAMETER"
+#define CLASS_READ			"READ"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* Function execution */
+	LOG_PARAMETER = (1 << 4),
+
+	/* SELECT */
+	LOG_READ = (1 << 5),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 6),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	int64 stackId;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static int64 statementTotal = 0;
+static int64 substatementTotal = 0;
+static int64 stackTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack to the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Create the unique stackId - used to keep the stack sane when memory
+	 * contexts are freed unexpectedly.
+	 */
+	stackItem->stackId = ++stackTotal;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop(int64 stackId)
+{
+	/* Make sure what we want to delete is at the top of the stack */
+	if (auditEventStack != NULL && auditEventStack->stackId == stackId)
+	{
+		MemoryContextDelete(auditEventStack->contextAudit);
+	}
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent, classifies it, then logs it if permissions were granted
+ * via roles or if the statement belongs in a class that is being logged.
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	MemoryContext contextOld;
+	StringInfoData auditStr;
+
+	/* By default put everything in the MISC class. */
+	enum LogClass class = LOG_MISC;
+	const char *className = CLASS_MISC;
+
+	/* Classify the statement using log stmt level and the command tag */
+	switch (stackItem->auditEvent.logStmtLevel)
+	{
+		case LOGSTMT_MOD:
+			className = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		case LOGSTMT_DDL:
+			className = CLASS_DDL;
+			class = LOG_DDL;
+
+		case LOGSTMT_ALL:
+			switch (stackItem->auditEvent.commandTag)
+			{
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					className = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				case T_VacuumStmt:
+				case T_ReindexStmt:
+					className = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				case T_DoStmt:
+					className = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * Only log the statement if:
+	 *
+	 * 1. If permissions were granted via roles
+	 * 2. The statement belongs to a class that is being logged
+	 */
+	if (!stackItem->auditEvent.granted && !(auditLogBitmap & class))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogBitmap & LOG_PARAMETER &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	ereport(LOG,
+		(errmsg("AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			className, auditStr.data),
+		 errhidestmt(true)));
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+audit_on_acl(Datum aclDatum,
+			 Oid auditOid,
+			 AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+audit_on_relation(Oid relOid,
+				  Oid auditOid,
+				  AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = audit_on_acl(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+audit_on_attribute(Oid relOid,
+				   AttrNumber attNum,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = audit_on_acl(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+audit_on_any_attribute(Oid relOid,
+					   Oid auditOid,
+					   Bitmapset *attributeSet,
+					   AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (audit_on_attribute(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			audit_on_attribute(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/* If this is the first rte then session log */
+		if (first)
+		{
+			auditEventStack->auditEvent.objectName = "";
+			auditEventStack->auditEvent.objectType = "";
+
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (audit_on_relation(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->selectedCols,
+											   ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEventStack->auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEventStack->auditEvent.granted =
+							audit_on_any_attribute(relOid, auditOid,
+												   rte->modifiedCols,
+												   auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Only do relation level logging if a grant was found. */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop(stackItem->stackId);
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute */
+		case OAT_FUNCTION_EXECUTE:
+			if (auditLogBitmap & LOG_FUNCTION)
+				log_function_execute(objectId);
+			break;
+
+		/* Log create */
+		case OAT_POST_CREATE:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter */
+		case OAT_POST_ALTER:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop */
+		case OAT_DROP:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessDrop *drop = arg;
+
+				if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+static ExecutorEnd_hook_type next_ExecutorEnd_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pg_audit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ExecutorEnd to pop statement audit event off the stack.
+ */
+static void
+pg_audit_ExecutorEnd_hook(QueryDesc *queryDesc)
+{
+	/* Call the next hook or standard function */
+	if (next_ExecutorEnd_hook)
+		next_ExecutorEnd_hook(queryDesc);
+	else
+		standard_ExecutorEnd(queryDesc);
+
+	/* Pop the audit event off the stack */
+	if (!internalStatement)
+	{
+		stack_pop(auditEventStack->stackId);
+	}
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pg_audit_ProcessUtility_hook(Node *parsetree,
+							 const char *queryString,
+							 ProcessUtilityContext context,
+							 ParamListInfo params,
+							 DestReceiver *dest,
+							 char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+	int64 stackId;
+
+	/* Allocate the audit event */
+	if (!IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackId = stackItem->stackId;
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap != 0 &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/* Process the audit event if there is one. */
+	if (stackItem != NULL)
+	{
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !stackItem->auditEvent.logged &&
+			!IsAbortedTransactionBlockState())
+			log_audit_event(stackItem);
+
+		stack_pop(stackId);
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pg_audit_object_access_hook(ObjectAccessType access,
+							Oid classId,
+							Oid objectId,
+							int subId,
+							void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&
+		auditLogBitmap & (LOG_DDL | LOG_FUNCTION))
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+	/* Continue only if session logging is enabled */
+	if (auditLogBitmap != LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type), schema,\n"
+				"       identity, command\n"
+				"  FROM pg_event_trigger_get_creation_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			bool	   isNull;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply addition data to current audit event */
+			auditEventStack->auditEvent.logStmtLevel =
+				GetCommandLogLevel(eventData->parsetree);
+			auditEventStack->auditEvent.commandTag =
+				nodeTag(eventData->parsetree);
+			auditEventStack->auditEvent.command =
+				CreateCommandTag(eventData->parsetree);
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 6);
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			auditEventStack->auditEvent.commandText =
+				TextDatumGetCString(
+					DirectFunctionCall1(pg_event_trigger_expand_command,
+										SPI_getbinval(spiTuple, spiTupDesc,
+													  7, &isNull)));
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type),\n"
+				"       schema_name, object_name, object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			char *schemaName;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			schemaName = SPI_getvalue(spiTuple, spiTupDesc, 5);
+
+			if (!(pg_strcasecmp(auditEventStack->auditEvent.objectType,
+							"TYPE") == 0 ||
+				  pg_strcasecmp(schemaName, "pg_toast") == 0))
+			{
+				auditEventStack->auditEvent.objectName =
+						SPI_getvalue(spiTuple, spiTupDesc, 7);
+
+				log_audit_event(auditEventStack);
+			}
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pg_audit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then then token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_PARAMETER) == 0)
+			class = LOG_PARAMETER;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pg_audit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pg_audit_log from extra (ignoring newval, which has already been
+ * converted to a bitmap above). Note that extra may not be set if the
+ * assignment is to be suppressed.
+ */
+static void
+assign_pg_audit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * This variable defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * This variables controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pg_audit_log,
+							   assign_pg_audit_log,
+							   NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pg_audit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook;
+
+	next_ExecutorEnd_hook = ExecutorEnd_hook;
+	ExecutorEnd_hook = pg_audit_ExecutorEnd_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pg_audit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pg_audit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/test/test.pl b/contrib/pg_audit/test/test.pl
new file mode 100755
index 0000000..b3f2443
--- /dev/null
+++ b/contrib/pg_audit/test/test.pl
@@ -0,0 +1,1433 @@
+#!/usr/bin/perl
+################################################################################
+# test.pl - pg_audit Unit Tests
+################################################################################
+
+################################################################################
+# Perl includes
+################################################################################
+use strict;
+use warnings;
+use Carp;
+
+use Getopt::Long;
+use Pod::Usage;
+use DBI;
+use Cwd qw(abs_path);
+use IPC::System::Simple qw(capture);
+
+################################################################################
+# Constants
+################################################################################
+use constant
+{
+	true  => 1,
+	false => 0
+};
+
+use constant
+{
+	CONTEXT_GLOBAL   => 'GLOBAL',
+	CONTEXT_DATABASE => 'DATABASE',
+	CONTEXT_ROLE	 => 'ROLE'
+};
+
+use constant
+{
+	CLASS			=> 'CLASS',
+
+	CLASS_DDL		=> 'DDL',
+	CLASS_FUNCTION	=> 'FUNCTION',
+	CLASS_MISC		=> 'MISC',
+	CLASS_PARAMETER => 'PARAMETER',
+	CLASS_READ		=> 'READ',
+	CLASS_WRITE		=> 'WRITE',
+
+	CLASS_ALL		=> 'ALL',
+	CLASS_NONE		=> 'NONE'
+};
+
+use constant
+{
+	COMMAND						=> 'COMMAND',
+	COMMAND_LOG					=> 'COMMAND_LOG',
+
+	COMMAND_ANALYZE					=> 'ANALYZE',
+	COMMAND_ALTER_AGGREGATE			=> 'ALTER AGGREGATE',
+	COMMAND_ALTER_COLLATION			=> 'ALTER COLLATION',
+	COMMAND_ALTER_CONVERSION		=> 'ALTER CONVERSION',
+	COMMAND_ALTER_DATABASE			=> 'ALTER DATABASE',
+	COMMAND_ALTER_ROLE				=> 'ALTER ROLE',
+	COMMAND_ALTER_ROLE_SET			=> 'ALTER ROLE SET',
+	COMMAND_ALTER_TABLE				=> 'ALTER TABLE',
+	COMMAND_ALTER_TABLE_COLUMN		=> 'ALTER TABLE COLUMN',
+	COMMAND_ALTER_TABLE_INDEX		=> 'ALTER TABLE INDEX',
+	COMMAND_BEGIN					=> 'BEGIN',
+	COMMAND_CLOSE					=> 'CLOSE CURSOR',
+	COMMAND_COMMIT					=> 'COMMIT',
+	COMMAND_COPY					=> 'COPY',
+	COMMAND_COPY_TO					=> 'COPY TO',
+	COMMAND_COPY_FROM				=> 'COPY FROM',
+	COMMAND_CREATE_AGGREGATE		=> 'CREATE AGGREGATE',
+	COMMAND_CREATE_COLLATION		=> 'CREATE COLLATION',
+	COMMAND_CREATE_CONVERSION		=> 'CREATE CONVERSION',
+	COMMAND_CREATE_DATABASE			=> 'CREATE DATABASE',
+	COMMAND_CREATE_INDEX			=> 'CREATE INDEX',
+	COMMAND_DEALLOCATE				=> 'DEALLOCATE',
+	COMMAND_DECLARE_CURSOR			=> 'DECLARE CURSOR',
+	COMMAND_DO						=> 'DO',
+	COMMAND_DISCARD_ALL				=> 'DISCARD ALL',
+	COMMAND_CREATE_FUNCTION			=> 'CREATE FUNCTION',
+	COMMAND_CREATE_ROLE				=> 'CREATE ROLE',
+	COMMAND_CREATE_SCHEMA			=> 'CREATE SCHEMA',
+	COMMAND_CREATE_TABLE			=> 'CREATE TABLE',
+	COMMAND_CREATE_TABLE_AS			=> 'CREATE TABLE AS',
+	COMMAND_DROP_DATABASE			=> 'DROP DATABASE',
+	COMMAND_DROP_SCHEMA				=> 'DROP SCHEMA',
+	COMMAND_DROP_TABLE				=> 'DROP TABLE',
+	COMMAND_DROP_TABLE_CONSTRAINT	=> 'DROP TABLE CONSTRAINT',
+	COMMAND_DROP_TABLE_INDEX		=> 'DROP TABLE INDEX',
+	COMMAND_DROP_TABLE_TOAST		=> 'DROP TABLE TOAST',
+	COMMAND_DROP_TABLE_TYPE			=> 'DROP TABLE TYPE',
+	COMMAND_EXECUTE					=> 'EXECUTE',
+	COMMAND_EXECUTE_READ			=> 'EXECUTE READ',
+	COMMAND_EXECUTE_WRITE			=> 'EXECUTE WRITE',
+	COMMAND_EXECUTE_FUNCTION		=> 'EXECUTE FUNCTION',
+	COMMAND_EXPLAIN					=> 'EXPLAIN',
+	COMMAND_FETCH					=> 'FETCH',
+	COMMAND_GRANT					=> 'GRANT',
+	COMMAND_INSERT					=> 'INSERT',
+	# COMMAND_PARAMETER				=> 'PARAMETER',
+	# COMMAND_PARAMETER_READ			=> 'PARAMETER_READ',
+	# COMMAND_PARAMETER_WRITE			=> 'PARAMETER_WRITE',
+	COMMAND_PREPARE					=> 'PREPARE',
+	COMMAND_PREPARE_READ			=> 'PREPARE READ',
+	COMMAND_PREPARE_WRITE			=> 'PREPARE WRITE',
+	COMMAND_REVOKE					=> 'REVOKE',
+	COMMAND_SELECT					=> 'SELECT',
+	COMMAND_SET						=> 'SET',
+	COMMAND_UPDATE					=> 'UPDATE'
+};
+
+use constant
+{
+	TYPE					=> 'TYPE',
+	TYPE_NONE				=> '',
+
+	TYPE_AGGREGATE			=> 'AGGREGATE',
+	TYPE_COLLATION			=> 'COLLATION',
+	TYPE_CONVERSION			=> 'CONVERSION',
+	TYPE_SCHEMA				=> 'SCHEMA',
+	TYPE_FUNCTION			=> 'FUNCTION',
+	TYPE_INDEX				=> 'INDEX',
+	TYPE_TABLE				=> 'TABLE',
+	TYPE_TABLE_COLUMN		=> 'TABLE COLUMN',
+	TYPE_TABLE_CONSTRAINT	=> 'TABLE CONSTRAINT',
+	TYPE_TABLE_TOAST		=> 'TABLE TOAST',
+	TYPE_TYPE				=> 'TYPE'
+};
+
+use constant
+{
+	NAME			=> 'NAME'
+};
+
+################################################################################
+# Command line parameters
+################################################################################
+my $strPgSqlBin = '../../../../bin/bin';	# Path of PG binaries to use for
+											# this test
+my $strTestPath = '../../../../data';		# Path where testing will occur
+my $iDefaultPort = 6000;					# Default port to run Postgres on
+my $bHelp = false;							# Display help
+my $bQuiet = false;							# Supress output except for errors
+my $bNoCleanup = false;						# Cleanup database on exit
+
+GetOptions ('q|quiet' => \$bQuiet,
+			'no-cleanup' => \$bNoCleanup,
+			'help' => \$bHelp,
+			'pgsql-bin=s' => \$strPgSqlBin,
+			'test-path=s' => \$strTestPath)
+	or pod2usage(2);
+
+# Display version and exit if requested
+if ($bHelp)
+{
+	print 'pg_audit unit test\n\n';
+	pod2usage();
+
+	exit 0;
+}
+
+################################################################################
+# Global variables
+################################################################################
+my $hDb;					# Connection to Postgres
+my $strLogExpected = '';	# The expected log compared with grepping AUDIT
+							# entries from the postgres log.
+
+my $strDatabase = 'postgres';	# Connected database (modified by PgSetDatabase)
+my $strUser = 'postgres';		# Connected user (modified by PgSetUser)
+my $strAuditRole = 'audit';		# Role to use for auditing
+
+my %oAuditLogHash;				# Hash to store pg_audit.log GUCS
+my %oAuditGrantHash;			# Hash to store pg_audit grants
+
+my $strCurrentAuditLog;		# pg_audit.log setting was Postgres was started with
+my $strTemporaryAuditLog;	# pg_audit.log setting that was set hot
+
+################################################################################
+# Stores the mapping between commands, classes, and types
+################################################################################
+my %oCommandHash =
+(&COMMAND_ANALYZE => {
+	&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_AGGREGATE => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_AGGREGATE},
+	&COMMAND_ALTER_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_COLLATION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_COLLATION},
+	&COMMAND_ALTER_CONVERSION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_CONVERSION},
+	&COMMAND_ALTER_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_ALTER_ROLE_SET => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_ALTER_ROLE},
+	&COMMAND_ALTER_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_ALTER_TABLE_COLUMN => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_COLUMN, &COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_ALTER_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_ALTER_TABLE},
+	&COMMAND_BEGIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CLOSE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COMMIT => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_COPY_FROM => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_COPY_TO => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_COPY},
+	&COMMAND_CREATE_AGGREGATE => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_AGGREGATE},
+	&COMMAND_CREATE_CONVERSION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_CONVERSION},
+	&COMMAND_CREATE_COLLATION => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_COLLATION},
+	&COMMAND_CREATE_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX},
+	&COMMAND_DEALLOCATE => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_DECLARE_CURSOR => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_DO => {&CLASS => &CLASS_FUNCTION, &TYPE => &TYPE_NONE},
+	&COMMAND_DISCARD_ALL => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_FUNCTION => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_FUNCTION},
+	&COMMAND_CREATE_ROLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_CREATE_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_SCHEMA},
+	&COMMAND_CREATE_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_CREATE_TABLE_AS => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_DATABASE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_SCHEMA => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_NONE},
+	&COMMAND_DROP_TABLE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_DROP_TABLE_CONSTRAINT => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_CONSTRAINT, &COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_INDEX => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_INDEX,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TOAST => {&CLASS => &CLASS_DDL,
+		&TYPE => &TYPE_TABLE_TOAST, &COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_DROP_TABLE_TYPE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TYPE,
+		&COMMAND => &COMMAND_DROP_TABLE},
+	&COMMAND_EXECUTE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXECUTE_FUNCTION => {&CLASS => &CLASS_FUNCTION,
+		&TYPE => &TYPE_FUNCTION, &COMMAND => &COMMAND_EXECUTE},
+	&COMMAND_EXPLAIN => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_FETCH => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_GRANT => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_PREPARE_READ => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_PREPARE_WRITE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE,
+		&COMMAND => &COMMAND_PREPARE},
+	&COMMAND_INSERT => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE},
+	&COMMAND_REVOKE => {&CLASS => &CLASS_DDL, &TYPE => &TYPE_TABLE},
+	&COMMAND_SELECT => {&CLASS => &CLASS_READ, &TYPE => &TYPE_NONE},
+	&COMMAND_SET => {&CLASS => &CLASS_MISC, &TYPE => &TYPE_NONE},
+	&COMMAND_UPDATE => {&CLASS => &CLASS_WRITE, &TYPE => &TYPE_NONE}
+);
+
+################################################################################
+# CommandExecute
+################################################################################
+sub CommandExecute
+{
+	my $strCommand = shift;
+	my $bSuppressError = shift;
+
+	# Set default
+	$bSuppressError = defined($bSuppressError) ? $bSuppressError : false;
+
+	# Run the command
+	my $iResult = system($strCommand);
+
+	if ($iResult != 0 && !$bSuppressError)
+	{
+		confess "command '${strCommand}' failed with error ${iResult}";
+	}
+}
+
+################################################################################
+# log
+################################################################################
+sub log
+{
+	my $strMessage = shift;
+	my $bError = shift;
+
+	# Set default
+	$bError = defined($bError) ? $bError : false;
+
+	if (!$bQuiet)
+	{
+		print "${strMessage}\n";
+	}
+
+	if ($bError)
+	{
+		exit 1;
+	}
+}
+
+################################################################################
+# ArrayToString
+################################################################################
+sub ArrayToString
+{
+	my @stryArray = @_;
+
+	my $strResult = '';
+
+	for (my $iIndex = 0; $iIndex < @stryArray; $iIndex++)
+	{
+		if ($iIndex != 0)
+		{
+			$strResult .= ', ';
+		}
+
+		$strResult .= $stryArray[$iIndex];
+	}
+
+	return $strResult;
+}
+
+################################################################################
+# BuildModule
+################################################################################
+sub BuildModule
+{
+	capture('cd ..;make');
+	CommandExecute("cp ../pg_audit.so" .
+				   " ${strPgSqlBin}/../lib/postgresql");
+	CommandExecute("cp ../pg_audit.control" .
+				   " ${strPgSqlBin}/../share/postgresql/extension");
+	CommandExecute("cp ../pg_audit--1.0.0.sql" .
+				   " ${strPgSqlBin}/../share/postgresql/extension");
+}
+
+################################################################################
+# PgConnect
+################################################################################
+sub PgConnect
+{
+	my $iPort = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+
+	# Log Connection
+	&log("   DB: connect user ${strUser}, database ${strDatabase}");
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# Connect to the db
+	$hDb = DBI->connect("dbi:Pg:dbname=${strDatabase};port=${iPort};host=/tmp",
+						$strUser, undef,
+						{AutoCommit => 1, RaiseError => 1});
+}
+
+################################################################################
+# PgDisconnect
+################################################################################
+sub PgDisconnect
+{
+	# Connect to the db (whether it is local or remote)
+	if (defined($hDb))
+	{
+		$hDb->disconnect;
+		undef($hDb);
+	}
+}
+
+################################################################################
+# PgExecute
+################################################################################
+sub PgExecute
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	my $hStatement = $hDb->prepare($strSql);
+
+	$hStatement->execute();
+	$hStatement->finish();
+}
+
+################################################################################
+# PgExecuteOnly
+################################################################################
+sub PgExecuteOnly
+{
+	my $strSql = shift;
+
+	# Log the statement
+	&log("  SQL: ${strSql}");
+
+	# Execute the statement
+	$hDb->do($strSql);
+}
+
+################################################################################
+# PgSetDatabase
+################################################################################
+sub PgSetDatabase
+{
+	my $strDatabaseParam = shift;
+
+	# Stop and start the database to reset pgconf entries
+	PgStop();
+	PgStart();
+
+	# Execute the statement
+	$strDatabase = $strDatabaseParam;
+	PgConnect();
+}
+
+################################################################################
+# PgSetUser
+################################################################################
+sub PgSetUser
+{
+	my $strUserParam = shift;
+
+	$strUser = $strUserParam;
+
+	# Stop and start the database to reset pgconf entries
+	if ((defined($strTemporaryAuditLog) && !defined($strCurrentAuditLog)) ||
+		(defined($strCurrentAuditLog) && !defined($strTemporaryAuditLog)) ||
+		$strCurrentAuditLog ne $strTemporaryAuditLog)
+	{
+		$strCurrentAuditLog = $strTemporaryAuditLog;
+
+		PgStop();
+		PgStart();
+	}
+	else
+	{
+		# Execute the statement
+		PgConnect();
+	}
+}
+
+################################################################################
+# SaveString
+################################################################################
+sub SaveString
+{
+	my $strFile = shift;
+	my $strString = shift;
+
+	# Open the file for writing
+	my $hFile;
+
+	open($hFile, '>', $strFile)
+		or confess "unable to open ${strFile}";
+
+	if ($strString ne '')
+	{
+		syswrite($hFile, $strString)
+			or confess "unable to write to ${strFile}: $!";
+	}
+
+	close($hFile);
+}
+
+################################################################################
+# PgLogExecute
+################################################################################
+sub PgLogExecute
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $oData = shift;
+	my $bExecute = shift;
+	my $bWait = shift;
+	my $bLogSql = shift;
+	my $strParameter = shift;
+	my $bExpectError = shift;
+
+	# Set defaults
+	$bExecute = defined($bExecute) ? $bExecute : true;
+	$bWait = defined($bWait) ? $bWait : true;
+	$bLogSql = defined($bLogSql) ? $bLogSql : true;
+
+	if ($bExecute)
+	{
+		eval
+		{
+			PgExecuteOnly($strSql);
+		};
+
+		if ($@ && !$bExpectError)
+		{
+			confess $@;
+		}
+	}
+
+	PgLogExpect($strCommand, $bLogSql ? $strSql : '', $strParameter, $oData);
+
+	if ($bWait)
+	{
+		PgLogWait();
+	}
+}
+
+################################################################################
+# QuoteCSV
+################################################################################
+sub QuoteCSV
+{
+	my $strCSV = shift;
+
+	if (defined($strCSV) &&
+		(index($strCSV, ',') >= 0 || index($strCSV, '"') > 0 ||
+		 index($strCSV, "\n") > 0 || index($strCSV, "\r") >= 0))
+	{
+		$strCSV =~ s/"/""/g;
+		$strCSV = "\"${strCSV}\"";
+	}
+
+	return $strCSV;
+}
+
+################################################################################
+# PgLogExpect
+################################################################################
+sub PgLogExpect
+{
+	my $strCommand = shift;
+	my $strSql = shift;
+	my $strParameter = shift;
+	my $oData = shift;
+
+	# If oData is false then no logging
+	if (defined($oData) && ref($oData) eq '' && !$oData)
+	{
+		return;
+	}
+
+	# Quote SQL if needs to be quoted
+	$strSql = QuoteCSV($strSql);
+
+	if (defined($strParameter))
+	{
+		$strSql .= ",${strParameter}";
+	}
+
+	# Log based on session
+	if (PgShouldLog($strCommand))
+	{
+		# Make sure class is defined
+		my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+		if (!defined($strClass))
+		{
+			confess "class is not defined for command ${strCommand}";
+		}
+
+		# Make sure object type is defined
+		my $strObjectType = $oCommandHash{$strCommand}{&TYPE};
+
+		if (!defined($strObjectType))
+		{
+			confess "object type is not defined for command ${strCommand}";
+		}
+
+		# Check for command override
+		my $strCommandLog = $strCommand;
+
+		if ($oCommandHash{$strCommand}{&COMMAND})
+		{
+			$strCommandLog = $oCommandHash{$strCommand}{&COMMAND};
+		}
+
+		my $strObjectName = '';
+
+		if (defined($oData) && ref($oData) ne 'ARRAY')
+		{
+			$strObjectName = QuoteCSV($oData);
+		}
+
+		my $strLog .= "SESSION,${strClass},${strCommandLog}," .
+					  "${strObjectType},${strObjectName},${strSql}";
+		&log("AUDIT: ${strLog}");
+
+		$strLogExpected .= "${strLog}\n";
+	}
+
+	# Log based on grants
+	if (ref($oData) eq 'ARRAY' && ($strCommand eq COMMAND_SELECT ||
+		$oCommandHash{$strCommand}{&CLASS} eq CLASS_WRITE))
+	{
+		foreach my $oTableHash (@{$oData})
+		{
+			my $strObjectName = QuoteCSV(${$oTableHash}{&NAME});
+			my $strCommandLog = ${$oTableHash}{&COMMAND};
+
+			if (defined($oAuditGrantHash{$strAuditRole}
+										{$strObjectName}{$strCommandLog}))
+			{
+				my $strCommandLog = defined(${$oTableHash}{&COMMAND_LOG}) ?
+					${$oTableHash}{&COMMAND_LOG} : $strCommandLog;
+				my $strClass = $oCommandHash{$strCommandLog}{&CLASS};
+				my $strObjectType = ${$oTableHash}{&TYPE};
+
+				my $strLog .= "OBJECT,${strClass},${strCommandLog}," .
+							  "${strObjectType},${strObjectName},${strSql}";
+				&log("AUDIT: ${strLog}");
+
+				$strLogExpected .= "${strLog}\n";
+			}
+		}
+
+		$oData = undef;
+	}
+}
+
+################################################################################
+# PgShouldLog
+################################################################################
+sub PgShouldLog
+{
+	my $strCommand = shift;
+
+	# Make sure class is defined
+	my $strClass = $oCommandHash{$strCommand}{&CLASS};
+
+	if (!defined($strClass))
+	{
+		confess "class is not defined for command ${strCommand}";
+	}
+
+	# Check logging for the role
+	my $bLog = undef;
+
+	if (defined($oAuditLogHash{&CONTEXT_ROLE}{$strUser}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_ROLE}{$strUser}{$strClass};
+	}
+
+	# Else check logging for the db
+	elsif (defined($oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_DATABASE}{$strDatabase}{$strClass};
+	}
+
+	# Else check logging for global
+	elsif (defined($oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}))
+	{
+		$bLog = $oAuditLogHash{&CONTEXT_GLOBAL}{&CONTEXT_GLOBAL}{$strClass};
+	}
+
+	return defined($bLog) ? true : false;
+}
+
+################################################################################
+# PgLogWait
+################################################################################
+sub PgLogWait
+{
+	my $strLogActual;
+
+	# Run in an eval block since grep returns 1 when nothing was found
+	eval
+	{
+		$strLogActual = capture("grep 'LOG:  AUDIT: '" .
+								" ${strTestPath}/postgresql.log");
+	};
+
+	# If an error was returned, continue if it was 1, otherwise confess
+	if ($@)
+	{
+		my $iExitStatus = $? >> 8;
+
+		if ($iExitStatus != 1)
+		{
+			confess "grep returned ${iExitStatus}";
+		}
+
+		$strLogActual = '';
+	}
+
+	# Strip the AUDIT and timestamp from the actual log
+	$strLogActual =~ s/prefix LOG:  AUDIT\: //g;
+	$strLogActual =~ s/SESSION,[0-9]+,[0-9]+,/SESSION,/g;
+	$strLogActual =~ s/OBJECT,[0-9]+,[0-9]+,/OBJECT,/g;
+
+	# Save the logs
+	SaveString("${strTestPath}/audit.actual", $strLogActual);
+	SaveString("${strTestPath}/audit.expected", $strLogExpected);
+
+	CommandExecute("diff ${strTestPath}/audit.expected" .
+				   " ${strTestPath}/audit.actual");
+}
+
+################################################################################
+# PgDrop
+################################################################################
+sub PgDrop
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Stop the cluster
+	PgStop(true, $strPath);
+
+	# Remove the directory
+	CommandExecute("rm -rf ${strTestPath}");
+}
+
+################################################################################
+# PgCreate
+################################################################################
+sub PgCreate
+{
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	CommandExecute("${strPgSqlBin}/initdb -D ${strPath} -U ${strUser}" .
+				   ' -A trust > /dev/null');
+}
+
+################################################################################
+# PgStop
+################################################################################
+sub PgStop
+{
+	my $bImmediate = shift;
+	my $strPath = shift;
+
+	# Set default
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+	$bImmediate = defined($bImmediate) ? $bImmediate : false;
+
+	# Disconnect user session
+	PgDisconnect();
+
+	# If postmaster process is running then stop the cluster
+	if (-e $strPath . '/postmaster.pid')
+	{
+		CommandExecute("${strPgSqlBin}/pg_ctl stop -D ${strPath} -w -s -m " .
+					  ($bImmediate ? 'immediate' : 'fast'));
+	}
+}
+
+################################################################################
+# PgStart
+################################################################################
+sub PgStart
+{
+	my $iPort = shift;
+	my $strPath = shift;
+
+	# Set default
+	$iPort = defined($iPort) ? $iPort : $iDefaultPort;
+	$strPath = defined($strPath) ? $strPath : $strTestPath;
+
+	# Make sure postgres is not running
+	if (-e $strPath . '/postmaster.pid')
+	{
+		confess "${strPath}/postmaster.pid exists, cannot start";
+	}
+
+	# Start the cluster
+	CommandExecute("${strPgSqlBin}/pg_ctl start -o \"" .
+				   "-c port=${iPort}" .
+				   " -c unix_socket_directories='/tmp'" .
+				   " -c shared_preload_libraries='pg_audit'" .
+				   " -c log_min_messages=debug1" .
+				   " -c log_line_prefix='prefix '" .
+				   " -c log_statement=all" .
+				   (defined($strCurrentAuditLog) ?
+					   " -c pg_audit.log='${strCurrentAuditLog}'" : '') .
+				   " -c pg_audit.role='${strAuditRole}'" .
+				   " -c log_connections=on" .
+				   "\" -D ${strPath} -l ${strPath}/postgresql.log -w -s");
+
+	# Connect user session
+	PgConnect();
+}
+
+################################################################################
+# PgAuditLogSet
+################################################################################
+sub PgAuditLogSet
+{
+	my $strContext = shift;
+	my $strName = shift;
+	my @stryClass = @_;
+
+	# Create SQL to set the GUC
+	my $strCommand;
+	my $strSql;
+
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		$strCommand = COMMAND_SET;
+		$strSql = "set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+		$strTemporaryAuditLog = ArrayToString(@stryClass);
+	}
+	elsif ($strContext eq CONTEXT_ROLE)
+	{
+		$strCommand = COMMAND_ALTER_ROLE_SET;
+		$strSql = "alter role ${strName} set pg_audit.log = '" .
+				  ArrayToString(@stryClass) . "'";
+	}
+	else
+	{
+		confess "unable to set pg_audit.log for context ${strContext}";
+	}
+
+	# Reset the audit log
+	if ($strContext eq CONTEXT_GLOBAL)
+	{
+		delete($oAuditLogHash{$strContext});
+		$strName = CONTEXT_GLOBAL;
+	}
+	else
+	{
+		delete($oAuditLogHash{$strContext}{$strName});
+	}
+
+	# Store all the classes in the hash and build the GUC
+	foreach my $strClass (@stryClass)
+	{
+		if ($strClass eq CLASS_ALL)
+		{
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_DDL} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_FUNCTION} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_MISC} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_READ} = true;
+			$oAuditLogHash{$strContext}{$strName}{&CLASS_WRITE} = true;
+		}
+
+		if (index($strClass, '-') == 0)
+		{
+			$strClass = substr($strClass, 1);
+
+			delete($oAuditLogHash{$strContext}{$strName}{$strClass});
+		}
+		else
+		{
+			$oAuditLogHash{$strContext}{$strName}{$strClass} = true;
+		}
+	}
+
+	PgLogExecute($strCommand, $strSql);
+}
+
+################################################################################
+# PgAuditGrantSet
+################################################################################
+sub PgAuditGrantSet
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_GRANT, "GRANT " .
+								(defined($strColumn) ?
+									lc(${strPrivilege}) ." (${strColumn})" :
+									uc(${strPrivilege})) .
+								" ON TABLE ${strObject} TO ${strRole} ");
+
+	$oAuditGrantHash{$strRole}{$strObject}{$strPrivilege} = true;
+}
+
+################################################################################
+# PgAuditGrantReset
+################################################################################
+sub PgAuditGrantReset
+{
+	my $strRole = shift;
+	my $strPrivilege = shift;
+	my $strObject = shift;
+	my $strColumn = shift;
+
+	# Create SQL to set the grant
+	PgLogExecute(COMMAND_REVOKE, "REVOKE  " . uc(${strPrivilege}) .
+				 (defined($strColumn) ? " (${strColumn})" : '') .
+				 " ON TABLE ${strObject} FROM ${strRole} ");
+
+	delete($oAuditGrantHash{$strRole}{$strObject}{$strPrivilege});
+}
+
+################################################################################
+# Main
+################################################################################
+my @oyTable;	   # Store table info for select, insert, update, delete
+my $strSql;		# Hold Sql commands
+
+# Drop the old cluster, build the code, and create a new cluster
+PgDrop();
+BuildModule();
+PgCreate();
+PgStart();
+
+PgExecute("create extension pg_audit");
+
+# Create test users and the audit role
+PgExecute("create user user1");
+PgExecute("create user user2");
+PgExecute("create role ${strAuditRole}");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL));
+
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_READ, CLASS_WRITE));
+
+# User1 follows the global log settings
+PgSetUser('user1');
+
+$strSql = 'CREATE  TABLE  public.test (id pg_catalog.int4   )' .
+		  '  WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test');
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+$strSql = 'drop table test';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'public.test');
+
+PgSetUser('user2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test2 (id int)', 'public.test2');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test2');
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test3 (id int)', 'public.test2');
+
+# Catalog select should not log
+PgLogExecute(COMMAND_SELECT, 'select * from pg_class limit 1',
+							   false);
+
+# Multi-table select
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test3, test2',
+							   \@oyTable);
+
+# Various CTE combinations
+PgAuditGrantSet($strAuditRole, &COMMAND_INSERT, 'public.test3');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (select id from test2)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (insert into test3 values (1) returning id)' .
+			 ' insert into test2 select id from cte',
+			 \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE, 'public.test2');
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_INSERT,
+			 'with cte as (update test2 set id = 1 returning id)' .
+			 ' insert into test3 select id from cte',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.test3', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT},
+			{&NAME => 'public.test2', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_INSERT});
+PgLogExecute(COMMAND_UPDATE,
+			 'with cte as (insert into test2 values (1) returning id)' .
+			 ' update test3 set id = cte.id' .
+			 ' from cte where test3.id <> cte.id',
+			 \@oyTable);
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user2', (CLASS_NONE));
+PgSetUser('user2');
+
+# Column-based audits
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table test4 (id int, name text)', 'public.test4');
+PgAuditGrantSet($strAuditRole, COMMAND_SELECT, 'public.test4', 'name');
+PgAuditGrantSet($strAuditRole, COMMAND_UPDATE, 'public.test4', 'id');
+PgAuditGrantSet($strAuditRole, COMMAND_INSERT, 'public.test4', 'name');
+
+# Select
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id from public.test4',
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from public.test4',
+							  \@oyTable);
+
+# Insert
+@oyTable = ();
+PgLogExecute(COMMAND_INSERT, 'insert into public.test4 (id) values (1)',
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_INSERT});
+PgLogExecute(COMMAND_INSERT, "insert into public.test4 (name) values ('test')",
+							  \@oyTable);
+
+# Update
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set name = 'foo'",
+							   \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update public.test4 set id = 1",
+							  \@oyTable);
+
+@oyTable = ({&NAME => 'public.test4', &TYPE => &TYPE_TABLE,
+			&COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update public.test4 set name = 'foo' where name = 'bar'",
+			 \@oyTable);
+
+# Drop test tables
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test2", 'public.test2');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test3", 'public.test3');
+PgLogExecute(COMMAND_DROP_TABLE, "drop table test4", 'public.test4');
+
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Create some email friendly tests.  These first tests are session logging only.
+PgSetUser('postgres');
+
+&log("\nExamples:");
+
+&log("\nSession Audit:\n");
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_DDL, CLASS_READ));
+
+PgSetUser('user1');
+
+$strSql = 'CREATE  TABLE  public.account (id pg_catalog.int4   ,' .
+		  ' name pg_catalog.text   COLLATE pg_catalog."default", ' .
+		  'password pg_catalog.text   COLLATE pg_catalog."default", '.
+		  'description pg_catalog.text   COLLATE pg_catalog."default")  '.
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.account');
+PgLogExecute(COMMAND_SELECT,
+			 'select * from account');
+PgLogExecute(COMMAND_INSERT,
+			 "insert into account (id, name, password, description)" .
+			 " values (1, 'user1', 'HASH1', 'blah, blah')");
+&log("AUDIT: <nothing logged>");
+
+# Now tests for object logging
+&log("\nObject Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_NONE));
+PgExecute("set pg_audit.role = 'audit'");
+PgSetUser('user1');
+
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account', 'password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_SELECT, 'select id, name from account',
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+PgAuditGrantSet($strAuditRole, &COMMAND_UPDATE,
+				'public.account', 'name, password');
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Now tests for session/object logging
+&log("\nSession/Object Audit:\n");
+
+PgSetUser('postgres');
+PgAuditLogSet(CONTEXT_ROLE, 'user1', (CLASS_READ, CLASS_WRITE));
+PgSetUser('user1');
+
+PgLogExecute(COMMAND_CREATE_TABLE,
+			 'create table account_role_map (account_id int, role_id int)',
+			 'public.account_role_map');
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.account_role_map');
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT},
+			{&NAME => 'public.account_role_map', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT,
+			 'select account.password, account_role_map.role_id from account' .
+			 ' inner join account_role_map' .
+			 ' on account.id = account_role_map.account_id',
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select password from account',
+							  \@oyTable);
+
+@oyTable = ();
+PgLogExecute(COMMAND_UPDATE, "update account set description = 'yada, yada'",
+							  \@oyTable);
+&log("AUDIT: <nothing logged>");
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT, &COMMAND_LOG => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE,
+			 "update account set description = 'yada, yada'" .
+			 " where password = 'HASH2'",
+			 \@oyTable);
+
+@oyTable = ({&NAME => 'public.account', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_UPDATE});
+PgLogExecute(COMMAND_UPDATE, "update account set password = 'HASH2'",
+							  \@oyTable);
+
+# Test all sql commands
+&log("\nExhaustive Command Tests:\n");
+
+PgSetUser('postgres');
+
+PgAuditLogSet(CONTEXT_GLOBAL, undef, (CLASS_ALL));
+PgLogExecute(COMMAND_SET, "set pg_audit.role = 'audit'");
+
+PgLogExecute(COMMAND_DO, "do \$\$\ begin raise notice 'test'; end; \$\$;");
+
+$strSql = 'CREATE SCHEMA  test ';
+PgLogExecute(COMMAND_CREATE_SCHEMA, $strSql, 'test');
+
+# Test COPY
+PgLogExecute(COMMAND_COPY_TO,
+			 "COPY pg_class to '" . abs_path($strTestPath) . "/class.out'");
+
+$strSql = 'CREATE  TABLE  test.pg_class  WITH (oids=OFF)   AS SELECT relname,' .
+		  ' relnamespace, reltype, reloftype, relowner, relam, relfilenode, ' .
+		  'reltablespace, relpages, reltuples, relallvisible, reltoastrelid, ' .
+		  'relhasindex, relisshared, relpersistence, relkind, relnatts, ' .
+		  'relchecks, relhasoids, relhaspkey, relhasrules, relhastriggers, ' .
+		  'relhassubclass, relrowsecurity, relispopulated, relreplident, ' .
+		  'relfrozenxid, relminmxid, relacl, reloptions ' .
+		  'FROM pg_catalog.pg_class ';
+PgLogExecute(COMMAND_INSERT, $strSql, undef, true, false);
+PgLogExecute(COMMAND_CREATE_TABLE_AS, $strSql, 'test.pg_class', false, true);
+
+$strSql = "COPY test.pg_class from '" . abs_path($strTestPath) . "/class.out'";
+PgLogExecute(COMMAND_INSERT, $strSql);
+#PgLogExecute(COMMAND_COPY_FROM, $strSql, undef, false, true);
+
+# Test prepared SELECT
+PgLogExecute(COMMAND_PREPARE_READ,
+			 'PREPARE pgclassstmt (oid) as select *' .
+			 ' from pg_class where oid = $1');
+PgLogExecute(COMMAND_EXECUTE_READ,
+			 'EXECUTE pgclassstmt (1)');
+PgLogExecute(COMMAND_DEALLOCATE,
+			 'DEALLOCATE pgclassstmt');
+
+# Test cursor
+PgLogExecute(COMMAND_BEGIN,
+			 'BEGIN');
+PgLogExecute(COMMAND_DECLARE_CURSOR,
+			 'DECLARE ctest SCROLL CURSOR FOR SELECT * FROM pg_class');
+PgLogExecute(COMMAND_FETCH,
+			 'FETCH NEXT FROM ctest');
+PgLogExecute(COMMAND_CLOSE,
+			 'CLOSE ctest');
+PgLogExecute(COMMAND_COMMIT,
+			 'COMMIT');
+
+# Test prepared INSERT
+$strSql = 'CREATE  TABLE  test.test_insert (id pg_catalog.int4   )  ' .
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'test.test_insert');
+
+$strSql = 'PREPARE pgclassstmt (oid) as insert into test.test_insert (id) ' .
+		  'values ($1)';
+PgLogExecute(COMMAND_PREPARE_WRITE, $strSql);
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, "1");
+
+$strSql = 'EXECUTE pgclassstmt (1)';
+PgLogExecute(COMMAND_EXECUTE_WRITE, $strSql, undef, true, true);
+
+# Create a table with a primary key
+$strSql = 'CREATE  TABLE  public.test (id pg_catalog.int4   , ' .
+		  'name pg_catalog.text   COLLATE pg_catalog."default", description ' .
+		  'pg_catalog.text   COLLATE pg_catalog."default", CONSTRAINT ' .
+		  'test_pkey PRIMARY KEY (id))  WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_INDEX, $strSql, 'public.test_pkey', true, false);
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test', false, true);
+
+PgLogExecute(COMMAND_ANALYZE, 'analyze test');
+
+# Grant select to public - this should have no affect on auditing
+$strSql = 'GRANT SELECT ON TABLE public.test TO PUBLIC ';
+PgLogExecute(COMMAND_GRANT, $strSql);
+
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+
+# Now grant select to audit and it should be logged
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test');
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select * from test', \@oyTable);
+
+# Check columns granted to public and make sure they do not log
+PgAuditGrantReset($strAuditRole, &COMMAND_SELECT, 'public.test');
+
+$strSql = 'GRANT select (name) ON TABLE public.test TO PUBLIC ';
+PgLogExecute(COMMAND_GRANT, $strSql);
+
+PgLogExecute(COMMAND_SELECT, 'select * from test');
+PgLogExecute(COMMAND_SELECT, 'select from test');
+
+# Try a select that does not reference any tables
+PgLogExecute(COMMAND_SELECT, 'select 1, current_timestamp');
+
+# Now try the same in a do block
+$strSql = 'do $$ declare test int; begin select 1 into test; end $$';
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'select 1';
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, true);
+
+# Insert some data into test and try a loop in a do block
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (1)');
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (2)');
+PgLogExecute(COMMAND_INSERT, 'insert into test (id) values (3)');
+
+$strSql = 'do $$ ' .
+		  'declare ' .
+		  '	result record;' .
+		  'begin ' .
+		  '	for result in select id from test loop ' .
+		  '		insert into test (id) values (result.id + 100); ' .
+		  '	end loop; ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'select id from test';
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false);
+
+$strSql = 'insert into test (id) values (result.id + 100)';
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+PgLogExecute(COMMAND_INSERT, $strSql, undef, false, false, undef, ",,");
+
+# Test EXECUTE with bind
+$strSql = "select * from test where id = ?";
+my $hStatement = $hDb->prepare($strSql);
+
+$strSql = "select * from test where id = \$1";
+$hStatement->bind_param(1, 101);
+$hStatement->execute();
+
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false, undef, "101");
+
+$hStatement->bind_param(1, 103);
+$hStatement->execute();
+
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, false, undef, "103");
+
+$hStatement->finish();
+
+# Cursors in a function block
+$strSql = "CREATE  FUNCTION public.test() RETURNS  pg_catalog.int4 LANGUAGE " .
+		  "plpgsql  VOLATILE  CALLED ON NULL INPUT SECURITY INVOKER COST 100 " .
+		  "  AS ' declare cur1 cursor for select * from hoge; tmp int; begin " .
+		  "create table hoge (id int); open cur1; fetch cur1 into tmp; close " .
+		  "cur1; return tmp; end'";
+
+PgLogExecute(COMMAND_CREATE_FUNCTION, $strSql, 'public.test()');
+
+$strSql = 'select public.test()';
+PgLogExecute(COMMAND_SELECT, $strSql, undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_FUNCTION, $strSql, 'public.test', false, false);
+
+$strSql = 'CREATE  TABLE  public.hoge (id pg_catalog.int4   )' .
+          '  WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.hoge', false, false);
+
+$strSql = 'select * from hoge';
+PgLogExecute(COMMAND_SELECT, $strSql, undef, false, true);
+#PgLogExecute(COMMAND_SELECT, 'select public.test()');
+
+# Now try some DDL in a do block
+$strSql = 'do $$ ' .
+		  'begin ' .
+		  '	create table test_block (id int); ' .
+		  '	drop table test_block; ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, true, false);
+
+$strSql = 'CREATE  TABLE  public.test_block (id pg_catalog.int4   )  ' .
+		  'WITH (oids=OFF)  ';
+PgLogExecute(COMMAND_CREATE_TABLE, $strSql, 'public.test_block', false, false);
+
+$strSql = 'drop table test_block';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'public.test_block', false, false);
+
+# Generate an error in a do block and make sure the stack gets cleaned up
+$strSql = 'do $$ ' .
+		  'begin ' .
+		  '	create table bobus.test_block (id int); ' .
+		  'end; $$';
+
+PgLogExecute(COMMAND_DO, $strSql, undef, undef, undef, undef, undef, true);
+# PgLogExecute(COMMAND_SELECT, 'select 1');
+# exit 0;
+
+# Try explain
+PgLogExecute(COMMAND_SELECT, 'explain select 1', undef, true, false);
+PgLogExecute(COMMAND_EXPLAIN, 'explain select 1', undef, false, true);
+
+# Now set grant to a specific column to audit and make sure it logs
+# Make sure the the converse is true
+PgAuditGrantSet($strAuditRole, &COMMAND_SELECT, 'public.test',
+				'name, description');
+PgLogExecute(COMMAND_SELECT, 'select id from test');
+
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select name from test', \@oyTable);
+
+# Test alter and drop table statements
+$strSql = 'ALTER TABLE public.test DROP COLUMN description ';
+PgLogExecute(COMMAND_ALTER_TABLE_COLUMN,
+			 $strSql, 'public.test.description', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE,
+			 $strSql, 'public.test', false, true);
+@oyTable = ({&NAME => 'public.test', &TYPE => &TYPE_TABLE,
+			 &COMMAND => &COMMAND_SELECT});
+PgLogExecute(COMMAND_SELECT, 'select from test', \@oyTable);
+
+$strSql = 'ALTER TABLE  public.test RENAME TO test2';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'public.test2');
+
+$strSql = 'ALTER TABLE public.test2 SET SCHEMA test';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'test.test2');
+
+$strSql = 'ALTER TABLE test.test2 ADD COLUMN description pg_catalog.text   ' .
+		  'COLLATE pg_catalog."default"';
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql, 'test.test2');
+
+$strSql = 'ALTER TABLE test.test2 DROP COLUMN description ';
+PgLogExecute(COMMAND_ALTER_TABLE_COLUMN, $strSql,
+			 'test.test2.description', true, false);
+PgLogExecute(COMMAND_ALTER_TABLE, $strSql,
+			 'test.test2', false, true);
+
+$strSql = 'drop table test.test2';
+PgLogExecute(COMMAND_DROP_TABLE, $strSql, 'test.test2', true, false);
+PgLogExecute(COMMAND_DROP_TABLE_CONSTRAINT, $strSql, 'test_pkey on test.test2',
+			 false, false);
+PgLogExecute(COMMAND_DROP_TABLE_INDEX, $strSql, 'test.test_pkey', false, true);
+
+$strSql = "CREATE  FUNCTION public.int_add(IN a pg_catalog.int4 , IN b " .
+		  "pg_catalog.int4 ) RETURNS  pg_catalog.int4 LANGUAGE plpgsql  " .
+		  "VOLATILE  CALLED ON NULL INPUT SECURITY INVOKER COST 100   AS '" .
+		  " begin return a + b; end '";
+PgLogExecute(COMMAND_CREATE_FUNCTION, $strSql,
+			 'public.int_add(integer,integer)');
+PgLogExecute(COMMAND_SELECT, "select int_add(1, 1)",
+							 undef, true, false);
+PgLogExecute(COMMAND_EXECUTE_FUNCTION, "select int_add(1, 1)",
+									   'public.int_add', false, true);
+
+$strSql = "CREATE AGGREGATE public.sum_test(  pg_catalog.int4) " .
+		  "(SFUNC=public.int_add, STYPE=pg_catalog.int4, INITCOND='0')";
+PgLogExecute(COMMAND_CREATE_AGGREGATE, $strSql, 'public.sum_test(integer)');
+
+# There's a bug here in deparse:
+$strSql = "ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2";
+PgLogExecute(COMMAND_ALTER_AGGREGATE, $strSql, 'public.sum_test2(integer)');
+
+$strSql = "CREATE COLLATION public.collation_test (LC_COLLATE = 'de_DE', " .
+		  "LC_CTYPE = 'de_DE')";
+PgLogExecute(COMMAND_CREATE_COLLATION, $strSql, 'public.collation_test');
+
+$strSql =  "ALTER COLLATION public.collation_test RENAME TO collation_test2";
+PgLogExecute(COMMAND_ALTER_COLLATION, $strSql, 'public.collation_test2');
+
+$strSql = "CREATE  CONVERSION public.conversion_test FOR 'SQL_ASCII' " .
+		  "TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic";
+PgLogExecute(COMMAND_CREATE_CONVERSION, $strSql, 'public.conversion_test');
+
+$strSql = "ALTER CONVERSION public.conversion_test RENAME TO conversion_test2";
+PgLogExecute(COMMAND_ALTER_CONVERSION, $strSql, 'public.conversion_test2');
+
+PgLogExecute(COMMAND_CREATE_DATABASE, "CREATE DATABASE database_test");
+PgLogExecute(COMMAND_ALTER_DATABASE,
+			 "ALTER DATABASE database_test rename to database_test2");
+PgLogExecute(COMMAND_DROP_DATABASE, "DROP DATABASE database_test2");
+
+# Make sure there are no more audit events pending in the postgres log
+PgLogWait();
+
+# Stop the database
+if (!$bNoCleanup)
+{
+	PgDrop();
+}
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index a698d0f..5b247a9 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 89fff77..6b0b407 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -125,6 +125,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbench         SYSTEM "pgbench.sgml">
 <!ENTITY pgarchivecleanup SYSTEM "pgarchivecleanup.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..4588619
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,347 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> module provides session and object
+    auditing via the standard logging facility.  Session and object auditing are
+    completely independent and can be combined.
+  </para>
+
+  <sect2>
+    <title>Session Auditing</title>
+
+    <para>
+      Session auditing allows the logging of all commands that are executed by
+      a user in the backend.  Each command is logged with a single entry and
+      includes the audit type (e.g. <literal>SESSION</literal>), command type
+      (e.g. <literal>CREATE TABLE</literal>, <literal>SELECT</literal>) and
+      statement (e.g. <literal>"select * from test"</literal>).
+
+      Fully-qualified names and object types will be logged for
+      <literal>CREATE</literal>, <literal>UPDATE</literal>, and
+      <literal>DROP</literal> commands on <literal>TABLE</literal>,
+      <literal>MATVIEW</literal>, <literal>VIEW</literal>,
+      <literal>INDEX</literal>, <literal>FOREIGN TABLE</literal>,
+      <literal>COMPOSITE TYPE</literal>, <literal>INDEX</literal>, and
+      <literal>SEQUENCE</literal> objects as well as function calls.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is controlled by the <literal>pg_audit.log</literal>
+        GUC. There are six classes of commands that are recognized:
+
+        <itemizedlist>
+          <listitem>
+            <para>
+              <literal>READ</literal> - <literal>SELECT</literal> and
+              <literal>COPY</literal> when the source is a table or query.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>WRITE</literal> - <literal>INSERT</literal>,
+              <literal>UPDATE</literal>, <literal>DELETE</literal>,
+              <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+              destination is a table.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>FUNCTION</literal> - Function calls and
+              <literal>DO</literal> blocks.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>DDL</literal> - DDL, plus <literal>VACUUM</literal>,
+              <literal>REINDEX</literal>, and <literal>ANALYZE</literal>.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>PARAMETER</literal> - Parameters that were passed for the statement.  Parameters immediately follow the statement text.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>MISC</literal> - Miscellaneous commands, e.g.
+              <literal>DISCARD</literal>, <literal>FETCH</literal>,
+              <literal>CHECKPOINT</literal>.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </para>
+
+      <para>
+        Enable session logging for all writes and DDL:
+          <programlisting>
+pg_audit.log = 'write, ddl'
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except miscellaneous:
+          <programlisting>
+pg_audit.log = 'all, -misc'
+          </programlisting>
+      </para>
+
+      <para>
+      Note that <literal>pg_audit.log</literal> can be set globally (in
+      <filename>postgresql.conf</filename>), at the database level (using
+      <literal>alter database ... set</literal>), or at the role level (using
+      <literal>alter role ... set</literal>).
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.log = 'read, ddl'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+      <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+select *
+    from account;
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Object Auditing</title>
+
+    <para>
+      Object auditing logs commands that affect a particular object.  Only
+      <literal>SELECT</literal>, <literal>INSERT</literal>,
+      <literal>UPDATE</literal> and <literal>DELETE</literal> commands are
+      supported.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level auditing is implemented via the roles system.  The
+        <literal>pg_audit.role</literal> GUC defines the role that will be used
+        for auditing.  An object will be audited when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.
+      </para>
+
+      <programlisting>
+postresql.conf: pg_audit.role = 'audit'
+
+grant select, delete
+   on public.account;
+      </programlisting>
+
+      <para>
+      Note that <literal>pg_audit.role</literal> can be set globally (in
+      <filename>postgresql.conf</filename>), at the database level (using
+      <literal>alter database ... set</literal>), or at the role level (using
+      <literal>alter role ... set</literal>).
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Examples</title>
+
+      <para>
+        Set <literal>pg_audit.role = 'audit'</literal> in
+        <literal>postgresql.conf</literal>.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to audit;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to audit;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to audit;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is compliant CSV format only if the log line prefix portion
+          of each log entry is removed.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</literal> - <literal>SESSION</literal> or
+            <literal>OBJECT</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT_ID</literal> - Unique statement ID for this
+            session.  Each statement ID represents a backend call.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>SUBSTATEMENT_ID</literal> - Sequential ID for each
+            substatement within the main statement.  For example, calling
+            a function from a query.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</literal> - <literal>READ</literal>,
+            <literal>WRITE</literal>, <literal>FUNCTION</literal>,
+            <literal>DDL</literal>, or <literal>MISC</literal>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</literal> - <literal>ALTER TABLE</literal>,
+            <literal>SELECT</literal>, <literal>CREATE INDEX</literal>,
+            <literal>UPDATE</literal>, etc.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</literal> - <literal>TABLE</literal>,
+            <literal>INDEX</literal>, <literal>VIEW</literal>, etc.  Only
+            available for DML and certain DDL commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</literal> - The fully-qualified object name
+            (e.g. public.account).  Only available for DML and certain DDL
+            commands.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</literal> - Statement execute on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#19David Steele
david@pgmasters.net
In reply to: David Steele (#8)
Re: Auditing extension for PostgreSQL (Take 2)

On 3/23/15 12:40 PM, David Steele wrote:

On 3/23/15 1:31 AM, Abhijit Menon-Sen wrote:

I'm experimenting with a few approaches to do this without reintroducing
switch statements to test every command. That will require core changes,
but I think we can find an acceptable arrangement. I'll post a proof of
concept in a few days.

Any progress on the POC? I'm interested in trying to get the ROLE class
back in before the Commitfest winds up, but not very happy with my
current string-matching options.

+ * Takes an AuditEvent and, if it log_check(), writes it to the audit
log.

I don't think log_check is the most useful name, because this sentence
doesn't tell me what the function may do. Similarly, I would prefer to
have log_acl_check be renamed acl_grants_audit or similar. (These are
all static functions anyway, I don't think a log_ prefix is needed.)

log_check() has become somewhat vestigial at this point since it is only
called from one place - I've been considering removing it and merging
into log_audit_event(). For the moment I've improved the comments.

log_check() got rolled into log_audit_event().

I like acl_grants_audit() and agree that it's a clearer name. I'll
incorporate that into the next version and apply the same scheme to the
other ACL functionsas well as do a general review of naming.

I ended up going with audit_on_acl, audit_on_relation, etc. and reworked
some of the other function names.

I attached the v6 patch to my previous email, or you can find it on the
CF page.

--
- David Steele
david@pgmasters.net

#20Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#18)
Re: Auditing extension for PostgreSQL (Take 2)

On Thu, Apr 2, 2015 at 2:46 AM, David Steele <david@pgmasters.net> wrote:

Hi Sawada,

On 3/25/15 9:24 AM, David Steele wrote:

On 3/25/15 7:46 AM, Sawada Masahiko wrote:

2.
I got ERROR when executing function uses cursor.

1) create empty table (hoge table)
2) create test function as follows.

create function test() returns int as $$
declare
cur1 cursor for select * from hoge;
tmp int;
begin
open cur1;
fetch cur1 into tmp;
return tmp;
end$$
language plpgsql ;

3) execute test function (got ERROR)
=# select test();
LOG: AUDIT: SESSION,6,1,READ,SELECT,,,selecT test();
LOG: AUDIT: SESSION,6,2,FUNCTION,EXECUTE,FUNCTION,public.test,selecT test();
LOG: AUDIT: SESSION,6,3,READ,SELECT,,,select * from hoge
CONTEXT: PL/pgSQL function test() line 6 at OPEN
ERROR: pg_audit stack is already empty
STATEMENT: selecT test();

It seems like that the item in stack is already freed by deleting
pg_audit memory context (in MemoryContextDelete()),
before calling stack_pop in dropping of top-level Portal.

This has been fixed and I have attached a new patch. I've seen this
with cursors before where the parent MemoryContext is freed before
control is returned to ProcessUtility. I think that's strange behavior
but there's not a lot I can do about it.

Thank you for updating the patch!

The code I put in to deal with this situation was not quite robust
enough so I had to harden it a bit more.

Let me know if you see any other issues.

I pulled HEAD, and then tried to compile source code after applied
following "deparsing utility command patch" without #0001 and #0002.
(because these two patches are already pushed)
</messages/by-id/20150325175954.GL3636@alvh.no-ip.org&gt;

But I could not use new pg_audit patch due to compile error (that
deparsing utility command patch might have).
I think I'm missing something, but I'm not sure.
Could you tell me which patch should I apply before compiling pg_audit?

Regards,

-------
Sawada Masahiko

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

#21David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#20)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/3/15 3:59 AM, Sawada Masahiko wrote:

On Thu, Apr 2, 2015 at 2:46 AM, David Steele <david@pgmasters.net> wrote:

Let me know if you see any other issues.

I pulled HEAD, and then tried to compile source code after applied
following "deparsing utility command patch" without #0001 and #0002.
(because these two patches are already pushed)
</messages/by-id/20150325175954.GL3636@alvh.no-ip.org&gt;

But I could not use new pg_audit patch due to compile error (that
deparsing utility command patch might have).
I think I'm missing something, but I'm not sure.
Could you tell me which patch should I apply before compiling pg_audit?

When Alvaro posted his last patch set he recommended applying them to
b3196e65:

/messages/by-id/20150325175954.GL3636@alvh.no-ip.org

Applying the 3/25 deparse patches onto b3196e65 (you'll see one trailing
space error) then applying pg_audit-v6.patch works fine.

--
- David Steele
david@pgmasters.net

#22Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#21)
Re: Auditing extension for PostgreSQL (Take 2)

On Fri, Apr 3, 2015 at 10:01 PM, David Steele <david@pgmasters.net> wrote:

On 4/3/15 3:59 AM, Sawada Masahiko wrote:

On Thu, Apr 2, 2015 at 2:46 AM, David Steele <david@pgmasters.net> wrote:

Let me know if you see any other issues.

I pulled HEAD, and then tried to compile source code after applied
following "deparsing utility command patch" without #0001 and #0002.
(because these two patches are already pushed)
</messages/by-id/20150325175954.GL3636@alvh.no-ip.org&gt;

But I could not use new pg_audit patch due to compile error (that
deparsing utility command patch might have).
I think I'm missing something, but I'm not sure.
Could you tell me which patch should I apply before compiling pg_audit?

When Alvaro posted his last patch set he recommended applying them to
b3196e65:

/messages/by-id/20150325175954.GL3636@alvh.no-ip.org

Applying the 3/25 deparse patches onto b3196e65 (you'll see one trailing
space error) then applying pg_audit-v6.patch works fine.

Thank you for your advice!
I'm testing pg_audit, so I will send report to you as soon as possible.

Regards,

-------
Sawada Masahiko

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

#23Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#21)
Re: Auditing extension for PostgreSQL (Take 2)

On Fri, Apr 3, 2015 at 10:01 PM, David Steele <david@pgmasters.net> wrote:

On 4/3/15 3:59 AM, Sawada Masahiko wrote:

On Thu, Apr 2, 2015 at 2:46 AM, David Steele <david@pgmasters.net> wrote:

Let me know if you see any other issues.

I pulled HEAD, and then tried to compile source code after applied
following "deparsing utility command patch" without #0001 and #0002.
(because these two patches are already pushed)
</messages/by-id/20150325175954.GL3636@alvh.no-ip.org&gt;

But I could not use new pg_audit patch due to compile error (that
deparsing utility command patch might have).
I think I'm missing something, but I'm not sure.
Could you tell me which patch should I apply before compiling pg_audit?

When Alvaro posted his last patch set he recommended applying them to
b3196e65:

/messages/by-id/20150325175954.GL3636@alvh.no-ip.org

Applying the 3/25 deparse patches onto b3196e65 (you'll see one trailing
space error) then applying pg_audit-v6.patch works fine.

I tested v6 patch, but I got SEGV when I executed test() function I
mentioned up thread.
Could you address this problem?

Regards,

-------
Sawada Masahiko

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

#24David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#23)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/6/15 8:40 AM, Sawada Masahiko wrote:

On Fri, Apr 3, 2015 at 10:01 PM, David Steele <david@pgmasters.net> wrote:

On 4/3/15 3:59 AM, Sawada Masahiko wrote:

On Thu, Apr 2, 2015 at 2:46 AM, David Steele <david@pgmasters.net> wrote:

Let me know if you see any other issues.

I pulled HEAD, and then tried to compile source code after applied
following "deparsing utility command patch" without #0001 and #0002.
(because these two patches are already pushed)
</messages/by-id/20150325175954.GL3636@alvh.no-ip.org&gt;

But I could not use new pg_audit patch due to compile error (that
deparsing utility command patch might have).
I think I'm missing something, but I'm not sure.
Could you tell me which patch should I apply before compiling pg_audit?

When Alvaro posted his last patch set he recommended applying them to
b3196e65:

/messages/by-id/20150325175954.GL3636@alvh.no-ip.org

Applying the 3/25 deparse patches onto b3196e65 (you'll see one trailing
space error) then applying pg_audit-v6.patch works fine.

I tested v6 patch, but I got SEGV when I executed test() function I
mentioned up thread.
Could you address this problem?

Unfortunately I'm not able to reproduce the issue. Here's what I tried
based on your original test:

postgres=# create table hoge (id int);
CREATE TABLE
postgres=# create function test() returns int as $$
postgres$# declare
postgres$# cur1 cursor for select * from hoge;
postgres$# tmp int;
postgres$# begin
postgres$# open cur1;
postgres$# fetch cur1 into tmp;
postgres$# return tmp;
postgres$# end$$
postgres-# language plpgsql ;
CREATE FUNCTION
postgres=# select test();
test
------

(1 row)
postgres=# insert into hoge values (1);
INSERT 0 1
postgres=# select test();
test
------
1
(1 row)

And the log output was:

prefix LOG: statement: create table hoge (id int);
prefix DEBUG: EventTriggerInvoke 16385
prefix LOG: AUDIT: SESSION,3,1,DDL,CREATE
TABLE,TABLE,public.hoge,CREATE TABLE public.hoge (id pg_catalog.int4
) WITH (oids=OFF)
prefix LOG: statement: create function test() returns int as $$
declare
cur1 cursor for select * from hoge;
tmp int;
begin
open cur1;
fetch cur1 into tmp;
return tmp;
end$$
language plpgsql ;
prefix DEBUG: EventTriggerInvoke 16385
prefix LOG: AUDIT: SESSION,4,1,DDL,CREATE
FUNCTION,FUNCTION,public.test(),"CREATE FUNCTION public.test() RETURNS
pg_catalog.int4 LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT
SECURITY INVOKER COST 100 AS '
declare
cur1 cursor for select * from hoge;
tmp int;
begin
open cur1;
fetch cur1 into tmp;
return tmp;
end'"
prefix LOG: statement: select test();
prefix LOG: AUDIT: SESSION,5,1,READ,SELECT,,,select test();
prefix LOG: AUDIT:
SESSION,5,2,FUNCTION,EXECUTE,FUNCTION,public.test,select test();
prefix LOG: AUDIT: SESSION,5,3,READ,SELECT,,,select * from hoge
prefix CONTEXT: PL/pgSQL function test() line 6 at OPEN
prefix LOG: statement: insert into hoge values (1);
prefix LOG: AUDIT: SESSION,6,1,WRITE,INSERT,,,insert into hoge values (1);
prefix LOG: statement: select test();
prefix LOG: AUDIT: SESSION,7,1,READ,SELECT,,,select test();
prefix LOG: AUDIT:
SESSION,7,2,FUNCTION,EXECUTE,FUNCTION,public.test,select test();
prefix LOG: AUDIT: SESSION,7,3,READ,SELECT,,,select * from hoge
prefix CONTEXT: PL/pgSQL function test() line 6 at OPEN

Does the above look like what you did? Any other information about your
environment would also be helpful. I'm thinking it might be some kind
of build issue.

--
- David Steele
david@pgmasters.net

#25Peter Eisentraut
peter_e@gmx.net
In reply to: David Steele (#1)
Re: Auditing extension for PostgreSQL (Take 2)

On 2/14/15 9:34 PM, David Steele wrote:

The patch I've attached satisfies the requirements that I've had from
customers in the past.

What I'm missing is a more precise description/documentation of what
those requirements might be.

"Audit" is a "big word". It might imply regulatory or standards
compliance on some level. We already have ways to log everything. If
customers want "auditing" instead, they will hopefully have a precise
requirements set, and we need a way to map that to a system
configuration. I don't think "we need auditing" -> "oh there's this
pg_audit thing, and it has a bunch of settings you can play with" is
going to be enough of a workflow. For starters, I would consider the
textual server log to be potentially lossy in many circumstances, so
there would need to be additional information on how to configure that
to be robust.

(Also, more accurately, this is an "audit trail", not an "audit". An
audit is an examination of a system, not a record of interactions with a
system. An audit trail might be useful for an audit.)

I see value in what you call object auditing, which is something you
can't easily do at the moment. But what you call session auditing seems
hardly distinct from statement logging. If we enhance log_statements a
little bit, there will not be a need for an extra module to do almost
the same thing.

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

#26Simon Riggs
simon@2ndQuadrant.com
In reply to: Peter Eisentraut (#25)
Re: Auditing extension for PostgreSQL (Take 2)

On 6 April 2015 at 16:34, Peter Eisentraut <peter_e@gmx.net> wrote:

On 2/14/15 9:34 PM, David Steele wrote:

The patch I've attached satisfies the requirements that I've had from
customers in the past.

What I'm missing is a more precise description/documentation of what
those requirements might be.

"Audit" is a "big word". It might imply regulatory or standards
compliance on some level. We already have ways to log everything. If
customers want "auditing" instead, they will hopefully have a precise
requirements set, and we need a way to map that to a system
configuration. I don't think "we need auditing" -> "oh there's this
pg_audit thing, and it has a bunch of settings you can play with" is
going to be enough of a workflow.

Yes, this needs better documentation, as does RLS.

For starters, I would consider the
textual server log to be potentially lossy in many circumstances, so
there would need to be additional information on how to configure that
to be robust.

It was intended to be used with a log filter plugin, to allow it to be
routed wherever is considered safe.

(Also, more accurately, this is an "audit trail", not an "audit". An
audit is an examination of a system, not a record of interactions with a
system. An audit trail might be useful for an audit.)

No problem with calling it pg_audit_trail

I see value in what you call object auditing, which is something you
can't easily do at the moment. But what you call session auditing seems
hardly distinct from statement logging. If we enhance log_statements a
little bit, there will not be a need for an extra module to do almost
the same thing.

Agreed: generating one line per statement isn't much different from
log_statements.

The earlier version of pg_audit generated different output.
Specifically, it allowed you to generate output for each object
tracked; one line per object.

The present version can trigger an audit trail event for a statement,
without tracking the object that was being audited. This prevents you
from searching for "all SQL that touches table X", i.e. we know the
statements were generated, but not which ones they were. IMHO that
makes the resulting audit trail unusable for auditing purposes. I
would like to see that functionality put back before it gets
committed, if that occurs.

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, RemoteDBA, 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

#27Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Simon Riggs (#26)
Re: Auditing extension for PostgreSQL (Take 2)

Simon Riggs wrote:

The present version can trigger an audit trail event for a statement,
without tracking the object that was being audited. This prevents you
from searching for "all SQL that touches table X", i.e. we know the
statements were generated, but not which ones they were. IMHO that
makes the resulting audit trail unusable for auditing purposes. I
would like to see that functionality put back before it gets
committed, if that occurs.

Is there a consensus that the current version is the one that we should
be reviewing, rather than the one Abhijit submitted? Last I checked,
that wasn't at all clear.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, 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

#28David Steele
david@pgmasters.net
In reply to: Peter Eisentraut (#25)
Re: Auditing extension for PostgreSQL (Take 2)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

On 4/6/15 4:34 PM, Peter Eisentraut wrote:

On 2/14/15 9:34 PM, David Steele wrote:

The patch I've attached satisfies the requirements that I've had
from customers in the past.

What I'm missing is a more precise description/documentation of
what those requirements might be.

Admittedly I'm not a financial or ISO certification auditor, but I've
been in the position of providing data to auditors on many of
occasions. The requests generally fall into three categories:

1) Data requests. Perhaps all the CDRs for a particular customer for
a particular month. Bulk data requests are not addressed by pg_audit.

2) DDL log. A list of all DDL changes made to the database. For
instance, the last time a function was updated and who did it. The
auditor would like to be sure that the function update timestamp
matches up with the last maintenance window and the person who is on
record as having done the updates.

3) DML log. This can be done with triggers, but requires quite a bit
of work and vigilance.

"Audit" is a "big word". It might imply regulatory or standards
compliance on some level. We already have ways to log everything.
If customers want "auditing" instead, they will hopefully have a
precise requirements set, and we need a way to map that to a
system configuration. I don't think "we need auditing" -> "oh
there's this pg_audit thing, and it has a bunch of settings you can
play with" is going to be enough of a workflow. For starters, I
would consider the textual server log to be potentially lossy in
many circumstances, so there would need to be additional
information on how to configure that to be robust.

Nothing is perfect, but there's a big difference between being able to
log everything and being able to use the data you logged to satisfy an
audit. Auditors tend to be reasonably tech savvy but there are
limits. An example of how pg_audit can provide better logging is at
the end of this email.

I agree that server logs are potentially lossy but that really
describes anywhere audit records might be written. Agreed that there
are better ways to do it (like writing back to the DB, or a remote
DB), but I thought those methods should be saved for a future version.

In my past experience having retention policies in place and being
able to show that they normally work are enough to satisfy an auditor.
Accidents happen and that's understood - as long as an explanation
for the failure is given. Such as, "We lost a backup tape, here's the
ticket for the incident and the name of the employee who handled the
case so you can follow up." Or, "On this date we had a disk failure
and lost the logs before the could be shipped, here's the ticket, etc."

(Also, more accurately, this is an "audit trail", not an "audit".
An audit is an examination of a system, not a record of
interactions with a system. An audit trail might be useful for an
audit.)

You are correct and I'd be happy to call it pg_audit_trail (as Simon
suggested) or pg_audit_log or something that's more descriptive.

I see value in what you call object auditing, which is something
you can't easily do at the moment. But what you call session
auditing seems hardly distinct from statement logging. If we
enhance log_statements a little bit, there will not be a need for
an extra module to do almost the same thing.

Even with session auditing you can have multiple log entries per
backend call. This is particularly true for DO blocks and functions
calls.

Here's a relatively simple example, but it shows how complex this
could get. Let's say we have a DO block with dynamic SQL:

do $$
declare
table_name text = 'do_table';
begin
execute 'create table ' || table_name || ' ("weird name" int)';
execute 'drop table ' || table_name;
end; $$

Setting log_statement=all will certain work but you only get the DO
block logged:

LOG: statement: do $$
declare
table_name text = 'do_table';
begin
execute 'create table ' || table_name || ' ("weird name" int)';
execute 'drop table ' || table_name;
end; $$

With pg_audit you get (forgive the LFs that email added) much more:

LOG: AUDIT: SESSION,38,1,FUNCTION,DO,,,"do $$
declare
table_name text = 'do_table';
begin
execute 'create table ' || table_name || ' (""weird name"" int)';
execute 'drop table ' || table_name;
end; $$"
LOG: AUDIT: SESSION,38,2,DDL,CREATE
TABLE,TABLE,public.do_table,"CREATE TABLE public.do_table (""weird
name"" pg_catalog.int4 ) WITH (oids=OFF) "
LOG: AUDIT: SESSION,38,3,DDL,DROP TABLE,TABLE,public.do_table,drop
table do_table

Not only is the DO block logged but each sub statement is logged as
well. They are logically grouped by the statement ID (in this case
38) so it's clear they were run as a single command. The commands
(DO, DROP TABLE, CREATE TABLE) and fully-qualified object names are
provided and the statements are quoted and escaped when needed to
making parsing easier.

There's no question that this sort of thing could be done with
log_statements, but I would argue it's more than a little enhancement.

- --
- - David Steele
david@pgmasters.net
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2

iQIcBAEBCAAGBQJVIyOyAAoJEIA2uIJQ5SFAOQEP/jcEsrAqxxGMn/Px6YSjzJCq
vKBGkilxNHbHn8GeAD617LPHl+4WjgmSWPA4OC2qbCa36tib0mBTRVpdaA1J9PTU
+Ml9kk9hHGdXTkoK2DlMFwVwJ4mZCvKXU4TOOYjdG6YkQaEoCdpEQ8n0Z0bxYogb
zBZ6GnxdkMzD8w33LByW9tf/ShWZsDKh47vqIhk1oGvQULlTGZ7CvAq793vWOCng
9+SBsct8BCUNRS0i1JcWjoLin9rJUNXLkyufIylKuAjbacBDIvQfRmKJJYTQA8lg
7K0Hy5gp7JNWTN+J6TQHM930FFFetVzXXaLRaJwZls9hqzPDSpXA2LleEQy9jzlf
CvSQgoAx/kkBEOjkKBAEL4PYcWXWhizysXkVAURwZ3huvm5wi8C2mVFilFz9oiZa
Z7L0FClFcBhX3ZuptDJOXF4WFdwE7TQDy3Go8aA/UY5gqe08Hqx/Atw881kBEC3j
uQIgdWY0WGVvT43igX44mUv3Q0aTNWHn/jTgaRURwPlpP+wViK3VIybIqiKtebq1
Iaqduge0pirDQMTDdFxt+F5C+ylK+R9TU9xPv8eQwrbq8o3ZIuoiLhtuzl8yQWMI
tiJJCfay4gkm+xZIjsFe9aj3Q0Xk4VjAt8MF9OunaNFTI4X5ZH3OSkZvmbbMjd1S
i6u19Khnj9ryje2nGNFS
=1jDh
-----END PGP SIGNATURE-----

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

#29David Steele
david@pgmasters.net
In reply to: Simon Riggs (#26)
Re: Auditing extension for PostgreSQL (Take 2)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

On 4/6/15 4:47 PM, Simon Riggs wrote:

On 6 April 2015 at 16:34, Peter Eisentraut <peter_e@gmx.net>
wrote:

"Audit" is a "big word". It might imply regulatory or standards
compliance on some level. We already have ways to log
everything. If customers want "auditing" instead, they will
hopefully have a precise requirements set, and we need a way to
map that to a system configuration. I don't think "we need
auditing" -> "oh there's this pg_audit thing, and it has a bunch
of settings you can play with" is going to be enough of a
workflow.

Yes, this needs better documentation, as does RLS.

Discussions like these definitely help when it comes to knowing what
to put in the documentation. The "what" is hard enough, the "why"
gets into some scary territory.

Still, audit requirements vary wildly and I'm not sure how much
justice I could do to the topic in the contrib docs. I think more
discussion of what's technically possible might be more fruitful.

For starters, I would consider the textual server log to be
potentially lossy in many circumstances, so there would need to
be additional information on how to configure that to be robust.

It was intended to be used with a log filter plugin, to allow it to
be routed wherever is considered safe.

That would certainly work.

(Also, more accurately, this is an "audit trail", not an "audit".
An audit is an examination of a system, not a record of
interactions with a system. An audit trail might be useful for
an audit.)

No problem with calling it pg_audit_trail

Nor I.

I see value in what you call object auditing, which is something
you can't easily do at the moment. But what you call session
auditing seems hardly distinct from statement logging. If we
enhance log_statements a little bit, there will not be a need for
an extra module to do almost the same thing.

Agreed: generating one line per statement isn't much different
from log_statements.

The earlier version of pg_audit generated different output.
Specifically, it allowed you to generate output for each object
tracked; one line per object.

That is still doable, but is covered by object-level auditing. Even
so, multiple log entries are possible (and even likely) with session
auditing. See my response to Peter for details.

The present version can trigger an audit trail event for a
statement, without tracking the object that was being audited. This
prevents you from searching for "all SQL that touches table X",
i.e. we know the statements were generated, but not which ones they
were. IMHO that makes the resulting audit trail unusable for
auditing purposes. I would like to see that functionality put back
before it gets committed, if that occurs.

Bringing this back would be easy (it actually requires removing, not
adding code) but I'd prefer to make it configurable.

- --
- - David Steele
david@pgmasters.net
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2

iQIcBAEBCAAGBQJVIybuAAoJEIA2uIJQ5SFAm/8P/2gS2oSvxF2VjP3WBJjH0d8p
QHlni3SDBIlJPPE1ZnNYbtUANWKSw2Ublpk50223TeejEnNZfORtA7TZ9qic+3Ei
83yK4SzQcSMA1xqMvGTDS621l4/Nkw/uWKO8BDGePTHRjsEPpgMxJxsHVfNddd5Z
MTP26vXPgyzj1H1GE4jPCi1kR6iiKx3GiagawmNJNgzdOXf25hQijpQ7mR0puw/T
V75MeNr0WNi4CtsyDgNnx0oVKBN4olG6aId6+q3jt+yuxboJ53Nq59xbfvxYUR+3
uPKX9jfwInZxQc+70g2CcKj+EglB9cDn4oaMUkAxqYWKWyRW0O2gs0IIkbQqk8qK
SlfBvAaZA1wfDelCztr8GHc8hLIh+hwb3mJq4zoclPg3+36hUgVyVIyRUjzW42sJ
shvd2KWkxP4iwN1+tru9YK3qZ1GXkZfodtXdJ7iY14k5eXTKBuRgHFO8BRXxW9xp
/KwIgkLD9gEjht6cncgP83lBoaxMFjrQE9N3hzX1wMM5ZYDAisbKK7JkGE2+yCsH
L/aiCOxyHbxaMZATopATbCBhULDMLKl9oICKY+jv17yeyGG5F5D78AWv0tuvk1jW
5enydtXPhcBIXRWIvTZgCfDpFs5Hv5r/+V70tiMQbJIzg2qvxHmC0VLEubxky0XE
TGfavKbTvK9dmw1dhzk5
=5KkP
-----END PGP SIGNATURE-----

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

#30Simon Riggs
simon@2ndQuadrant.com
In reply to: David Steele (#29)
Re: Auditing extension for PostgreSQL (Take 2)

On 6 April 2015 at 20:38, David Steele <david@pgmasters.net> wrote:

The earlier version of pg_audit generated different output.
Specifically, it allowed you to generate output for each object
tracked; one line per object.

That discussion covers recursive SQL. That is important too, but not
what I am saying.

My point is what we log when an SQL statement covers multiple tables,
e.g. join SELECTs, or inheritance cases, views.

That is still doable, but is covered by object-level auditing. Even
so, multiple log entries are possible (and even likely) with session
auditing. See my response to Peter for details.

The present version can trigger an audit trail event for a
statement, without tracking the object that was being audited. This
prevents you from searching for "all SQL that touches table X",
i.e. we know the statements were generated, but not which ones they
were. IMHO that makes the resulting audit trail unusable for
auditing purposes. I would like to see that functionality put back
before it gets committed, if that occurs.

Bringing this back would be easy (it actually requires removing, not
adding code) but I'd prefer to make it configurable.

That is my preference also. My concern was raised when it was
*removed* without confirming others agreed.

Typical questions:

Who has written to table X?
Who has read data from table Y yesterday between time1 and time2?
Has anyone accessed a table directly, rather than through a security view?

--
Simon Riggs http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, RemoteDBA, 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

#31Peter Eisentraut
peter_e@gmx.net
In reply to: Alvaro Herrera (#27)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/6/15 5:03 PM, Alvaro Herrera wrote:

Simon Riggs wrote:

The present version can trigger an audit trail event for a statement,
without tracking the object that was being audited. This prevents you
from searching for "all SQL that touches table X", i.e. we know the
statements were generated, but not which ones they were. IMHO that
makes the resulting audit trail unusable for auditing purposes. I
would like to see that functionality put back before it gets
committed, if that occurs.

Is there a consensus that the current version is the one that we should
be reviewing, rather than the one Abhijit submitted? Last I checked,
that wasn't at all clear.

Well, this one is the commitfest thread of record.

At quick glance, my comments about "how does this map to specific
customer requirements" apply to the other submission as well.

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

#32David Steele
david@pgmasters.net
In reply to: Simon Riggs (#30)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

Attached is the v7 pg_audit patch.

I've tried to address Peter's documentation concerns by cleaning up the
terminology and adding a real-world case plus usage recommendations.
The word "auditing" has been expunged from the docs in favor of the term
"audit logging".

Per Simon's request, there is now a pg_audit.log_relation setting that
makes session audit logging exhaustively log all relations as it did
before. The ROLE logging class is back as well.

Simon also suggested a way that pg_audit could be tested with standard
regression so I have converted all tests over and removed test.pl.

Sawada, I'd certainly appreciate it if you'd try again and see if you
are still getting a segfault with your test code (which you can find in
the regression tests).

Currently the patch will compile on master (I tested with b22a36a) or
optionally with Alvaro's deparse patches applied (only 0001 & 0002
needed). I've supplied a different regression test out file
(expected/pg_audit-deparse.out) which can be copied over the standard
out file (expected/pg_audit.out) if you'd like to do regression on
pg_audit with deparse. The small section of code that calls
pg_event_trigger_ddl_commands() can be compiled by defining DEPARSE or
removed the #ifdefs around that block.

Please let me know if I've missed anything and I look forward to
comments and questions.

Thanks,
--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v7.patchtext/plain; charset=UTF-8; name=pg_audit-v7.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index c56050e..cf33e38 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -28,6 +28,7 @@ SUBDIRS = \
 		oid2name	\
 		pageinspect	\
 		passwordcheck	\
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/.gitignore b/contrib/pg_audit/.gitignore
new file mode 100644
index 0000000..a5267cf
--- /dev/null
+++ b/contrib/pg_audit/.gitignore
@@ -0,0 +1,5 @@
+log/
+results/
+tmp_check/
+regression.diffs
+regression.out
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..7b36011
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,21 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+REGRESS = pg_audit
+REGRESS_OPTS = --temp-config=$(top_srcdir)/contrib/pg_audit/pg_audit.conf
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/expected/pg_audit-deparse.out b/contrib/pg_audit/expected/pg_audit-deparse.out
new file mode 100644
index 0000000..617cba1
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit-deparse.out
@@ -0,0 +1,877 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,2,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+                  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+drop table test2;
+drop table test3;
+drop table test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,4,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,5,1,DDL,CREATE SCHEMA,SCHEMA,test,CREATE SCHEMA test;
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,2,DDL,CREATE TABLE AS,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,9,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,10,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,10,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,11,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+NOTICE:  AUDIT: SESSION,12,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+NOTICE:  AUDIT: SESSION,13,1,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,14,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+NOTICE:  AUDIT: SESSION,15,1,MISC,CLOSE CURSOR,,,CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,15,2,MISC,COMMIT,,,COMMIT;
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,16,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+NOTICE:  AUDIT: SESSION,17,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,18,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,18,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,19,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,19,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,19,2,DDL,CREATE TABLE,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,20,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,21,1,ROLE,GRANT,TABLE,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,22,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,23,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,24,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,25,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,25,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,26,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,26,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,27,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,28,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,29,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,30,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,30,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,30,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,30,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,30,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test cursors and functions in a do block
+CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;
+NOTICE:  AUDIT: SESSION,31,1,DDL,CREATE FUNCTION,FUNCTION,public.test(),"CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;"
+SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,1,READ,SELECT,,,SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,3,READ,SELECT,TABLE,public.test,SELECT * FROM test
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+    1
+(1 row)
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,33,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,34,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,37,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,38,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+NOTICE:  AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,41,1,DDL,CREATE FUNCTION,FUNCTION,"public.int_add(integer,integer)","CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,42,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,42,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,43,1,DDL,CREATE AGGREGATE,AGGREGATE,public.sum_test(integer),"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,44,1,DDL,ALTER AGGREGATE,AGGREGATE,public.sum_test2(integer),ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,45,1,DDL,CREATE CONVERSION,CONVERSION,public.conversion_test,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,46,1,DDL,ALTER CONVERSION,CONVERSION,public.conversion_test2,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,47,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,48,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,49,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
diff --git a/contrib/pg_audit/expected/pg_audit.out b/contrib/pg_audit/expected/pg_audit.out
new file mode 100644
index 0000000..f76654d
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit.out
@@ -0,0 +1,866 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,2,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+                  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+drop table test2;
+drop table test3;
+drop table test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,4,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,5,1,DDL,CREATE SCHEMA,,,CREATE SCHEMA test;
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,2,DDL,CREATE TABLE AS,,,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,9,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,10,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,10,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,11,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+NOTICE:  AUDIT: SESSION,12,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+NOTICE:  AUDIT: SESSION,13,1,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,14,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+NOTICE:  AUDIT: SESSION,15,1,MISC,CLOSE CURSOR,,,CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,15,2,MISC,COMMIT,,,COMMIT;
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,16,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+NOTICE:  AUDIT: SESSION,17,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,18,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,18,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,19,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,19,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,20,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,21,1,ROLE,GRANT,,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,22,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,23,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,24,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,25,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,25,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,26,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,26,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,27,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,28,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,29,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,30,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,30,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,30,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,30,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,30,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test cursors and functions in a do block
+CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;
+NOTICE:  AUDIT: SESSION,31,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;"
+SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,1,READ,SELECT,,,SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,3,READ,SELECT,TABLE,public.test,SELECT * FROM test
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+    1
+(1 row)
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,33,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,34,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,37,1,DDL,ALTER TABLE,INDEX,public.test_pkey,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,38,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,41,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,42,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,42,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,43,1,DDL,CREATE AGGREGATE,,,"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,44,1,DDL,ALTER AGGREGATE,,,ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,45,1,DDL,CREATE CONVERSION,,,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,46,1,DDL,ALTER CONVERSION,,,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,47,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,48,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,49,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..4540fe2
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1787 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pg_audit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pg_audit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * auditLogRelation controls whether all relations are logged for READ and
+ * WRITE classes during session logging.
+ */
+bool auditLogRelation = false;
+
+/*
+ * auditLogNotice raises a notice as well as logging via the standard facility.
+ * This is primarily for the benefit of testing.
+ */
+bool auditLogNotice = false;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pg_audit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_PARAMETER		"PARAMETER"
+#define CLASS_READ			"READ"
+#define CLASS_ROLE			"ROLE"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* Function execution */
+	LOG_PARAMETER = (1 << 4),
+
+	/* SELECT */
+	LOG_READ = (1 << 5),
+
+	/* GRANT, REVOKE, CREATE/ALTER/DROP ROLE */
+	LOG_ROLE = (1 << 6),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 7),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_ALTER_ROLE	"ALTER ROLE"
+#define COMMAND_DROP_ROLE	"CREATE ROLE"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	int64 stackId;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static int64 statementTotal = 0;
+static int64 substatementTotal = 0;
+static int64 stackTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack to the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Create the unique stackId - used to keep the stack sane when memory
+	 * contexts are freed unexpectedly.
+	 */
+	stackItem->stackId = ++stackTotal;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop(int64 stackId)
+{
+	/* Make sure what we want to delete is at the top of the stack */
+	if (auditEventStack != NULL && auditEventStack->stackId == stackId)
+	{
+		MemoryContextDelete(auditEventStack->contextAudit);
+	}
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent, classifies it, then logs it if permissions were granted
+ * via roles or if the statement belongs in a class that is being logged.
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	MemoryContext contextOld;
+	StringInfoData auditStr;
+
+	/* By default put everything in the MISC class. */
+	enum LogClass class = LOG_MISC;
+	const char *className = CLASS_MISC;
+
+	/* Classify the statement using log stmt level and the command tag */
+	switch (stackItem->auditEvent.logStmtLevel)
+	{
+		/* All mods go in WRITE class */
+		case LOGSTMT_MOD:
+			className = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		/* Separate ROLE from other DDL statements */
+		case LOGSTMT_DDL:
+			/* Identify role statements */
+			if (stackItem->auditEvent.commandTag == T_GrantStmt ||
+				stackItem->auditEvent.commandTag == T_GrantRoleStmt ||
+				stackItem->auditEvent.commandTag == T_CreateRoleStmt ||
+				(stackItem->auditEvent.commandTag == T_RenameStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_ALTER_ROLE) == 0) ||
+				(stackItem->auditEvent.commandTag == T_DropStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_DROP_ROLE) == 0) ||
+				stackItem->auditEvent.commandTag == T_DropRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleSetStmt)
+			{
+				className = CLASS_ROLE;
+				class = LOG_ROLE;
+			}
+			/* Else log as DDL */
+			else
+			{
+				className = CLASS_DDL;
+				class = LOG_DDL;
+			}
+
+		/* Figure out the rest */
+		case LOGSTMT_ALL:
+			switch (stackItem->auditEvent.commandTag)
+			{
+				/* READ statements */
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					className = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				/* Reindex is DDL (because cluster is DDL) */
+				case T_ReindexStmt:
+					className = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				/* FUNCTION statements */
+				case T_DoStmt:
+					className = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * Only log the statement if:
+	 *
+	 * 1. If permissions were granted via roles
+	 * 2. The statement belongs to a class that is being logged
+	 */
+	if (!stackItem->auditEvent.granted && !(auditLogBitmap & class))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogBitmap & LOG_PARAMETER &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	elog(auditLogNotice ? NOTICE : LOG,
+		 "AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			className, auditStr.data);
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+audit_on_acl(Datum aclDatum,
+			 Oid auditOid,
+			 AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+audit_on_relation(Oid relOid,
+				  Oid auditOid,
+				  AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = audit_on_acl(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+audit_on_attribute(Oid relOid,
+				   AttrNumber attNum,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = audit_on_acl(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+audit_on_any_attribute(Oid relOid,
+					   Oid auditOid,
+					   Bitmapset *attributeSet,
+					   AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (audit_on_attribute(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			audit_on_attribute(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/*
+		 * If this is the first rte then session log unless auditLogRelation
+		 * is set.
+		 */
+		if (first && !auditLogRelation)
+		{
+			auditEventStack->auditEvent.objectName = "";
+			auditEventStack->auditEvent.objectType = "";
+
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (audit_on_relation(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->selectedCols,
+											   ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEventStack->auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEventStack->auditEvent.granted =
+							audit_on_any_attribute(relOid, auditOid,
+												   rte->modifiedCols,
+												   auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Do relation level logging if a grant was found */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		/* Do relation level logging if auditLogRelation is set */
+		if (auditLogRelation)
+		{
+			auditEventStack->auditEvent.logged = false;
+			auditEventStack->auditEvent.granted = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop(stackItem->stackId);
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute */
+		case OAT_FUNCTION_EXECUTE:
+			if (auditLogBitmap & LOG_FUNCTION)
+				log_function_execute(objectId);
+			break;
+
+		/* Log create */
+		case OAT_POST_CREATE:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter */
+		case OAT_POST_ALTER:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop */
+		case OAT_DROP:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessDrop *drop = arg;
+
+				if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+static ExecutorEnd_hook_type next_ExecutorEnd_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pg_audit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ExecutorEnd to pop statement audit event off the stack.
+ */
+static void
+pg_audit_ExecutorEnd_hook(QueryDesc *queryDesc)
+{
+	/* Call the next hook or standard function */
+	if (next_ExecutorEnd_hook)
+		next_ExecutorEnd_hook(queryDesc);
+	else
+		standard_ExecutorEnd(queryDesc);
+
+	/* Pop the audit event off the stack */
+	if (!internalStatement)
+	{
+		stack_pop(auditEventStack->stackId);
+	}
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pg_audit_ProcessUtility_hook(Node *parsetree,
+							 const char *queryString,
+							 ProcessUtilityContext context,
+							 ParamListInfo params,
+							 DestReceiver *dest,
+							 char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+	int64 stackId;
+
+	/* Allocate the audit event */
+	if (!IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackId = stackItem->stackId;
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap != 0 &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/* Process the audit event if there is one. */
+	if (stackItem != NULL)
+	{
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !stackItem->auditEvent.logged &&
+			!IsAbortedTransactionBlockState())
+			log_audit_event(stackItem);
+
+		stack_pop(stackId);
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pg_audit_object_access_hook(ObjectAccessType access,
+							Oid classId,
+							Oid objectId,
+							int subId,
+							void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&
+		auditLogBitmap & (LOG_DDL | LOG_FUNCTION) && auditEventStack)
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+#ifdef DEPARSE
+	/* Continue only if session DDL logging is enabled */
+	if (auditLogBitmap & LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_ddl_command_end()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		auditEventStack->auditEvent.logStmtLevel =
+			GetCommandLogLevel(eventData->parsetree);
+		auditEventStack->auditEvent.commandTag =
+			nodeTag(eventData->parsetree);
+		auditEventStack->auditEvent.command =
+			CreateCommandTag(eventData->parsetree);
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type), schema,\n"
+				"       identity, command\n"
+				"  FROM pg_event_trigger_ddl_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply object name and type for audit event */
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 6);
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+#endif
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_sql_drop()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type),\n"
+				"       schema_name, object_name, object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			char *schemaName;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			schemaName = SPI_getvalue(spiTuple, spiTupDesc, 5);
+
+			if (!(pg_strcasecmp(auditEventStack->auditEvent.objectType,
+							"TYPE") == 0 ||
+				  pg_strcasecmp(schemaName, "pg_toast") == 0))
+			{
+				auditEventStack->auditEvent.objectName =
+						SPI_getvalue(spiTuple, spiTupDesc, 7);
+
+				log_audit_event(auditEventStack);
+			}
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pg_audit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then then token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_PARAMETER) == 0)
+			class = LOG_PARAMETER;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_ROLE) == 0)
+			class = LOG_ROLE;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pg_audit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pg_audit_log from extra (ignoring newval, which has already been
+ * converted to a bitmap above). Note that extra may not be set if the
+ * assignment is to be suppressed.
+ */
+static void
+assign_pg_audit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * Defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable object auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * Controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable session auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pg_audit_log,
+							   assign_pg_audit_log,
+							   NULL);
+
+	/*
+	 * pg_audit.log_relation = on
+	 *
+	 * Controls whether relations get separate log entries during session
+	 * logging of READ and WRITE classes.  This works as if all relations in the
+	 * database had been added to the audit role and provides a shortcut when
+	 * really detailed logging of absolutely every relation is required.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_relation",
+							 "Enable session relation logging",
+							 NULL,
+							 &auditLogRelation,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+
+	/*
+	 * pg_audit.log_notice = on
+	 *
+	 * Audit logging is raised as notices that can be seen on the client.  This is
+	 * intended for testing purposes.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_notice",
+							 "Raise a notice when logging",
+							 NULL,
+							 &auditLogNotice,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pg_audit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook;
+
+	next_ExecutorEnd_hook = ExecutorEnd_hook;
+	ExecutorEnd_hook = pg_audit_ExecutorEnd_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pg_audit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pg_audit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.conf b/contrib/pg_audit/pg_audit.conf
new file mode 100644
index 0000000..e9f4a22
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.conf
@@ -0,0 +1 @@
+shared_preload_libraries = pg_audit
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/sql/pg_audit.sql b/contrib/pg_audit/sql/pg_audit.sql
new file mode 100644
index 0000000..0e9ae0a
--- /dev/null
+++ b/contrib/pg_audit/sql/pg_audit.sql
@@ -0,0 +1,527 @@
+-- Load pg_audit module
+create extension pg_audit;
+
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+
+--
+-- Create auditor role
+CREATE ROLE auditor;
+
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+DROP TABLE test;
+
+--
+-- Create second test user
+\connect contrib_regression super
+
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+
+SELECT *
+  FROM test3, test2;
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+
+\connect contrib_regression user2
+
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+
+--
+-- Drop test tables
+drop table test2;
+drop table test3;
+drop table test4;
+
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+
+--
+-- Create test schema
+CREATE SCHEMA test;
+
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+1	user1	HASH2	yada, yada
+\.
+
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+
+EXECUTE pgclassstmt (1);
+DEALLOCATE pgclassstmt;
+
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+
+FETCH NEXT FROM ctest;
+CLOSE ctest;
+COMMIT;
+
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+EXECUTE pgclassstmt (1);
+
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+
+--
+-- Check that analyze is logged
+ANALYZE test;
+
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+
+SELECT *
+  FROM test;
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+
+SELECT 1,
+	   current_user;
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+
+explain select 1;
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+INSERT INTO TEST (id)
+		  VALUES (2);
+INSERT INTO TEST (id)
+		  VALUES (3);
+
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+
+--
+-- Test cursors and functions in a do block
+CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;
+
+SELECT public.test();
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+
+ALTER TABLE public.test
+	RENAME TO test2;
+
+ALTER TABLE public.test2
+	SET SCHEMA test;
+
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+
+ALTER TABLE test.test2
+	DROP COLUMN description;
+
+DROP TABLE test.test2;
+
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+
+SELECT int_add(1, 1);
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index f21fa14..f418e93 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 8b9d6a9..e8dc26e 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -125,6 +125,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbench         SYSTEM "pgbench.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
 <!ENTITY pgcrypto        SYSTEM "pgcrypto.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..70b4fd1
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,603 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> extenstion provides detailed session
+    and/or object audit logging via the standard logging facility.  The goal
+    is to provide the tools needed to produce audit logs required to pass any
+    goverment, financial, or ISO certification audit.
+  </para>
+
+  <para>
+    An audit is an official inspection of an individual's or organization's
+    accounts, typically by an independent body.  The information gathered by
+    <filename>pg_audit</filename> is properly called an audit trail or audit
+    log.  The term audit log is used in this documentation.
+  </para>
+
+  <sect2>
+    <title>Why <literal>pg_audit</>?</title>
+  
+    <para>
+      Basic statement logging can be provided by the standard logging facility
+      using <literal>log_statements = all</>.  This is acceptable for monitoring
+      and other usages but does not provide the level of detail generally
+      required for an audit.  It is not enough to have a list of all the
+      operations performed against the database. It must also be possible to
+      find particular statements that are of interest to an auditor.
+    </para>
+
+    <para>
+      For example, an auditor may want to verify that a particular table was
+      created inside a documented maintence window.  This might seem like a
+      simple job for grep, but what if you are presented with something like
+      this (intentionally obfuscated) example:
+    </para>
+
+    <programlisting>
+DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+    
+    <para>
+      Standard logging will give you this:
+    </para>
+
+    <programlisting>
+LOG:  statement: DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+    
+    <para>
+      It appears that finding the table of interest may require some knowledge
+      of the code in cases where tables are created dynamically.  This is not
+      ideal since it would be preferrable to just search on the table name.
+      This is where <literal>pg_audit</> comes in.  For the same input,
+      it will produce this output in the log:
+    </para>
+
+    <programlisting>
+AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;"
+AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.important_table,CREATE TABLE important_table (id INT)
+    </programlisting>
+
+    <para>
+      Not only is the <literal>DO</> block logged, but substatement 2 contains
+      the full text of the <literal>CREATE TABLE</> with the statement type,
+      object type, and full-qualified name to make searches easy.
+    </para>
+
+    <para>
+      When logging <literal>SELECT</> and <literal>DML</> statements,
+      <literal>pg_audit</> can be configured to log a separate entry for each
+      relation referenced in a statement.  No parsing is required to find all
+      statements that touch a particular table.  In fact, the goal is that the
+      statement text is provided primarily for deep forensics and should not be
+      the directly required for any search.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Usage Considerations</title>
+    
+    <para>
+      Depending on settings, it is possible for <literal>pg_audit</literal> to
+      generate an enormous volume of logging.  Be careful to determine
+      exactly what needs to be audit logged in your environment to avoid
+      logging too much.
+    </para>
+    
+    <para>
+      For example, when working in an OLAP environment it would probably not be
+      wise to audit log inserts into a large fact table.  The size of the log
+      file will likely be many times the actual data size of the inserts because
+      the log file is expressed as text.  Since logs are generally stored with
+      the OS this may lead to disk space being exhausted very
+      quickly.  In cases where it is not possible to limit audit logging to
+      certain tables, be sure to assess the performance impact while testing
+      and allocate plenty of space on the log volume.  This may also be true for
+      OLTP environments.  Even if the insert volume is not as high, the
+      performance impact of audit logging may still noticeably affect latency.
+    </para>
+    
+    <para>
+      To limit the number of relations audit logged for <literal>SELECT</>
+      and <literal>DML</> statments, consider using object audit logging
+      (see <xref linkend="pgaudit-object-audit-logging">).  Object audit logging
+      allows selection of the relations to be logged allowing for reduction
+      of the overall log volume.  However, when new relations are added they
+      must be explicitly added to object audit logging.  A programmatic
+      solution where specified tables are excluded from logging and all others
+      are included may be a good option in this case.
+    </para>
+  </sect2>
+  
+  <sect2>
+    <title>Settings</title>
+    
+    <para>
+      Settings may be modified only by a superuser. Allowing normal users to
+      change their settings would defeat the point of an audit log.
+    </para>
+      
+    <para>
+      Settings can be specified globally (in
+      <filename>postgresql.conf</filename> or using
+      <literal>ALTER SYSTEM ... SET</>), at the database level (using
+      <literal>ALTER DATABASE ... SET</literal>), or at the role level (using
+      <literal>ALTER ROLE ... SET</literal>).  Note that settings are not
+      inherited through normal role inheritance and <literal>SET ROLE</> will
+      not alter a user's <literal>pg_audit</> settings.  This is a limitation
+      of the roles system and not inherent to <literal>pg_audit</>.
+    </para>
+      
+    <para>
+      The <literal>pg_audit</> extension must be loaded in
+      <xref linkend="guc-shared-preload-libraries">.  Otherwise, an error
+      will be raised at load time and no audit logging will occur.
+    </para>
+
+    <variablelist>
+      <varlistentry id="guc-pgaudit-log" xreflabel="pg_audit.log">
+        <term><varname>pg_audit.log</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies which classes of statements will be logged by session
+            audit logging.  Possible values are:
+          </para>
+
+          <itemizedlist>
+            <listitem>
+              <para>
+                <literal>READ</literal> - <literal>SELECT</literal> and
+                <literal>COPY</literal> when the source is a relation or a
+                query.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>WRITE</literal> - <literal>INSERT</literal>,
+                <literal>UPDATE</literal>, <literal>DELETE</literal>,
+                <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+                destination is a relation.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>FUNCTION</literal> - Function calls and
+                <literal>DO</literal> blocks.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>ROLE</literal> - Statements related to roles and
+                privileges: <literal>GRANT</literal>,
+                <literal>REVOKE</literal>,
+                <literal>CREATE/ALTER/DROP ROLE</literal>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>DDL</literal> - All <literal>DDL</> that is not included
+                in the <literal>ROLE</> class plus <literal>REINDEX</>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>PARAMETER</literal> - Parameters that were passed for the
+                statement.  Parameters immediately follow the statement text.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>MISC</literal> - Miscellaneous commands, e.g.
+                <literal>DISCARD</literal>, <literal>FETCH</literal>,
+                <literal>CHECKPOINT</literal>, <literal>VACUUM</literal>.
+              </para>
+            </listitem>
+          </itemizedlist>
+            
+          <para>
+            Multiple classes can be provided using a comma-separated list and
+            classes can be subtracted by prefacing the class with a
+            <literal>-</> sign (see <xref linkend="pgaudit-session-audit-logging">).
+            The default is <literal>none</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-notice" xreflabel="pg_audit.log_notice">
+        <term><varname>pg_audit.log_notice</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_notice</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that the audit log messages should be raised as
+            <literal>NOTICE</> instead of <literal>LOG</>.  The primary
+            advantage is that <literal>NOTICE</> messages can be exposed
+            through the user interface.  This setting is used for regression
+            testing and may also be useful to end users for testing.  It is not
+            intended to be used in a production environment as it will leak
+            which statements are being logged to the user. The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-relation" xreflabel="pg_audit.log_relation">
+        <term><varname>pg_audit.log_relation</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_relation</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies whether session audit logging should create a separate
+            log entry for each relation referenced in a <literal>SELECT</> or
+            <literal>DML</> statement.  This is a useful shortcut for exhaustive
+            logging without using object audit logging.  The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-role" xreflabel="pg_audit.role">
+        <term><varname>pg_audit.role</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.role</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies the master role to use for object audit logging.  Muliple
+            audit roles can be defined by granting them to the master role.
+            This allows multiple groups to be in charge of different aspects
+            of audit logging.  There is no default.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </sect2>
+
+  <sect2 id="pgaudit-session-audit-logging">
+    <title>Session Audit Logging</title>
+
+    <para>
+      Session audit logging provides detailed logs of all statements executed
+      by a user in the backend.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is enabled with the <xref linkend="guc-pgaudit-log">
+        setting.
+
+        Enable session logging for all <literal>DML</> and <literal>DDL</> and
+        log all relations in <literal>DML</> statements:
+          <programlisting>
+set pg_audit.log = 'write, ddl';
+set pg_audit.log_relation = on;
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except <literal>MISC</> and
+        raise audit log messages as <literal>NOTICE</>:
+          <programlisting>
+set pg_audit.log = 'all, -misc';
+set pg_audit.log_notice = on;
+          </programlisting>
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example session audit logging is used to for logging
+        <literal>DDL</> and <literal>SELECT</> statements.  Note that the
+        insert statement is not logged since the <literal>WRITE</> class
+        is not enabled
+      </para>
+
+      <para>
+        SQL:
+      </para>
+      <programlisting>
+set pg_audit.log = 'read, ddl';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+
+select *
+    from account;
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,2,1,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2 id="pgaudit-object-audit-logging">
+    <title>Object Auditing</title>
+
+    <para>
+      Object audit logging logs statements that affect a particular relation.
+      Only <literal>SELECT</>, <literal>INSERT</>, <literal>UPDATE</> and
+      <literal>DELETE</> commands are supported.  <literal>TRUNCATE</> is not
+      included because there is no specific privilege for it.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level audit logging is implemented via the roles system.  The
+        <xref linkend="guc-pgaudit-role"> setting defines the role that
+        will be used for audit logging.  An object will be audit logged when the
+        audit role has permissions for the command executed or inherits the
+        permissions from another role.  This allows you to effectively
+        have multiple audit roles even though there is a single master role
+        in any context.
+      </para>
+
+      <para>
+      Set <xref linkend="guc-pgaudit-role"> to <literal>auditor</> and
+      grant <literal>SELECT</> and <literal>DELETE</> privileges on the
+      <literal>account</> table.  Any <literal>SELECT</> or
+      <literal>DELETE</> statements on <literal>account</> will now be
+      logged:
+      </para>
+      
+      <programlisting>
+set pg_audit.role = 'auditor';
+
+grant select, delete
+   on public.account
+   to auditor;
+      </programlisting>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example object audit logging is used to illustrate how a
+        granular approach may be taken towards logging of <literal>SELECT</>
+        and <literal>DML</> statements.  Note that logging on the
+        <literal>account</> table is controlled by column-level permissions,
+        while logging on <literal>account_role_map</> is table-level.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+set pg_audit.role = 'auditor';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to auditor;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to auditor;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to auditor;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is compliant CSV format only if the log line prefix portion
+          of each log entry is removed.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</> - <literal>SESSION</> or
+            <literal>OBJECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT_ID</> - Unique statement ID for this session.
+            Each statement ID represents a backend call.  Statement IDs are
+            sequental even if some statements are not logged.  There may be
+            multiple entries for a statement ID when more than one relation
+            is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>SUBSTATEMENT_ID</> - Sequential ID for each
+            substatement within the main statement.  For example, calling
+            a function from a query.  Substatement IDs are continuous
+            even if some substatements are not logged.  There may be multiple
+            entries for a substatement ID when more than one relation is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</> - e.g. (<literal>READ</>,
+            <literal>ROLE</>) (see <xref linkend="guc-pgaudit-log">).
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</> - e.g. <literal>ALTER TABLE</>,
+            <literal>SELECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</> - <literal>TABLE</>,
+            <literal>INDEX</>, <literal>VIEW</>, etc.
+            Available for <literal>SELECT</>, <literal>DML</> and most
+            <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</> - The fully-qualified object name
+            (e.g. public.account).  Available for <literal>SELECT</>,
+            <literal>DML</> and most <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</> - Statement executed on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+
+    <para>
+      Use <xref linkend="guc-log-line-prefix"> to add any other fields that
+      are needed to satisfy your audit log requirements.  A typical log line
+      prefix might be <literal>'%m %u %d: '</> which would provide the date/time,
+      user name, and database name for each audit log.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Caveats</title>
+    
+    <itemizedlist>
+      <listitem>
+        <para>
+          Object renames are logged under the name they were renamed to.
+          For example, renaming a table will produce the following result:
+        </para>
+
+        <programlisting>
+ALTER TABLE test RENAME TO test2;
+          
+AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,ALTER TABLE test RENAME TO test2
+        </programlisting>
+      </listitem>
+
+      <listitem>
+        <para>
+          Autovacuum and Autoanalyze are not logged, nor are they intended to be.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#33Tatsuo Ishii
ishii@postgresql.org
In reply to: David Steele (#32)
Re: Auditing extension for PostgreSQL (Take 2)

This patch does not apply cleanly due to the moving of pgbench (patch
to filelist.sgml failed).

Best regards,
--
Tatsuo Ishii
SRA OSS, Inc. Japan
English: http://www.sraoss.co.jp/index_en.php
Japanese:http://www.sraoss.co.jp

Attached is the v7 pg_audit patch.

I've tried to address Peter's documentation concerns by cleaning up the
terminology and adding a real-world case plus usage recommendations.
The word "auditing" has been expunged from the docs in favor of the term
"audit logging".

Per Simon's request, there is now a pg_audit.log_relation setting that
makes session audit logging exhaustively log all relations as it did
before. The ROLE logging class is back as well.

Simon also suggested a way that pg_audit could be tested with standard
regression so I have converted all tests over and removed test.pl.

Sawada, I'd certainly appreciate it if you'd try again and see if you
are still getting a segfault with your test code (which you can find in
the regression tests).

Currently the patch will compile on master (I tested with b22a36a) or
optionally with Alvaro's deparse patches applied (only 0001 & 0002
needed). I've supplied a different regression test out file
(expected/pg_audit-deparse.out) which can be copied over the standard
out file (expected/pg_audit.out) if you'd like to do regression on
pg_audit with deparse. The small section of code that calls
pg_event_trigger_ddl_commands() can be compiled by defining DEPARSE or
removed the #ifdefs around that block.

Please let me know if I've missed anything and I look forward to
comments and questions.

Thanks,
--
- David Steele
david@pgmasters.net

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

#34David Steele
david@pgmasters.net
In reply to: Tatsuo Ishii (#33)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/14/15 7:13 PM, Tatsuo Ishii wrote:

This patch does not apply cleanly due to the moving of pgbench (patch
to filelist.sgml failed).

Thank you for pointing that out!

Ironic that it was the commit directly after the one I was testing with
that broke the patch. It appears the end of the last CF is a very bad
time to be behind HEAD.

Fixed in attached v8 patch.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v8.patchtext/plain; charset=UTF-8; name=pg_audit-v8.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index d63e441..ed9cf6a 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -28,6 +28,7 @@ SUBDIRS = \
 		oid2name	\
 		pageinspect	\
 		passwordcheck	\
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/.gitignore b/contrib/pg_audit/.gitignore
new file mode 100644
index 0000000..a5267cf
--- /dev/null
+++ b/contrib/pg_audit/.gitignore
@@ -0,0 +1,5 @@
+log/
+results/
+tmp_check/
+regression.diffs
+regression.out
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..7b36011
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,21 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+REGRESS = pg_audit
+REGRESS_OPTS = --temp-config=$(top_srcdir)/contrib/pg_audit/pg_audit.conf
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/expected/pg_audit-deparse.out b/contrib/pg_audit/expected/pg_audit-deparse.out
new file mode 100644
index 0000000..66fd20d
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit-deparse.out
@@ -0,0 +1,897 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,2,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+                  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+drop table test2;
+drop table test3;
+drop table test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,4,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,5,1,DDL,CREATE SCHEMA,SCHEMA,test,CREATE SCHEMA test;
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,2,DDL,CREATE TABLE AS,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,9,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,10,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,10,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,11,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+NOTICE:  AUDIT: SESSION,12,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+NOTICE:  AUDIT: SESSION,13,1,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,14,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+NOTICE:  AUDIT: SESSION,15,1,MISC,CLOSE CURSOR,,,CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,15,2,MISC,COMMIT,,,COMMIT;
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,16,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+NOTICE:  AUDIT: SESSION,17,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,18,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,18,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,19,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,19,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,19,2,DDL,CREATE TABLE,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,20,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,21,1,ROLE,GRANT,TABLE,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,22,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,23,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,24,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,25,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,25,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,26,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,26,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,27,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,28,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,29,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,30,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,30,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,30,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,30,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,30,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test cursors and functions in a do block
+CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;
+NOTICE:  AUDIT: SESSION,31,1,DDL,CREATE FUNCTION,FUNCTION,public.test(),"CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;"
+SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,1,READ,SELECT,,,SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,3,READ,SELECT,TABLE,public.test,SELECT * FROM test
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+    1
+(1 row)
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,33,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,34,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,37,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,38,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+NOTICE:  AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+NOTICE:  AUDIT: SESSION,41,1,DDL,CREATE TABLE,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,41,2,DDL,CREATE TABLE,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,41,3,DDL,CREATE SCHEMA,SCHEMA,foo,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,41,3,DDL,CREATE SCHEMA,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,41,3,DDL,CREATE SCHEMA,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,42,1,DDL,CREATE FUNCTION,FUNCTION,"public.int_add(integer,integer)","CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,43,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,43,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,44,1,DDL,CREATE AGGREGATE,AGGREGATE,public.sum_test(integer),"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,45,1,DDL,ALTER AGGREGATE,AGGREGATE,public.sum_test2(integer),ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,46,1,DDL,CREATE CONVERSION,CONVERSION,public.conversion_test,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,47,1,DDL,ALTER CONVERSION,CONVERSION,public.conversion_test2,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,48,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,49,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,50,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
diff --git a/contrib/pg_audit/expected/pg_audit.out b/contrib/pg_audit/expected/pg_audit.out
new file mode 100644
index 0000000..987d43a
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit.out
@@ -0,0 +1,880 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,2,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+                  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+drop table test2;
+drop table test3;
+drop table test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,4,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,5,1,DDL,CREATE SCHEMA,,,CREATE SCHEMA test;
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,2,DDL,CREATE TABLE AS,,,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,9,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,10,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,10,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,11,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+NOTICE:  AUDIT: SESSION,12,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+NOTICE:  AUDIT: SESSION,13,1,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,14,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+NOTICE:  AUDIT: SESSION,15,1,MISC,CLOSE CURSOR,,,CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,15,2,MISC,COMMIT,,,COMMIT;
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,16,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+NOTICE:  AUDIT: SESSION,17,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,18,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,18,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,19,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,19,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,20,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,21,1,ROLE,GRANT,,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,22,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,23,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,24,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,25,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,25,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,26,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,26,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,27,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,28,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,29,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,30,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,30,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,30,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,30,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,30,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test cursors and functions in a do block
+CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;
+NOTICE:  AUDIT: SESSION,31,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;"
+SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,1,READ,SELECT,,,SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT public.test();
+NOTICE:  AUDIT: SESSION,32,3,READ,SELECT,TABLE,public.test,SELECT * FROM test
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+    1
+(1 row)
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,33,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,34,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,37,1,DDL,ALTER TABLE,INDEX,public.test_pkey,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,38,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,40,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+NOTICE:  AUDIT: SESSION,41,1,DDL,CREATE TABLE,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,41,2,DDL,CREATE TABLE,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,41,3,DDL,CREATE SCHEMA,,,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,42,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,43,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,43,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,44,1,DDL,CREATE AGGREGATE,,,"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,45,1,DDL,ALTER AGGREGATE,,,ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,46,1,DDL,CREATE CONVERSION,,,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,47,1,DDL,ALTER CONVERSION,,,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,48,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,49,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,50,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..b73fcbf
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1786 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pg_audit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pg_audit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * auditLogRelation controls whether all relations are logged for READ and
+ * WRITE classes during session logging.
+ */
+bool auditLogRelation = false;
+
+/*
+ * auditLogNotice raises a notice as well as logging via the standard facility.
+ * This is primarily for the benefit of testing.
+ */
+bool auditLogNotice = false;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pg_audit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_PARAMETER		"PARAMETER"
+#define CLASS_READ			"READ"
+#define CLASS_ROLE			"ROLE"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* Function execution */
+	LOG_PARAMETER = (1 << 4),
+
+	/* SELECT */
+	LOG_READ = (1 << 5),
+
+	/* GRANT, REVOKE, CREATE/ALTER/DROP ROLE */
+	LOG_ROLE = (1 << 6),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 7),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_ALTER_ROLE	"ALTER ROLE"
+#define COMMAND_DROP_ROLE	"CREATE ROLE"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	int64 stackId;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static int64 statementTotal = 0;
+static int64 substatementTotal = 0;
+static int64 stackTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack to the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Create the unique stackId - used to keep the stack sane when memory
+	 * contexts are freed unexpectedly.
+	 */
+	stackItem->stackId = ++stackTotal;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop(int64 stackId)
+{
+	/* Make sure what we want to delete is at the top of the stack */
+	if (auditEventStack != NULL && auditEventStack->stackId == stackId)
+	{
+		MemoryContextDelete(auditEventStack->contextAudit);
+	}
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent, classifies it, then logs it if permissions were granted
+ * via roles or if the statement belongs in a class that is being logged.
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	MemoryContext contextOld;
+	StringInfoData auditStr;
+
+	/* By default put everything in the MISC class. */
+	enum LogClass class = LOG_MISC;
+	const char *className = CLASS_MISC;
+
+	/* Classify the statement using log stmt level and the command tag */
+	switch (stackItem->auditEvent.logStmtLevel)
+	{
+		/* All mods go in WRITE class */
+		case LOGSTMT_MOD:
+			className = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		/* Separate ROLE from other DDL statements */
+		case LOGSTMT_DDL:
+			/* Identify role statements */
+			if (stackItem->auditEvent.commandTag == T_GrantStmt ||
+				stackItem->auditEvent.commandTag == T_GrantRoleStmt ||
+				stackItem->auditEvent.commandTag == T_CreateRoleStmt ||
+				(stackItem->auditEvent.commandTag == T_RenameStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_ALTER_ROLE) == 0) ||
+				(stackItem->auditEvent.commandTag == T_DropStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_DROP_ROLE) == 0) ||
+				stackItem->auditEvent.commandTag == T_DropRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleSetStmt)
+			{
+				className = CLASS_ROLE;
+				class = LOG_ROLE;
+			}
+			/* Else log as DDL */
+			else
+			{
+				className = CLASS_DDL;
+				class = LOG_DDL;
+			}
+
+		/* Figure out the rest */
+		case LOGSTMT_ALL:
+			switch (stackItem->auditEvent.commandTag)
+			{
+				/* READ statements */
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					className = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				/* Reindex is DDL (because cluster is DDL) */
+				case T_ReindexStmt:
+					className = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				/* FUNCTION statements */
+				case T_DoStmt:
+					className = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * Only log the statement if:
+	 *
+	 * 1. If permissions were granted via roles
+	 * 2. The statement belongs to a class that is being logged
+	 */
+	if (!stackItem->auditEvent.granted && !(auditLogBitmap & class))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogBitmap & LOG_PARAMETER &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	elog(auditLogNotice ? NOTICE : LOG,
+		 "AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			className, auditStr.data);
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+audit_on_acl(Datum aclDatum,
+			 Oid auditOid,
+			 AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+audit_on_relation(Oid relOid,
+				  Oid auditOid,
+				  AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = audit_on_acl(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+audit_on_attribute(Oid relOid,
+				   AttrNumber attNum,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = audit_on_acl(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+audit_on_any_attribute(Oid relOid,
+					   Oid auditOid,
+					   Bitmapset *attributeSet,
+					   AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (audit_on_attribute(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			audit_on_attribute(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/*
+		 * If this is the first rte then session log unless auditLogRelation
+		 * is set.
+		 */
+		if (first && !auditLogRelation)
+		{
+			auditEventStack->auditEvent.objectName = "";
+			auditEventStack->auditEvent.objectType = "";
+
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (audit_on_relation(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->selectedCols,
+											   ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEventStack->auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEventStack->auditEvent.granted =
+							audit_on_any_attribute(relOid, auditOid,
+												   rte->modifiedCols,
+												   auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Do relation level logging if a grant was found */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		/* Do relation level logging if auditLogRelation is set */
+		if (auditLogRelation)
+		{
+			auditEventStack->auditEvent.logged = false;
+			auditEventStack->auditEvent.granted = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop(stackItem->stackId);
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute */
+		case OAT_FUNCTION_EXECUTE:
+			if (auditLogBitmap & LOG_FUNCTION)
+				log_function_execute(objectId);
+			break;
+
+		/* Log create */
+		case OAT_POST_CREATE:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter */
+		case OAT_POST_ALTER:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop */
+		case OAT_DROP:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessDrop *drop = arg;
+
+				if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+static ExecutorEnd_hook_type next_ExecutorEnd_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pg_audit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ExecutorEnd to pop statement audit event off the stack.
+ */
+static void
+pg_audit_ExecutorEnd_hook(QueryDesc *queryDesc)
+{
+	/* Call the next hook or standard function */
+	if (next_ExecutorEnd_hook)
+		next_ExecutorEnd_hook(queryDesc);
+	else
+		standard_ExecutorEnd(queryDesc);
+
+	/* Pop the audit event off the stack */
+	if (!internalStatement)
+	{
+		stack_pop(auditEventStack->stackId);
+	}
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pg_audit_ProcessUtility_hook(Node *parsetree,
+							 const char *queryString,
+							 ProcessUtilityContext context,
+							 ParamListInfo params,
+							 DestReceiver *dest,
+							 char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+	int64 stackId;
+
+	/* Allocate the audit event */
+	if (!IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackId = stackItem->stackId;
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap != 0 &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/* Process the audit event if there is one. */
+	if (stackItem != NULL)
+	{
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !stackItem->auditEvent.logged &&
+			!IsAbortedTransactionBlockState())
+			log_audit_event(stackItem);
+
+		stack_pop(stackId);
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pg_audit_object_access_hook(ObjectAccessType access,
+							Oid classId,
+							Oid objectId,
+							int subId,
+							void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&
+		auditLogBitmap & (LOG_DDL | LOG_FUNCTION) && auditEventStack)
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+#ifdef DEPARSE
+	/* Continue only if session DDL logging is enabled */
+	if (auditLogBitmap & LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_ddl_command_end()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		auditEventStack->auditEvent.logStmtLevel =
+			GetCommandLogLevel(eventData->parsetree);
+		auditEventStack->auditEvent.commandTag =
+			nodeTag(eventData->parsetree);
+		auditEventStack->auditEvent.command =
+			CreateCommandTag(eventData->parsetree);
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT UPPER(object_type), identity\n"
+				"  FROM pg_event_trigger_ddl_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply object name and type for audit event */
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 1);
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 2);
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+#endif
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_sql_drop()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type),\n"
+				"       schema_name, object_name, object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			char *schemaName;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			schemaName = SPI_getvalue(spiTuple, spiTupDesc, 5);
+
+			if (!(pg_strcasecmp(auditEventStack->auditEvent.objectType,
+							"TYPE") == 0 ||
+				  pg_strcasecmp(schemaName, "pg_toast") == 0))
+			{
+				auditEventStack->auditEvent.objectName =
+						SPI_getvalue(spiTuple, spiTupDesc, 7);
+
+				log_audit_event(auditEventStack);
+			}
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pg_audit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then the token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_PARAMETER) == 0)
+			class = LOG_PARAMETER;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_ROLE) == 0)
+			class = LOG_ROLE;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pg_audit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pg_audit_log from extra (ignoring newval, which has already been
+ * converted to a bitmap above). Note that extra may not be set if the
+ * assignment is to be suppressed.
+ */
+static void
+assign_pg_audit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * Defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable object auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * Controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable session auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pg_audit_log,
+							   assign_pg_audit_log,
+							   NULL);
+
+	/*
+	 * pg_audit.log_relation = on
+	 *
+	 * Controls whether relations get separate log entries during session
+	 * logging of READ and WRITE classes.  This works as if all relations in the
+	 * database had been added to the audit role and provides a shortcut when
+	 * really detailed logging of absolutely every relation is required.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_relation",
+							 "Enable session relation logging",
+							 NULL,
+							 &auditLogRelation,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+
+	/*
+	 * pg_audit.log_notice = on
+	 *
+	 * Audit logging is raised as notices that can be seen on the client.  This is
+	 * intended for testing purposes.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_notice",
+							 "Raise a notice when logging",
+							 NULL,
+							 &auditLogNotice,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pg_audit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook;
+
+	next_ExecutorEnd_hook = ExecutorEnd_hook;
+	ExecutorEnd_hook = pg_audit_ExecutorEnd_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pg_audit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pg_audit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.conf b/contrib/pg_audit/pg_audit.conf
new file mode 100644
index 0000000..e9f4a22
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.conf
@@ -0,0 +1 @@
+shared_preload_libraries = pg_audit
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/sql/pg_audit.sql b/contrib/pg_audit/sql/pg_audit.sql
new file mode 100644
index 0000000..2a0436b
--- /dev/null
+++ b/contrib/pg_audit/sql/pg_audit.sql
@@ -0,0 +1,533 @@
+-- Load pg_audit module
+create extension pg_audit;
+
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+
+--
+-- Create auditor role
+CREATE ROLE auditor;
+
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+DROP TABLE test;
+
+--
+-- Create second test user
+\connect contrib_regression super
+
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+
+SELECT *
+  FROM test3, test2;
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+
+\connect contrib_regression user2
+
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+
+--
+-- Drop test tables
+drop table test2;
+drop table test3;
+drop table test4;
+
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+
+--
+-- Create test schema
+CREATE SCHEMA test;
+
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+1	user1	HASH2	yada, yada
+\.
+
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+
+EXECUTE pgclassstmt (1);
+DEALLOCATE pgclassstmt;
+
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+
+FETCH NEXT FROM ctest;
+CLOSE ctest;
+COMMIT;
+
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+EXECUTE pgclassstmt (1);
+
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+
+--
+-- Check that analyze is logged
+ANALYZE test;
+
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+
+SELECT *
+  FROM test;
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+
+SELECT 1,
+	   current_user;
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+
+explain select 1;
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+INSERT INTO TEST (id)
+		  VALUES (2);
+INSERT INTO TEST (id)
+		  VALUES (3);
+
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+
+--
+-- Test cursors and functions in a do block
+CREATE FUNCTION public.test()
+	RETURNS INT LANGUAGE plpgsql AS $$
+DECLARE
+	cur1 CURSOR FOR SELECT * FROM test;
+	tmp INT;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 INTO tmp;
+	CLOSE cur1;
+	RETURN tmp;
+end $$;
+
+SELECT public.test();
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+
+ALTER TABLE public.test
+	RENAME TO test2;
+
+ALTER TABLE public.test2
+	SET SCHEMA test;
+
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+
+ALTER TABLE test.test2
+	DROP COLUMN description;
+
+DROP TABLE test.test2;
+
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+
+SELECT int_add(1, 1);
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 5773095..20a4e62 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index ab935a6..7f39f42 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -125,6 +125,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
 <!ENTITY pgcrypto        SYSTEM "pgcrypto.sgml">
 <!ENTITY pgfreespacemap  SYSTEM "pgfreespacemap.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..dea10a8
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,613 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> extenstion provides detailed session
+    and/or object audit logging via the standard logging facility.  The goal
+    is to provide the tools needed to produce audit logs required to pass any
+    goverment, financial, or ISO certification audit.
+  </para>
+
+  <para>
+    An audit is an official inspection of an individual's or organization's
+    accounts, typically by an independent body.  The information gathered by
+    <filename>pg_audit</filename> is properly called an audit trail or audit
+    log.  The term audit log is used in this documentation.
+  </para>
+
+  <sect2>
+    <title>Why <literal>pg_audit</>?</title>
+  
+    <para>
+      Basic statement logging can be provided by the standard logging facility
+      using <literal>log_statements = all</>.  This is acceptable for monitoring
+      and other usages but does not provide the level of detail generally
+      required for an audit.  It is not enough to have a list of all the
+      operations performed against the database. It must also be possible to
+      find particular statements that are of interest to an auditor.
+    </para>
+
+    <para>
+      For example, an auditor may want to verify that a particular table was
+      created inside a documented maintence window.  This might seem like a
+      simple job for grep, but what if you are presented with something like
+      this (intentionally obfuscated) example:
+    </para>
+
+    <programlisting>
+DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+    
+    <para>
+      Standard logging will give you this:
+    </para>
+
+    <programlisting>
+LOG:  statement: DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+    
+    <para>
+      It appears that finding the table of interest may require some knowledge
+      of the code in cases where tables are created dynamically.  This is not
+      ideal since it would be preferrable to just search on the table name.
+      This is where <literal>pg_audit</> comes in.  For the same input,
+      it will produce this output in the log:
+    </para>
+
+    <programlisting>
+AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;"
+AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.important_table,CREATE TABLE important_table (id INT)
+    </programlisting>
+
+    <para>
+      Not only is the <literal>DO</> block logged, but substatement 2 contains
+      the full text of the <literal>CREATE TABLE</> with the statement type,
+      object type, and full-qualified name to make searches easy.
+    </para>
+
+    <para>
+      When logging <literal>SELECT</> and <literal>DML</> statements,
+      <literal>pg_audit</> can be configured to log a separate entry for each
+      relation referenced in a statement.  No parsing is required to find all
+      statements that touch a particular table.  In fact, the goal is that the
+      statement text is provided primarily for deep forensics and should not be
+      the directly required for any search.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Usage Considerations</title>
+    
+    <para>
+      Depending on settings, it is possible for <literal>pg_audit</literal> to
+      generate an enormous volume of logging.  Be careful to determine
+      exactly what needs to be audit logged in your environment to avoid
+      logging too much.
+    </para>
+    
+    <para>
+      For example, when working in an OLAP environment it would probably not be
+      wise to audit log inserts into a large fact table.  The size of the log
+      file will likely be many times the actual data size of the inserts because
+      the log file is expressed as text.  Since logs are generally stored with
+      the OS this may lead to disk space being exhausted very
+      quickly.  In cases where it is not possible to limit audit logging to
+      certain tables, be sure to assess the performance impact while testing
+      and allocate plenty of space on the log volume.  This may also be true for
+      OLTP environments.  Even if the insert volume is not as high, the
+      performance impact of audit logging may still noticeably affect latency.
+    </para>
+    
+    <para>
+      To limit the number of relations audit logged for <literal>SELECT</>
+      and <literal>DML</> statments, consider using object audit logging
+      (see <xref linkend="pgaudit-object-audit-logging">).  Object audit logging
+      allows selection of the relations to be logged allowing for reduction
+      of the overall log volume.  However, when new relations are added they
+      must be explicitly added to object audit logging.  A programmatic
+      solution where specified tables are excluded from logging and all others
+      are included may be a good option in this case.
+    </para>
+  </sect2>
+  
+  <sect2>
+    <title>Settings</title>
+    
+    <para>
+      Settings may be modified only by a superuser. Allowing normal users to
+      change their settings would defeat the point of an audit log.
+    </para>
+      
+    <para>
+      Settings can be specified globally (in
+      <filename>postgresql.conf</filename> or using
+      <literal>ALTER SYSTEM ... SET</>), at the database level (using
+      <literal>ALTER DATABASE ... SET</literal>), or at the role level (using
+      <literal>ALTER ROLE ... SET</literal>).  Note that settings are not
+      inherited through normal role inheritance and <literal>SET ROLE</> will
+      not alter a user's <literal>pg_audit</> settings.  This is a limitation
+      of the roles system and not inherent to <literal>pg_audit</>.
+    </para>
+      
+    <para>
+      The <literal>pg_audit</> extension must be loaded in
+      <xref linkend="guc-shared-preload-libraries">.  Otherwise, an error
+      will be raised at load time and no audit logging will occur.
+    </para>
+
+    <variablelist>
+      <varlistentry id="guc-pgaudit-log" xreflabel="pg_audit.log">
+        <term><varname>pg_audit.log</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies which classes of statements will be logged by session
+            audit logging.  Possible values are:
+          </para>
+
+          <itemizedlist>
+            <listitem>
+              <para>
+                <literal>READ</literal> - <literal>SELECT</literal> and
+                <literal>COPY</literal> when the source is a relation or a
+                query.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>WRITE</literal> - <literal>INSERT</literal>,
+                <literal>UPDATE</literal>, <literal>DELETE</literal>,
+                <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+                destination is a relation.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>FUNCTION</literal> - Function calls and
+                <literal>DO</literal> blocks.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>ROLE</literal> - Statements related to roles and
+                privileges: <literal>GRANT</literal>,
+                <literal>REVOKE</literal>,
+                <literal>CREATE/ALTER/DROP ROLE</literal>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>DDL</literal> - All <literal>DDL</> that is not included
+                in the <literal>ROLE</> class plus <literal>REINDEX</>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>PARAMETER</literal> - Parameters that were passed for the
+                statement.  Parameters immediately follow the statement text.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>MISC</literal> - Miscellaneous commands, e.g.
+                <literal>DISCARD</literal>, <literal>FETCH</literal>,
+                <literal>CHECKPOINT</literal>, <literal>VACUUM</literal>.
+              </para>
+            </listitem>
+          </itemizedlist>
+            
+          <para>
+            Multiple classes can be provided using a comma-separated list and
+            classes can be subtracted by prefacing the class with a
+            <literal>-</> sign (see <xref linkend="pgaudit-session-audit-logging">).
+            The default is <literal>none</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-notice" xreflabel="pg_audit.log_notice">
+        <term><varname>pg_audit.log_notice</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_notice</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that the audit log messages should be raised as
+            <literal>NOTICE</> instead of <literal>LOG</>.  The primary
+            advantage is that <literal>NOTICE</> messages can be exposed
+            through the user interface.  This setting is used for regression
+            testing and may also be useful to end users for testing.  It is not
+            intended to be used in a production environment as it will leak
+            which statements are being logged to the user. The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-relation" xreflabel="pg_audit.log_relation">
+        <term><varname>pg_audit.log_relation</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_relation</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies whether session audit logging should create a separate
+            log entry for each relation referenced in a <literal>SELECT</> or
+            <literal>DML</> statement.  This is a useful shortcut for exhaustive
+            logging without using object audit logging.  The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-role" xreflabel="pg_audit.role">
+        <term><varname>pg_audit.role</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.role</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies the master role to use for object audit logging.  Muliple
+            audit roles can be defined by granting them to the master role.
+            This allows multiple groups to be in charge of different aspects
+            of audit logging.  There is no default.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </sect2>
+
+  <sect2 id="pgaudit-session-audit-logging">
+    <title>Session Audit Logging</title>
+
+    <para>
+      Session audit logging provides detailed logs of all statements executed
+      by a user in the backend.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is enabled with the <xref linkend="guc-pgaudit-log">
+        setting.
+
+        Enable session logging for all <literal>DML</> and <literal>DDL</> and
+        log all relations in <literal>DML</> statements:
+          <programlisting>
+set pg_audit.log = 'write, ddl';
+set pg_audit.log_relation = on;
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except <literal>MISC</> and
+        raise audit log messages as <literal>NOTICE</>:
+          <programlisting>
+set pg_audit.log = 'all, -misc';
+set pg_audit.log_notice = on;
+          </programlisting>
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example session audit logging is used to for logging
+        <literal>DDL</> and <literal>SELECT</> statements.  Note that the
+        insert statement is not logged since the <literal>WRITE</> class
+        is not enabled
+      </para>
+
+      <para>
+        SQL:
+      </para>
+      <programlisting>
+set pg_audit.log = 'read, ddl';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+
+select *
+    from account;
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,2,1,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2 id="pgaudit-object-audit-logging">
+    <title>Object Auditing</title>
+
+    <para>
+      Object audit logging logs statements that affect a particular relation.
+      Only <literal>SELECT</>, <literal>INSERT</>, <literal>UPDATE</> and
+      <literal>DELETE</> commands are supported.  <literal>TRUNCATE</> is not
+      included because there is no specific privilege for it.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level audit logging is implemented via the roles system.  The
+        <xref linkend="guc-pgaudit-role"> setting defines the role that
+        will be used for audit logging.  An object will be audit logged when the
+        audit role has permissions for the command executed or inherits the
+        permissions from another role.  This allows you to effectively
+        have multiple audit roles even though there is a single master role
+        in any context.
+      </para>
+
+      <para>
+      Set <xref linkend="guc-pgaudit-role"> to <literal>auditor</> and
+      grant <literal>SELECT</> and <literal>DELETE</> privileges on the
+      <literal>account</> table.  Any <literal>SELECT</> or
+      <literal>DELETE</> statements on <literal>account</> will now be
+      logged:
+      </para>
+      
+      <programlisting>
+set pg_audit.role = 'auditor';
+
+grant select, delete
+   on public.account
+   to auditor;
+      </programlisting>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example object audit logging is used to illustrate how a
+        granular approach may be taken towards logging of <literal>SELECT</>
+        and <literal>DML</> statements.  Note that logging on the
+        <literal>account</> table is controlled by column-level permissions,
+        while logging on <literal>account_role_map</> is table-level.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+set pg_audit.role = 'auditor';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to auditor;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to auditor;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to auditor;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is compliant CSV format only if the log line prefix portion
+          of each log entry is removed.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</> - <literal>SESSION</> or
+            <literal>OBJECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT_ID</> - Unique statement ID for this session.
+            Each statement ID represents a backend call.  Statement IDs are
+            sequental even if some statements are not logged.  There may be
+            multiple entries for a statement ID when more than one relation
+            is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>SUBSTATEMENT_ID</> - Sequential ID for each
+            substatement within the main statement.  For example, calling
+            a function from a query.  Substatement IDs are continuous
+            even if some substatements are not logged.  There may be multiple
+            entries for a substatement ID when more than one relation is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</> - e.g. (<literal>READ</>,
+            <literal>ROLE</>) (see <xref linkend="guc-pgaudit-log">).
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</> - e.g. <literal>ALTER TABLE</>,
+            <literal>SELECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</> - <literal>TABLE</>,
+            <literal>INDEX</>, <literal>VIEW</>, etc.
+            Available for <literal>SELECT</>, <literal>DML</> and most
+            <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</> - The fully-qualified object name
+            (e.g. public.account).  Available for <literal>SELECT</>,
+            <literal>DML</> and most <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</> - Statement executed on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+
+    <para>
+      Use <xref linkend="guc-log-line-prefix"> to add any other fields that
+      are needed to satisfy your audit log requirements.  A typical log line
+      prefix might be <literal>'%m %u %d: '</> which would provide the date/time,
+      user name, and database name for each audit log.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Caveats</title>
+    
+    <itemizedlist>
+      <listitem>
+        <para>
+          Object renames are logged under the name they were renamed to.
+          For example, renaming a table will produce the following result:
+        </para>
+
+        <programlisting>
+ALTER TABLE test RENAME TO test2;
+          
+AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,ALTER TABLE test RENAME TO test2
+        </programlisting>
+      </listitem>
+      
+      <listitem>
+        <para>
+          It is possible to have a command logged more than once.  For example,
+          when a table is created with a primary key specified at creation time
+          the index for the primary key will be logged independently and another
+          audit log will be made for the index under the create entry.  The
+          multiple entries will however be contained within one statement.
+        </para>
+      </listitem>
+
+      <listitem>
+        <para>
+          Autovacuum and Autoanalyze are not logged, nor are they intended to be.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#35Tatsuo Ishii
ishii@postgresql.org
In reply to: David Steele (#34)
Re: Auditing extension for PostgreSQL (Take 2)

Thank you for pointing that out!

Ironic that it was the commit directly after the one I was testing with
that broke the patch. It appears the end of the last CF is a very bad
time to be behind HEAD.

Fixed in attached v8 patch.

Thank you for your quick response.

BTW, in my understanding pg_audit allows to track a table access even
if it's used in a view. I think this is a nice feature and it would be
better explicitly stated in the document and the test case is better
included in the regression test.

Here is a sample session:

CREATE TABLE test2 (id INT);
CREATE VIEW vtest2 AS SELECT * FROM test2;
GRANT SELECT ON TABLE public.test2 TO auditor;
GRANT SELECT ON TABLE public.vtest2 TO auditor;
SELECT * FROM vtest2;
NOTICE: AUDIT: SESSION,1,1,READ,SELECT,,,SELECT * FROM vtest2;
NOTICE: AUDIT: OBJECT,1,1,READ,SELECT,VIEW,public.vtest2,SELECT * FROM vtest2;
NOTICE: AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,SELECT * FROM vtest2;

Best regards,
--
Tatsuo Ishii
SRA OSS, Inc. Japan
English: http://www.sraoss.co.jp/index_en.php
Japanese:http://www.sraoss.co.jp

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

#36Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#34)
Re: Auditing extension for PostgreSQL (Take 2)

On Wed, Apr 15, 2015 at 8:57 AM, David Steele <david@pgmasters.net> wrote:

On 4/14/15 7:13 PM, Tatsuo Ishii wrote:

This patch does not apply cleanly due to the moving of pgbench (patch
to filelist.sgml failed).

Thank you for pointing that out!

Ironic that it was the commit directly after the one I was testing with
that broke the patch. It appears the end of the last CF is a very bad
time to be behind HEAD.

Fixed in attached v8 patch.

Thank you for updating the patch!

I applied the patch and complied them successfully without WARNING.

I tested v8 patch with CURSOR case I mentioned before, and got
segmentation fault again.
Here are log messages in my environment,

=# select test();
LOG: server process (PID 29730) was terminated by signal 11:
Segmentation fault
DETAIL: Failed process was running: select test();
LOG: terminating any other active server processes
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back
the current transaction and exit, because another server process
exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and
repeat your command.
FATAL: the database system is in recovery mode

I hope that these messages helps you to address this problem.
I will also try to address this.

Regards,

-------
Sawada Masahiko

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

#37Sawada Masahiko
sawada.mshk@gmail.com
In reply to: Sawada Masahiko (#36)
Re: Auditing extension for PostgreSQL (Take 2)

On Wed, Apr 15, 2015 at 10:52 AM, Sawada Masahiko <sawada.mshk@gmail.com> wrote:

On Wed, Apr 15, 2015 at 8:57 AM, David Steele <david@pgmasters.net> wrote:

On 4/14/15 7:13 PM, Tatsuo Ishii wrote:

This patch does not apply cleanly due to the moving of pgbench (patch
to filelist.sgml failed).

Thank you for pointing that out!

Ironic that it was the commit directly after the one I was testing with
that broke the patch. It appears the end of the last CF is a very bad
time to be behind HEAD.

Fixed in attached v8 patch.

Thank you for updating the patch!

I applied the patch and complied them successfully without WARNING.

I tested v8 patch with CURSOR case I mentioned before, and got
segmentation fault again.
Here are log messages in my environment,

=# select test();
LOG: server process (PID 29730) was terminated by signal 11:
Segmentation fault
DETAIL: Failed process was running: select test();
LOG: terminating any other active server processes
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back
the current transaction and exit, because another server process
exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and
repeat your command.
FATAL: the database system is in recovery mode

I hope that these messages helps you to address this problem.
I will also try to address this.

Regards,

-------
Sawada Masahiko

I will also try to address this.

I investigated this problem and inform you my result here.

When we execute test() function I mentioned, the three stack items in
total are stored into auditEventStack.
The two of them are freed by stack_pop() -> stack_free() (i.g,
stack_free() is called by stack_pop()).
One of them is freed by PortalDrop() -> .. ->
MemoryContextDeleteChildren() -> ... -> stack_free().
And it is freed at the same time as deleting pg_audit memory context,
and stack will be completely empty.

But after freeing all items, finish_xact_command() function could call
PortalDrop(), so ExecutorEnd() function could be called again.
pg_audit has ExecutorEnd_hook, so postgres tries to free that item.. SEGV.

In my environment, the following change fixes it.

--- pg_audit.c.org    2015-04-15 14:21:07.541866525 +0900
+++ pg_audit.c    2015-04-15 11:36:53.758877339 +0900
@@ -1291,7 +1291,7 @@
         standard_ExecutorEnd(queryDesc);
     /* Pop the audit event off the stack */
-    if (!internalStatement)
+    if (!internalStatement && auditEventStack != NULL)
     {
         stack_pop(auditEventStack->stackId);
     }

It might be good idea to add Assert() at before calling stack_pop().

I'm not sure this change is exactly correct, but I hope this
information helps you.

Regards,

-------
Sawada Masahiko

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

#38David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#37)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/15/15 11:30 AM, Sawada Masahiko wrote:

On Wed, Apr 15, 2015 at 10:52 AM, Sawada Masahiko <sawada.mshk@gmail.com> wrote:

I tested v8 patch with CURSOR case I mentioned before, and got
segmentation fault again.
Here are log messages in my environment,

=# select test();
LOG: server process (PID 29730) was terminated by signal 11:
Segmentation fault
DETAIL: Failed process was running: select test();
LOG: terminating any other active server processes
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back
the current transaction and exit, because another server process
exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and
repeat your command.
FATAL: the database system is in recovery mode

I investigated this problem and inform you my result here.

When we execute test() function I mentioned, the three stack items in
total are stored into auditEventStack.
The two of them are freed by stack_pop() -> stack_free() (i.g,
stack_free() is called by stack_pop()).
One of them is freed by PortalDrop() -> .. ->
MemoryContextDeleteChildren() -> ... -> stack_free().
And it is freed at the same time as deleting pg_audit memory context,
and stack will be completely empty.

But after freeing all items, finish_xact_command() function could call
PortalDrop(), so ExecutorEnd() function could be called again.
pg_audit has ExecutorEnd_hook, so postgres tries to free that item.. SEGV.

In my environment, the following change fixes it.

--- pg_audit.c.org    2015-04-15 14:21:07.541866525 +0900
+++ pg_audit.c    2015-04-15 11:36:53.758877339 +0900
@@ -1291,7 +1291,7 @@
standard_ExecutorEnd(queryDesc);
/* Pop the audit event off the stack */
-    if (!internalStatement)
+    if (!internalStatement && auditEventStack != NULL)
{
stack_pop(auditEventStack->stackId);
}

It might be good idea to add Assert() at before calling stack_pop().

I'm not sure this change is exactly correct, but I hope this
information helps you.

I appreciate you taking the time to look - this is the same conclusion I
came to. I also hardened another area that I thought might be vulnerable.

I've seen this problem with explicit cursors before (and fixed it in
another place a while ago). The memory context that is current in
ExecutorStart is freed before ExecutorEnd is called only in this case as
far as I can tell. I'm not sure this is very consistent behavior.

I have attached patch v9 which fixes this issue as you suggested, but
I'm not completely satisfied with it. It seems like there could be an
unintentional pop from the stack in a case of deeper nesting. This
might not be possible but it's hard to disprove.

I'll think about it some more, but meanwhile this patch addresses the
present issue.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v9.patchtext/plain; charset=UTF-8; name=pg_audit-v9.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index d63e441..ed9cf6a 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -28,6 +28,7 @@ SUBDIRS = \
 		oid2name	\
 		pageinspect	\
 		passwordcheck	\
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/.gitignore b/contrib/pg_audit/.gitignore
new file mode 100644
index 0000000..a5267cf
--- /dev/null
+++ b/contrib/pg_audit/.gitignore
@@ -0,0 +1,5 @@
+log/
+results/
+tmp_check/
+regression.diffs
+regression.out
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..7b36011
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,21 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+REGRESS = pg_audit
+REGRESS_OPTS = --temp-config=$(top_srcdir)/contrib/pg_audit/pg_audit.conf
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/expected/pg_audit-deparse.out b/contrib/pg_audit/expected/pg_audit-deparse.out
new file mode 100644
index 0000000..e72699c
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit-deparse.out
@@ -0,0 +1,921 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,VIEW,public.vw_test3,"SELECT *
+  FROM vw_test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,6,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,6,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+                  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,4,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,5,1,DDL,CREATE SCHEMA,SCHEMA,test,CREATE SCHEMA test;
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,2,DDL,CREATE TABLE AS,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,9,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,10,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,10,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,11,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+NOTICE:  AUDIT: SESSION,12,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,13,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,14,1,MISC,COMMIT,,,COMMIT;
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,15,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+NOTICE:  AUDIT: SESSION,16,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,17,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,17,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,18,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,18,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,18,2,DDL,CREATE TABLE,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,19,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,20,1,ROLE,GRANT,TABLE,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,21,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,22,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,23,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,24,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,24,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,25,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,25,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,26,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,27,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,28,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,29,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,29,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,29,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,29,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,29,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,30,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,30,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,30,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,31,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,32,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+NOTICE:  AUDIT: SESSION,32,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,33,1,DDL,ALTER TABLE,TABLE,public.test2,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,34,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,37,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,37,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,37,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+NOTICE:  AUDIT: SESSION,38,1,DDL,CREATE TABLE,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,38,2,DDL,CREATE TABLE,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,38,3,DDL,CREATE SCHEMA,SCHEMA,foo,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,38,3,DDL,CREATE SCHEMA,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,38,3,DDL,CREATE SCHEMA,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,39,1,DDL,CREATE FUNCTION,FUNCTION,"public.int_add(integer,integer)","CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,40,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,40,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,41,1,DDL,CREATE AGGREGATE,AGGREGATE,public.sum_test(integer),"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,42,1,DDL,ALTER AGGREGATE,AGGREGATE,public.sum_test2(integer),ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,43,1,DDL,CREATE CONVERSION,CONVERSION,public.conversion_test,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,44,1,DDL,ALTER CONVERSION,CONVERSION,public.conversion_test2,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,45,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,46,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,47,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+NOTICE:  AUDIT: SESSION,48,1,DDL,CREATE TABLE,TABLE,public.hoge,"CREATE TABLE hoge
+(
+	id int
+);"
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+    cur1 cursor for select * from hoge;
+    tmp int;
+BEGIN
+    OPEN cur1;
+    FETCH cur1 into tmp;
+    RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+NOTICE:  AUDIT: SESSION,49,1,DDL,CREATE FUNCTION,FUNCTION,public.test(),"CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+    cur1 cursor for select * from hoge;
+    tmp int;
+BEGIN
+    OPEN cur1;
+    FETCH cur1 into tmp;
+    RETURN tmp;
+END $$
+LANGUAGE plpgsql ;"
+SELECT test();
+NOTICE:  AUDIT: SESSION,50,1,READ,SELECT,,,SELECT test();
+NOTICE:  AUDIT: SESSION,50,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT test();
+NOTICE:  AUDIT: SESSION,50,3,READ,SELECT,TABLE,public.hoge,select * from hoge
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+     
+(1 row)
+
diff --git a/contrib/pg_audit/expected/pg_audit.out b/contrib/pg_audit/expected/pg_audit.out
new file mode 100644
index 0000000..467d848
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit.out
@@ -0,0 +1,904 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,VIEW,public.vw_test3,"SELECT *
+  FROM vw_test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,6,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,6,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+                  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,4,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,5,1,DDL,CREATE SCHEMA,,,CREATE SCHEMA test;
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,2,DDL,CREATE TABLE AS,,,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,9,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,10,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,10,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,11,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+NOTICE:  AUDIT: SESSION,12,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,13,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,14,1,MISC,COMMIT,,,COMMIT;
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,15,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+NOTICE:  AUDIT: SESSION,16,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,17,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,17,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,18,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,18,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,19,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,20,1,ROLE,GRANT,,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,21,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,22,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,23,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,24,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,24,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,25,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,25,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,26,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,27,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,28,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,29,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,29,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,29,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,29,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,29,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+		     VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+		     VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,30,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,30,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,30,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,31,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,32,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,33,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,34,1,DDL,ALTER TABLE,INDEX,public.test_pkey,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,37,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,37,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,37,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+NOTICE:  AUDIT: SESSION,38,1,DDL,CREATE TABLE,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,38,2,DDL,CREATE TABLE,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,38,3,DDL,CREATE SCHEMA,,,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,39,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,40,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,40,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,41,1,DDL,CREATE AGGREGATE,,,"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,42,1,DDL,ALTER AGGREGATE,,,ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,43,1,DDL,CREATE CONVERSION,,,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,44,1,DDL,ALTER CONVERSION,,,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,45,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,46,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,47,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+NOTICE:  AUDIT: SESSION,48,1,DDL,CREATE TABLE,TABLE,public.hoge,"CREATE TABLE hoge
+(
+	id int
+);"
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+    cur1 cursor for select * from hoge;
+    tmp int;
+BEGIN
+    OPEN cur1;
+    FETCH cur1 into tmp;
+    RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+NOTICE:  AUDIT: SESSION,49,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+    cur1 cursor for select * from hoge;
+    tmp int;
+BEGIN
+    OPEN cur1;
+    FETCH cur1 into tmp;
+    RETURN tmp;
+END $$
+LANGUAGE plpgsql ;"
+SELECT test();
+NOTICE:  AUDIT: SESSION,50,1,READ,SELECT,,,SELECT test();
+NOTICE:  AUDIT: SESSION,50,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT test();
+NOTICE:  AUDIT: SESSION,50,3,READ,SELECT,TABLE,public.hoge,select * from hoge
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+     
+(1 row)
+
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..9dd2d7c
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1790 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pg_audit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pg_audit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * auditLogRelation controls whether all relations are logged for READ and
+ * WRITE classes during session logging.
+ */
+bool auditLogRelation = false;
+
+/*
+ * auditLogNotice raises a notice as well as logging via the standard facility.
+ * This is primarily for the benefit of testing.
+ */
+bool auditLogNotice = false;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pg_audit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_PARAMETER		"PARAMETER"
+#define CLASS_READ			"READ"
+#define CLASS_ROLE			"ROLE"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* Function execution */
+	LOG_PARAMETER = (1 << 4),
+
+	/* SELECT */
+	LOG_READ = (1 << 5),
+
+	/* GRANT, REVOKE, CREATE/ALTER/DROP ROLE */
+	LOG_ROLE = (1 << 6),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 7),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_ALTER_ROLE	"ALTER ROLE"
+#define COMMAND_DROP_ROLE	"CREATE ROLE"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	int64 stackId;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static int64 statementTotal = 0;
+static int64 substatementTotal = 0;
+static int64 stackTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack to the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Create the unique stackId - used to keep the stack sane when memory
+	 * contexts are freed unexpectedly.
+	 */
+	stackItem->stackId = ++stackTotal;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop(int64 stackId)
+{
+	/* Make sure what we want to delete is at the top of the stack */
+	if (auditEventStack != NULL && auditEventStack->stackId == stackId)
+	{
+		MemoryContextDelete(auditEventStack->contextAudit);
+	}
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent, classifies it, then logs it if permissions were granted
+ * via roles or if the statement belongs in a class that is being logged.
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	MemoryContext contextOld;
+	StringInfoData auditStr;
+
+	/* By default put everything in the MISC class. */
+	enum LogClass class = LOG_MISC;
+	const char *className = CLASS_MISC;
+
+	/* Classify the statement using log stmt level and the command tag */
+	switch (stackItem->auditEvent.logStmtLevel)
+	{
+		/* All mods go in WRITE class */
+		case LOGSTMT_MOD:
+			className = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		/* Separate ROLE from other DDL statements */
+		case LOGSTMT_DDL:
+			/* Identify role statements */
+			if (stackItem->auditEvent.commandTag == T_GrantStmt ||
+				stackItem->auditEvent.commandTag == T_GrantRoleStmt ||
+				stackItem->auditEvent.commandTag == T_CreateRoleStmt ||
+				(stackItem->auditEvent.commandTag == T_RenameStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_ALTER_ROLE) == 0) ||
+				(stackItem->auditEvent.commandTag == T_DropStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_DROP_ROLE) == 0) ||
+				stackItem->auditEvent.commandTag == T_DropRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleSetStmt)
+			{
+				className = CLASS_ROLE;
+				class = LOG_ROLE;
+			}
+			/* Else log as DDL */
+			else
+			{
+				className = CLASS_DDL;
+				class = LOG_DDL;
+			}
+
+		/* Figure out the rest */
+		case LOGSTMT_ALL:
+			switch (stackItem->auditEvent.commandTag)
+			{
+				/* READ statements */
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					className = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				/* Reindex is DDL (because cluster is DDL) */
+				case T_ReindexStmt:
+					className = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				/* FUNCTION statements */
+				case T_DoStmt:
+					className = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * Only log the statement if:
+	 *
+	 * 1. If permissions were granted via roles
+	 * 2. The statement belongs to a class that is being logged
+	 */
+	if (!stackItem->auditEvent.granted && !(auditLogBitmap & class))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogBitmap & LOG_PARAMETER &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	elog(auditLogNotice ? NOTICE : LOG,
+		 "AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			className, auditStr.data);
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+audit_on_acl(Datum aclDatum,
+			 Oid auditOid,
+			 AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+audit_on_relation(Oid relOid,
+				  Oid auditOid,
+				  AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = audit_on_acl(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+audit_on_attribute(Oid relOid,
+				   AttrNumber attNum,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = audit_on_acl(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+audit_on_any_attribute(Oid relOid,
+					   Oid auditOid,
+					   Bitmapset *attributeSet,
+					   AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (audit_on_attribute(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			audit_on_attribute(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/*
+		 * If this is the first rte then session log unless auditLogRelation
+		 * is set.
+		 */
+		if (first && !auditLogRelation)
+		{
+			auditEventStack->auditEvent.objectName = "";
+			auditEventStack->auditEvent.objectType = "";
+
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms = (ACL_SELECT | ACL_UPDATE | ACL_INSERT) &
+								 rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (audit_on_relation(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->selectedCols,
+											   ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEventStack->auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEventStack->auditEvent.granted =
+							audit_on_any_attribute(relOid, auditOid,
+												   rte->modifiedCols,
+												   auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Do relation level logging if a grant was found */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		/* Do relation level logging if auditLogRelation is set */
+		if (auditLogRelation)
+		{
+			auditEventStack->auditEvent.logged = false;
+			auditEventStack->auditEvent.granted = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop(stackItem->stackId);
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute */
+		case OAT_FUNCTION_EXECUTE:
+			if (auditLogBitmap & LOG_FUNCTION)
+				log_function_execute(objectId);
+			break;
+
+		/* Log create */
+		case OAT_POST_CREATE:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter */
+		case OAT_POST_ALTER:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop */
+		case OAT_DROP:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessDrop *drop = arg;
+
+				if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+static ExecutorEnd_hook_type next_ExecutorEnd_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pg_audit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ExecutorEnd to pop statement audit event off the stack.
+ */
+static void
+pg_audit_ExecutorEnd_hook(QueryDesc *queryDesc)
+{
+	/* Call the next hook or standard function */
+	if (next_ExecutorEnd_hook)
+		next_ExecutorEnd_hook(queryDesc);
+	else
+		standard_ExecutorEnd(queryDesc);
+
+	/* Pop the audit event off the stack */
+	if (!internalStatement && auditEventStack)
+	{
+		stack_pop(auditEventStack->stackId);
+	}
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pg_audit_ProcessUtility_hook(Node *parsetree,
+							 const char *queryString,
+							 ProcessUtilityContext context,
+							 ParamListInfo params,
+							 DestReceiver *dest,
+							 char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+	int64 stackId;
+
+	/* Allocate the audit event */
+	if (!IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackId = stackItem->stackId;
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.objectName = "";
+		stackItem->auditEvent.objectType = "";
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap != 0 &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/*
+	 * Process the audit event if there is one.  Also check that this event was
+	 * not popped off the stack by a memory context being freed elsewhere.
+	 */
+	if (stackItem != NULL && auditEventStack != NULL &&
+		auditEventStack->stackId == stackId)
+	{
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !auditEventStack->auditEvent.logged &&
+			!IsAbortedTransactionBlockState())
+			log_audit_event(auditEventStack);
+
+		stack_pop(stackId);
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pg_audit_object_access_hook(ObjectAccessType access,
+							Oid classId,
+							Oid objectId,
+							int subId,
+							void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&
+		auditLogBitmap & (LOG_DDL | LOG_FUNCTION) && auditEventStack)
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+#ifdef DEPARSE
+	/* Continue only if session DDL logging is enabled */
+	if (auditLogBitmap & LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_ddl_command_end()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		auditEventStack->auditEvent.logStmtLevel =
+			GetCommandLogLevel(eventData->parsetree);
+		auditEventStack->auditEvent.commandTag =
+			nodeTag(eventData->parsetree);
+		auditEventStack->auditEvent.command =
+			CreateCommandTag(eventData->parsetree);
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT UPPER(object_type), identity\n"
+				"  FROM pg_event_trigger_ddl_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply object name and type for audit event */
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 1);
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 2);
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+#endif
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_sql_drop()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT classid, objid, objsubid, UPPER(object_type),\n"
+				"       schema_name, object_name, object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+			char *schemaName;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 4);
+			schemaName = SPI_getvalue(spiTuple, spiTupDesc, 5);
+
+			if (!(pg_strcasecmp(auditEventStack->auditEvent.objectType,
+							"TYPE") == 0 ||
+				  pg_strcasecmp(schemaName, "pg_toast") == 0))
+			{
+				auditEventStack->auditEvent.objectName =
+						SPI_getvalue(spiTuple, spiTupDesc, 7);
+
+				log_audit_event(auditEventStack);
+			}
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pg_audit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then the token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_PARAMETER) == 0)
+			class = LOG_PARAMETER;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_ROLE) == 0)
+			class = LOG_ROLE;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pg_audit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pg_audit_log from extra (ignoring newval, which has already been
+ * converted to a bitmap above). Note that extra may not be set if the
+ * assignment is to be suppressed.
+ */
+static void
+assign_pg_audit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * Defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable object auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * Controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable session auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pg_audit_log,
+							   assign_pg_audit_log,
+							   NULL);
+
+	/*
+	 * pg_audit.log_relation = on
+	 *
+	 * Controls whether relations get separate log entries during session
+	 * logging of READ and WRITE classes.  This works as if all relations in the
+	 * database had been added to the audit role and provides a shortcut when
+	 * really detailed logging of absolutely every relation is required.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_relation",
+							 "Enable session relation logging",
+							 NULL,
+							 &auditLogRelation,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+
+	/*
+	 * pg_audit.log_notice = on
+	 *
+	 * Audit logging is raised as notices that can be seen on the client.  This is
+	 * intended for testing purposes.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_notice",
+							 "Raise a notice when logging",
+							 NULL,
+							 &auditLogNotice,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pg_audit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook;
+
+	next_ExecutorEnd_hook = ExecutorEnd_hook;
+	ExecutorEnd_hook = pg_audit_ExecutorEnd_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pg_audit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pg_audit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.conf b/contrib/pg_audit/pg_audit.conf
new file mode 100644
index 0000000..e9f4a22
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.conf
@@ -0,0 +1 @@
+shared_preload_libraries = pg_audit
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/sql/pg_audit.sql b/contrib/pg_audit/sql/pg_audit.sql
new file mode 100644
index 0000000..9b11d1a
--- /dev/null
+++ b/contrib/pg_audit/sql/pg_audit.sql
@@ -0,0 +1,556 @@
+-- Load pg_audit module
+create extension pg_audit;
+
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+
+--
+-- Create auditor role
+CREATE ROLE auditor;
+
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+DROP TABLE test;
+
+--
+-- Create second test user
+\connect contrib_regression super
+
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+
+SELECT *
+  FROM test3, test2;
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+	               RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+	               RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+
+\connect contrib_regression user2
+
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+                  VALUES (1);
+
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+                  VALUES ('test');
+
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+
+--
+-- Create test schema
+CREATE SCHEMA test;
+
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+1	user1	HASH2	yada, yada
+\.
+
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+
+EXECUTE pgclassstmt (1);
+DEALLOCATE pgclassstmt;
+
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+
+FETCH NEXT FROM ctest;
+CLOSE ctest;
+COMMIT;
+
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+                      VALUES ($1);
+EXECUTE pgclassstmt (1);
+
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+
+--
+-- Check that analyze is logged
+ANALYZE test;
+
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+
+SELECT *
+  FROM test;
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+
+SELECT 1,
+	   current_user;
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+
+explain select 1;
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+INSERT INTO TEST (id)
+		  VALUES (2);
+INSERT INTO TEST (id)
+		  VALUES (3);
+
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+		     VALUES (result.id + 100);
+	END LOOP;
+END $$;
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+
+ALTER TABLE public.test
+	RENAME TO test2;
+
+ALTER TABLE public.test2
+	SET SCHEMA test;
+
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+
+ALTER TABLE test.test2
+	DROP COLUMN description;
+
+DROP TABLE test.test2;
+
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+
+SELECT int_add(1, 1);
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+    cur1 cursor for select * from hoge;
+    tmp int;
+BEGIN
+    OPEN cur1;
+    FETCH cur1 into tmp;
+    RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+
+SELECT test();
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 5773095..20a4e62 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index ab935a6..7f39f42 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -125,6 +125,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
 <!ENTITY pgcrypto        SYSTEM "pgcrypto.sgml">
 <!ENTITY pgfreespacemap  SYSTEM "pgfreespacemap.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..ba04e7b
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,614 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> extenstion provides detailed session
+    and/or object audit logging via the standard logging facility.  The goal
+    is to provide the tools needed to produce audit logs required to pass any
+    goverment, financial, or ISO certification audit.
+  </para>
+
+  <para>
+    An audit is an official inspection of an individual's or organization's
+    accounts, typically by an independent body.  The information gathered by
+    <filename>pg_audit</filename> is properly called an audit trail or audit
+    log.  The term audit log is used in this documentation.
+  </para>
+
+  <sect2>
+    <title>Why <literal>pg_audit</>?</title>
+  
+    <para>
+      Basic statement logging can be provided by the standard logging facility
+      using <literal>log_statements = all</>.  This is acceptable for monitoring
+      and other usages but does not provide the level of detail generally
+      required for an audit.  It is not enough to have a list of all the
+      operations performed against the database. It must also be possible to
+      find particular statements that are of interest to an auditor.
+    </para>
+
+    <para>
+      For example, an auditor may want to verify that a particular table was
+      created inside a documented maintence window.  This might seem like a
+      simple job for grep, but what if you are presented with something like
+      this (intentionally obfuscated) example:
+    </para>
+
+    <programlisting>
+DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+    
+    <para>
+      Standard logging will give you this:
+    </para>
+
+    <programlisting>
+LOG:  statement: DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+    
+    <para>
+      It appears that finding the table of interest may require some knowledge
+      of the code in cases where tables are created dynamically.  This is not
+      ideal since it would be preferrable to just search on the table name.
+      This is where <literal>pg_audit</> comes in.  For the same input,
+      it will produce this output in the log:
+    </para>
+
+    <programlisting>
+AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;"
+AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.important_table,CREATE TABLE important_table (id INT)
+    </programlisting>
+
+    <para>
+      Not only is the <literal>DO</> block logged, but substatement 2 contains
+      the full text of the <literal>CREATE TABLE</> with the statement type,
+      object type, and full-qualified name to make searches easy.
+    </para>
+
+    <para>
+      When logging <literal>SELECT</> and <literal>DML</> statements,
+      <literal>pg_audit</> can be configured to log a separate entry for each
+      relation referenced in a statement.  No parsing is required to find all
+      statements that touch a particular table.  In fact, the goal is that the
+      statement text is provided primarily for deep forensics and should not be
+      the directly required for any search.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Usage Considerations</title>
+    
+    <para>
+      Depending on settings, it is possible for <literal>pg_audit</literal> to
+      generate an enormous volume of logging.  Be careful to determine
+      exactly what needs to be audit logged in your environment to avoid
+      logging too much.
+    </para>
+    
+    <para>
+      For example, when working in an OLAP environment it would probably not be
+      wise to audit log inserts into a large fact table.  The size of the log
+      file will likely be many times the actual data size of the inserts because
+      the log file is expressed as text.  Since logs are generally stored with
+      the OS this may lead to disk space being exhausted very
+      quickly.  In cases where it is not possible to limit audit logging to
+      certain tables, be sure to assess the performance impact while testing
+      and allocate plenty of space on the log volume.  This may also be true for
+      OLTP environments.  Even if the insert volume is not as high, the
+      performance impact of audit logging may still noticeably affect latency.
+    </para>
+    
+    <para>
+      To limit the number of relations audit logged for <literal>SELECT</>
+      and <literal>DML</> statments, consider using object audit logging
+      (see <xref linkend="pgaudit-object-audit-logging">).  Object audit logging
+      allows selection of the relations to be logged allowing for reduction
+      of the overall log volume.  However, when new relations are added they
+      must be explicitly added to object audit logging.  A programmatic
+      solution where specified tables are excluded from logging and all others
+      are included may be a good option in this case.
+    </para>
+  </sect2>
+  
+  <sect2>
+    <title>Settings</title>
+    
+    <para>
+      Settings may be modified only by a superuser. Allowing normal users to
+      change their settings would defeat the point of an audit log.
+    </para>
+      
+    <para>
+      Settings can be specified globally (in
+      <filename>postgresql.conf</filename> or using
+      <literal>ALTER SYSTEM ... SET</>), at the database level (using
+      <literal>ALTER DATABASE ... SET</literal>), or at the role level (using
+      <literal>ALTER ROLE ... SET</literal>).  Note that settings are not
+      inherited through normal role inheritance and <literal>SET ROLE</> will
+      not alter a user's <literal>pg_audit</> settings.  This is a limitation
+      of the roles system and not inherent to <literal>pg_audit</>.
+    </para>
+      
+    <para>
+      The <literal>pg_audit</> extension must be loaded in
+      <xref linkend="guc-shared-preload-libraries">.  Otherwise, an error
+      will be raised at load time and no audit logging will occur.
+    </para>
+
+    <variablelist>
+      <varlistentry id="guc-pgaudit-log" xreflabel="pg_audit.log">
+        <term><varname>pg_audit.log</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies which classes of statements will be logged by session
+            audit logging.  Possible values are:
+          </para>
+
+          <itemizedlist>
+            <listitem>
+              <para>
+                <literal>READ</literal> - <literal>SELECT</literal> and
+                <literal>COPY</literal> when the source is a relation or a
+                query.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>WRITE</literal> - <literal>INSERT</literal>,
+                <literal>UPDATE</literal>, <literal>DELETE</literal>,
+                <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+                destination is a relation.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>FUNCTION</literal> - Function calls and
+                <literal>DO</literal> blocks.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>ROLE</literal> - Statements related to roles and
+                privileges: <literal>GRANT</literal>,
+                <literal>REVOKE</literal>,
+                <literal>CREATE/ALTER/DROP ROLE</literal>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>DDL</literal> - All <literal>DDL</> that is not included
+                in the <literal>ROLE</> class plus <literal>REINDEX</>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>PARAMETER</literal> - Parameters that were passed for the
+                statement.  Parameters immediately follow the statement text.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>MISC</literal> - Miscellaneous commands, e.g.
+                <literal>DISCARD</literal>, <literal>FETCH</literal>,
+                <literal>CHECKPOINT</literal>, <literal>VACUUM</literal>.
+              </para>
+            </listitem>
+          </itemizedlist>
+            
+          <para>
+            Multiple classes can be provided using a comma-separated list and
+            classes can be subtracted by prefacing the class with a
+            <literal>-</> sign (see <xref linkend="pgaudit-session-audit-logging">).
+            The default is <literal>none</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-notice" xreflabel="pg_audit.log_notice">
+        <term><varname>pg_audit.log_notice</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_notice</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that the audit log messages should be raised as
+            <literal>NOTICE</> instead of <literal>LOG</>.  The primary
+            advantage is that <literal>NOTICE</> messages can be exposed
+            through the user interface.  This setting is used for regression
+            testing and may also be useful to end users for testing.  It is not
+            intended to be used in a production environment as it will leak
+            which statements are being logged to the user. The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-relation" xreflabel="pg_audit.log_relation">
+        <term><varname>pg_audit.log_relation</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_relation</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies whether session audit logging should create a separate
+            log entry for each relation (<literal>TABLE</>, <literal>VIEW</>,
+            etc.) referenced in a <literal>SELECT</> or <literal>DML</>
+            statement.  This is a useful shortcut for exhaustive logging
+            without using object audit logging.  The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-role" xreflabel="pg_audit.role">
+        <term><varname>pg_audit.role</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.role</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies the master role to use for object audit logging.  Muliple
+            audit roles can be defined by granting them to the master role.
+            This allows multiple groups to be in charge of different aspects
+            of audit logging.  There is no default.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </sect2>
+
+  <sect2 id="pgaudit-session-audit-logging">
+    <title>Session Audit Logging</title>
+
+    <para>
+      Session audit logging provides detailed logs of all statements executed
+      by a user in the backend.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is enabled with the <xref linkend="guc-pgaudit-log">
+        setting.
+
+        Enable session logging for all <literal>DML</> and <literal>DDL</> and
+        log all relations in <literal>DML</> statements:
+          <programlisting>
+set pg_audit.log = 'write, ddl';
+set pg_audit.log_relation = on;
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except <literal>MISC</> and
+        raise audit log messages as <literal>NOTICE</>:
+          <programlisting>
+set pg_audit.log = 'all, -misc';
+set pg_audit.log_notice = on;
+          </programlisting>
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example session audit logging is used to for logging
+        <literal>DDL</> and <literal>SELECT</> statements.  Note that the
+        insert statement is not logged since the <literal>WRITE</> class
+        is not enabled
+      </para>
+
+      <para>
+        SQL:
+      </para>
+      <programlisting>
+set pg_audit.log = 'read, ddl';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+
+select *
+    from account;
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,2,1,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2 id="pgaudit-object-audit-logging">
+    <title>Object Auditing</title>
+
+    <para>
+      Object audit logging logs statements that affect a particular relation.
+      Only <literal>SELECT</>, <literal>INSERT</>, <literal>UPDATE</> and
+      <literal>DELETE</> commands are supported.  <literal>TRUNCATE</> is not
+      included because there is no specific privilege for it.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level audit logging is implemented via the roles system.  The
+        <xref linkend="guc-pgaudit-role"> setting defines the role that
+        will be used for audit logging.  A relation (<literal>TABLE</>,
+        <literal>VIEW</>, etc.) will be audit logged when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.  This allows you to effectively have multiple audit roles
+        even though there is a single master role in any context.
+      </para>
+
+      <para>
+      Set <xref linkend="guc-pgaudit-role"> to <literal>auditor</> and
+      grant <literal>SELECT</> and <literal>DELETE</> privileges on the
+      <literal>account</> table.  Any <literal>SELECT</> or
+      <literal>DELETE</> statements on <literal>account</> will now be
+      logged:
+      </para>
+      
+      <programlisting>
+set pg_audit.role = 'auditor';
+
+grant select, delete
+   on public.account
+   to auditor;
+      </programlisting>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example object audit logging is used to illustrate how a
+        granular approach may be taken towards logging of <literal>SELECT</>
+        and <literal>DML</> statements.  Note that logging on the
+        <literal>account</> table is controlled by column-level permissions,
+        while logging on <literal>account_role_map</> is table-level.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+set pg_audit.role = 'auditor';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to auditor;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to auditor;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to auditor;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is compliant CSV format only if the log line prefix portion
+          of each log entry is removed.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</> - <literal>SESSION</> or
+            <literal>OBJECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT_ID</> - Unique statement ID for this session.
+            Each statement ID represents a backend call.  Statement IDs are
+            sequental even if some statements are not logged.  There may be
+            multiple entries for a statement ID when more than one relation
+            is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>SUBSTATEMENT_ID</> - Sequential ID for each
+            substatement within the main statement.  For example, calling
+            a function from a query.  Substatement IDs are continuous
+            even if some substatements are not logged.  There may be multiple
+            entries for a substatement ID when more than one relation is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</> - e.g. (<literal>READ</>,
+            <literal>ROLE</>) (see <xref linkend="guc-pgaudit-log">).
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</> - e.g. <literal>ALTER TABLE</>,
+            <literal>SELECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</> - <literal>TABLE</>,
+            <literal>INDEX</>, <literal>VIEW</>, etc.
+            Available for <literal>SELECT</>, <literal>DML</> and most
+            <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</> - The fully-qualified object name
+            (e.g. public.account).  Available for <literal>SELECT</>,
+            <literal>DML</> and most <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</> - Statement executed on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+
+    <para>
+      Use <xref linkend="guc-log-line-prefix"> to add any other fields that
+      are needed to satisfy your audit log requirements.  A typical log line
+      prefix might be <literal>'%m %u %d: '</> which would provide the date/time,
+      user name, and database name for each audit log.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Caveats</title>
+    
+    <itemizedlist>
+      <listitem>
+        <para>
+          Object renames are logged under the name they were renamed to.
+          For example, renaming a table will produce the following result:
+        </para>
+
+        <programlisting>
+ALTER TABLE test RENAME TO test2;
+          
+AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,ALTER TABLE test RENAME TO test2
+        </programlisting>
+      </listitem>
+      
+      <listitem>
+        <para>
+          It is possible to have a command logged more than once.  For example,
+          when a table is created with a primary key specified at creation time
+          the index for the primary key will be logged independently and another
+          audit log will be made for the index under the create entry.  The
+          multiple entries will however be contained within one statement ID.
+        </para>
+      </listitem>
+
+      <listitem>
+        <para>
+          Autovacuum and Autoanalyze are not logged, nor are they intended to be.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#39David Steele
david@pgmasters.net
In reply to: Tatsuo Ishii (#35)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/14/15 8:37 PM, Tatsuo Ishii wrote:

BTW, in my understanding pg_audit allows to track a table access even
if it's used in a view. I think this is a nice feature and it would be
better explicitly stated in the document and the test case is better
included in the regression test.

Here is a sample session:

CREATE TABLE test2 (id INT);
CREATE VIEW vtest2 AS SELECT * FROM test2;
GRANT SELECT ON TABLE public.test2 TO auditor;
GRANT SELECT ON TABLE public.vtest2 TO auditor;
SELECT * FROM vtest2;
NOTICE: AUDIT: SESSION,1,1,READ,SELECT,,,SELECT * FROM vtest2;
NOTICE: AUDIT: OBJECT,1,1,READ,SELECT,VIEW,public.vtest2,SELECT * FROM vtest2;
NOTICE: AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,SELECT * FROM vtest2;

That's the idea! In the documentation I throw around the word
"relation" pretty liberally, but you are right that some clarification
would be helpful.

I have added a few parenthetical statements to the docs that should make
them clearer. I also took your suggestion and added a view regression test.

Both are in patch v9 which I attached to my previous email on this thread.

Thank you for taking the time to have a look.

--
- David Steele
david@pgmasters.net

#40Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#38)
Re: Auditing extension for PostgreSQL (Take 2)

On Thu, Apr 16, 2015 at 2:34 AM, David Steele <david@pgmasters.net> wrote:

On 4/15/15 11:30 AM, Sawada Masahiko wrote:

On Wed, Apr 15, 2015 at 10:52 AM, Sawada Masahiko <sawada.mshk@gmail.com> wrote:

I tested v8 patch with CURSOR case I mentioned before, and got
segmentation fault again.
Here are log messages in my environment,

=# select test();
LOG: server process (PID 29730) was terminated by signal 11:
Segmentation fault
DETAIL: Failed process was running: select test();
LOG: terminating any other active server processes
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back
the current transaction and exit, because another server process
exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and
repeat your command.
FATAL: the database system is in recovery mode

I investigated this problem and inform you my result here.

When we execute test() function I mentioned, the three stack items in
total are stored into auditEventStack.
The two of them are freed by stack_pop() -> stack_free() (i.g,
stack_free() is called by stack_pop()).
One of them is freed by PortalDrop() -> .. ->
MemoryContextDeleteChildren() -> ... -> stack_free().
And it is freed at the same time as deleting pg_audit memory context,
and stack will be completely empty.

But after freeing all items, finish_xact_command() function could call
PortalDrop(), so ExecutorEnd() function could be called again.
pg_audit has ExecutorEnd_hook, so postgres tries to free that item.. SEGV.

In my environment, the following change fixes it.

--- pg_audit.c.org    2015-04-15 14:21:07.541866525 +0900
+++ pg_audit.c    2015-04-15 11:36:53.758877339 +0900
@@ -1291,7 +1291,7 @@
standard_ExecutorEnd(queryDesc);
/* Pop the audit event off the stack */
-    if (!internalStatement)
+    if (!internalStatement && auditEventStack != NULL)
{
stack_pop(auditEventStack->stackId);
}

It might be good idea to add Assert() at before calling stack_pop().

I'm not sure this change is exactly correct, but I hope this
information helps you.

I appreciate you taking the time to look - this is the same conclusion I
came to. I also hardened another area that I thought might be vulnerable.

I've seen this problem with explicit cursors before (and fixed it in
another place a while ago). The memory context that is current in
ExecutorStart is freed before ExecutorEnd is called only in this case as
far as I can tell. I'm not sure this is very consistent behavior.

I have attached patch v9 which fixes this issue as you suggested, but
I'm not completely satisfied with it. It seems like there could be an
unintentional pop from the stack in a case of deeper nesting. This
might not be possible but it's hard to disprove.

I'll think about it some more, but meanwhile this patch addresses the
present issue.

Thank you for updating the patch.

One question about regarding since v7 (or later) patch is;
What is the different between OBJECT logging and SESSION logging?

I used v9 patch with "pg_audit.log_relation = on", and got quite
similar but different log as follows.

=# select * from hoge, bar where hoge.col = bar.col;
NOTICE: AUDIT: OBJECT,8,1,READ,SELECT,TABLE,public.hoge,"select *
from hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.hoge,"select *
from hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: OBJECT,8,1,READ,SELECT,TABLE,public.bar,"select * from
hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.bar,"select *
from hoge, bar where hoge.col = bar.col;"

The behaviour of SESSION log is similar to OBJECT log now, and SESSION
log per session (i.g, pg_audit.log_relation = off) is also similar to
'log_statement = all'. (enhancing log_statement might be enough)
So I couldn't understand needs of SESSION log.

Regards,

-------
Sawada Masahiko

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

#41David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#40)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/20/15 4:40 AM, Sawada Masahiko wrote:

Thank you for updating the patch.

One question about regarding since v7 (or later) patch is;
What is the different between OBJECT logging and SESSION logging?

In brief, session logging can log anything that happens in a user
session while object logging only targets DML and SELECT on selected
relations.

The pg_audit.log_relation setting is intended to mimic the prior
behavior of pg_audit before object logging was added.

In general, I would not expect pg_audit.log = 'read, write' to be used
with pg_audit.role. In other words, session logging of DML and SELECT
should probably not be turned on at the same time as object logging is
in use. Object logging is intended to be a fine-grained version of
pg_audit.log = 'read, write' (one or both).

I used v9 patch with "pg_audit.log_relation = on", and got quite
similar but different log as follows.

=# select * from hoge, bar where hoge.col = bar.col;
NOTICE: AUDIT: OBJECT,8,1,READ,SELECT,TABLE,public.hoge,"select *
from hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.hoge,"select *
from hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: OBJECT,8,1,READ,SELECT,TABLE,public.bar,"select * from
hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.bar,"select *
from hoge, bar where hoge.col = bar.col;"

The behaviour of SESSION log is similar to OBJECT log now, and SESSION
log per session (i.g, pg_audit.log_relation = off) is also similar to
'log_statement = all'. (enhancing log_statement might be enough)
So I couldn't understand needs of SESSION log.

Session logging is quite different from 'log_statement = all'. See:

/messages/by-id/552323B2.8060708@pgmasters.net

and/or the "Why pg_audit?" section of the pg_audit documentation.

I agree that it may make sense in the future to merge session logging
into log_statements, but for now it does provide important additional
functionality for creating audit logs.

Regards,
--
- David Steele
david@pgmasters.net

#42Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#41)
Re: Auditing extension for PostgreSQL (Take 2)

On Mon, Apr 20, 2015 at 10:17 PM, David Steele <david@pgmasters.net> wrote:

On 4/20/15 4:40 AM, Sawada Masahiko wrote:

Thank you for updating the patch.

One question about regarding since v7 (or later) patch is;
What is the different between OBJECT logging and SESSION logging?

In brief, session logging can log anything that happens in a user
session while object logging only targets DML and SELECT on selected
relations.

The pg_audit.log_relation setting is intended to mimic the prior
behavior of pg_audit before object logging was added.

In general, I would not expect pg_audit.log = 'read, write' to be used
with pg_audit.role. In other words, session logging of DML and SELECT
should probably not be turned on at the same time as object logging is
in use. Object logging is intended to be a fine-grained version of
pg_audit.log = 'read, write' (one or both).

Thank you for your explanation!
I understood about how to use two logging style.

I used v9 patch with "pg_audit.log_relation = on", and got quite
similar but different log as follows.

=# select * from hoge, bar where hoge.col = bar.col;
NOTICE: AUDIT: OBJECT,8,1,READ,SELECT,TABLE,public.hoge,"select *
from hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.hoge,"select *
from hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: OBJECT,8,1,READ,SELECT,TABLE,public.bar,"select * from
hoge, bar where hoge.col = bar.col;"
NOTICE: AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.bar,"select *
from hoge, bar where hoge.col = bar.col;"

The behaviour of SESSION log is similar to OBJECT log now, and SESSION
log per session (i.g, pg_audit.log_relation = off) is also similar to
'log_statement = all'. (enhancing log_statement might be enough)
So I couldn't understand needs of SESSION log.

Session logging is quite different from 'log_statement = all'. See:

/messages/by-id/552323B2.8060708@pgmasters.net

and/or the "Why pg_audit?" section of the pg_audit documentation.

I agree that it may make sense in the future to merge session logging
into log_statements, but for now it does provide important additional
functionality for creating audit logs.

I'm concerned that behaviour of pg_audit has been changed at a few
times as far as I remember. Did we achieve consensus on this design?

And one question; OBJECT logging of all tuple deletion (i.g. DELETE
FROM hoge) seems like not work as follows.

=# grant all on bar TO masahiko;

(1) Delete all tuple
=# insert into bar values(1);
=# delete from bar ;
NOTICE: AUDIT: SESSION,47,1,WRITE,DELETE,TABLE,public.bar,delete from bar ;
DELETE 1

(2) Delete specified tuple (but same result as (1))
=# insert into bar values(1);
=# delete from bar where col = 1;
NOTICE: AUDIT: OBJECT,48,1,WRITE,DELETE,TABLE,public.bar,delete from
bar where col = 1;
NOTICE: AUDIT: SESSION,48,1,WRITE,DELETE,TABLE,public.bar,delete from
bar where col = 1;
DELETE 1

Regards,

-------
Sawada Masahiko

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

#43David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#42)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/23/15 5:49 AM, Sawada Masahiko wrote:

I'm concerned that behaviour of pg_audit has been changed at a few
times as far as I remember. Did we achieve consensus on this design?

The original author Abhijit expressed support for the SESSION/OBJECT
concept before I started working on the code and so has Stephen Frost.
As far as I know all outstanding comments from the community have been
addressed.

Overall behavior has not changed very much since being submitted to the
CF in February - mostly just tweaks and additional options.

And one question; OBJECT logging of all tuple deletion (i.g. DELETE
FROM hoge) seems like not work as follows.

=# grant all on bar TO masahiko;

(1) Delete all tuple
=# insert into bar values(1);
=# delete from bar ;
NOTICE: AUDIT: SESSION,47,1,WRITE,DELETE,TABLE,public.bar,delete from bar ;
DELETE 1

(2) Delete specified tuple (but same result as (1))
=# insert into bar values(1);
=# delete from bar where col = 1;
NOTICE: AUDIT: OBJECT,48,1,WRITE,DELETE,TABLE,public.bar,delete from
bar where col = 1;
NOTICE: AUDIT: SESSION,48,1,WRITE,DELETE,TABLE,public.bar,delete from
bar where col = 1;
DELETE 1

Definitely a bug. Object logging works in the second case because the
select privileges on the "col" column trigger logging. I have fixed
this and added a regression test.

I also found a way to get the stack memory context under the query
memory context. Because of the order of execution it requires moving
the memory context but I still think it's a much better solution. I was
able to remove most of the stack pops (except function logging) and the
output remained stable.

I've also added some checking to make sure that if anything looks funny
on the stack an error will be generated.

Thanks for the feedback!

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v10.patchtext/plain; charset=UTF-8; name=pg_audit-v10.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index e2c4e27..3433f21 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -28,6 +28,7 @@ SUBDIRS = \
 		oid2name	\
 		pageinspect	\
 		passwordcheck	\
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/.gitignore b/contrib/pg_audit/.gitignore
new file mode 100644
index 0000000..a5267cf
--- /dev/null
+++ b/contrib/pg_audit/.gitignore
@@ -0,0 +1,5 @@
+log/
+results/
+tmp_check/
+regression.diffs
+regression.out
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..7b36011
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,21 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+REGRESS = pg_audit
+REGRESS_OPTS = --temp-config=$(top_srcdir)/contrib/pg_audit/pg_audit.conf
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/expected/pg_audit-deparse.out b/contrib/pg_audit/expected/pg_audit-deparse.out
new file mode 100644
index 0000000..c5651eb
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit-deparse.out
@@ -0,0 +1,942 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,VIEW,public.vw_test3,"SELECT *
+  FROM vw_test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,6,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,6,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+				  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+				  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+				  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,4,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,5,1,DDL,CREATE SCHEMA,,,CREATE SCHEMA test;
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,2,DDL,CREATE TABLE AS,,,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,9,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,10,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,10,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,11,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+NOTICE:  AUDIT: SESSION,12,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+NOTICE:  AUDIT: SESSION,13,1,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,14,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+NOTICE:  AUDIT: SESSION,15,1,MISC,CLOSE CURSOR,,,CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,16,1,MISC,COMMIT,,,COMMIT;
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,17,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);
+NOTICE:  AUDIT: SESSION,18,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,19,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,19,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,20,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,20,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,21,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,22,1,ROLE,GRANT,,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,23,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,24,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,25,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,26,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,26,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,27,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,27,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,28,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,29,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,30,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,31,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,31,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,31,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,31,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,31,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,32,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,32,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,32,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,34,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,INDEX,public.test_pkey,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,37,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,38,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,39,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,39,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,39,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+NOTICE:  AUDIT: SESSION,40,1,DDL,CREATE TABLE,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,40,2,DDL,CREATE TABLE,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,40,3,DDL,CREATE SCHEMA,,,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,41,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,42,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,42,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,43,1,DDL,CREATE AGGREGATE,,,"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,44,1,DDL,ALTER AGGREGATE,,,ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,45,1,DDL,CREATE CONVERSION,,,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,46,1,DDL,ALTER CONVERSION,,,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,47,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,48,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,49,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+NOTICE:  AUDIT: SESSION,50,1,DDL,CREATE TABLE,TABLE,public.hoge,"CREATE TABLE hoge
+(
+	id int
+);"
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+NOTICE:  AUDIT: SESSION,51,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;"
+SELECT test();
+NOTICE:  AUDIT: SESSION,52,1,READ,SELECT,,,SELECT test();
+NOTICE:  AUDIT: SESSION,52,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT test();
+NOTICE:  AUDIT: SESSION,52,3,READ,SELECT,TABLE,public.hoge,select * from hoge
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+     
+(1 row)
+
+--
+-- Delete all rows then delete 1 row
+SET pg_audit.log = 'write';
+SET pg_audit.role = 'auditor';
+create table bar
+(
+	col int
+);
+grant delete
+   on bar
+   to auditor;
+insert into bar (col)
+		 values (1);
+NOTICE:  AUDIT: SESSION,53,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col)
+		 values (1);"
+delete from bar;
+NOTICE:  AUDIT: OBJECT,54,1,WRITE,DELETE,TABLE,public.bar,delete from bar;
+NOTICE:  AUDIT: SESSION,54,1,WRITE,DELETE,TABLE,public.bar,delete from bar;
+insert into bar (col)
+		 values (1);
+NOTICE:  AUDIT: SESSION,55,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col)
+		 values (1);"
+delete from bar
+ where col = 1;
+NOTICE:  AUDIT: OBJECT,56,1,WRITE,DELETE,TABLE,public.bar,"delete from bar
+ where col = 1;"
+NOTICE:  AUDIT: SESSION,56,1,WRITE,DELETE,TABLE,public.bar,"delete from bar
+ where col = 1;"
+drop table bar;
diff --git a/contrib/pg_audit/expected/pg_audit.out b/contrib/pg_audit/expected/pg_audit.out
new file mode 100644
index 0000000..c5651eb
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit.out
@@ -0,0 +1,942 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,VIEW,public.vw_test3,"SELECT *
+  FROM vw_test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,6,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,6,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+				  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+				  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+				  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,4,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,5,1,DDL,CREATE SCHEMA,,,CREATE SCHEMA test;
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,6,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,7,2,DDL,CREATE TABLE AS,,,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,9,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,10,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,10,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,11,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+NOTICE:  AUDIT: SESSION,12,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+NOTICE:  AUDIT: SESSION,13,1,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,14,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+NOTICE:  AUDIT: SESSION,15,1,MISC,CLOSE CURSOR,,,CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,16,1,MISC,COMMIT,,,COMMIT;
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,17,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);
+NOTICE:  AUDIT: SESSION,18,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,19,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,19,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,20,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,20,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,21,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,22,1,ROLE,GRANT,,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,23,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,24,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,25,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,26,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,26,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,27,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,27,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,28,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,29,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,30,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,31,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,31,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,31,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,31,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,31,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,32,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,32,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,32,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,34,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,35,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,INDEX,public.test_pkey,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,37,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,38,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,39,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,39,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,39,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+NOTICE:  AUDIT: SESSION,40,1,DDL,CREATE TABLE,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,40,2,DDL,CREATE TABLE,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,40,3,DDL,CREATE SCHEMA,,,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,41,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,42,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,42,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,43,1,DDL,CREATE AGGREGATE,,,"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,44,1,DDL,ALTER AGGREGATE,,,ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,45,1,DDL,CREATE CONVERSION,,,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,46,1,DDL,ALTER CONVERSION,,,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,47,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,48,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,49,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+NOTICE:  AUDIT: SESSION,50,1,DDL,CREATE TABLE,TABLE,public.hoge,"CREATE TABLE hoge
+(
+	id int
+);"
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+NOTICE:  AUDIT: SESSION,51,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;"
+SELECT test();
+NOTICE:  AUDIT: SESSION,52,1,READ,SELECT,,,SELECT test();
+NOTICE:  AUDIT: SESSION,52,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT test();
+NOTICE:  AUDIT: SESSION,52,3,READ,SELECT,TABLE,public.hoge,select * from hoge
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+     
+(1 row)
+
+--
+-- Delete all rows then delete 1 row
+SET pg_audit.log = 'write';
+SET pg_audit.role = 'auditor';
+create table bar
+(
+	col int
+);
+grant delete
+   on bar
+   to auditor;
+insert into bar (col)
+		 values (1);
+NOTICE:  AUDIT: SESSION,53,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col)
+		 values (1);"
+delete from bar;
+NOTICE:  AUDIT: OBJECT,54,1,WRITE,DELETE,TABLE,public.bar,delete from bar;
+NOTICE:  AUDIT: SESSION,54,1,WRITE,DELETE,TABLE,public.bar,delete from bar;
+insert into bar (col)
+		 values (1);
+NOTICE:  AUDIT: SESSION,55,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col)
+		 values (1);"
+delete from bar
+ where col = 1;
+NOTICE:  AUDIT: OBJECT,56,1,WRITE,DELETE,TABLE,public.bar,"delete from bar
+ where col = 1;"
+NOTICE:  AUDIT: SESSION,56,1,WRITE,DELETE,TABLE,public.bar,"delete from bar
+ where col = 1;"
+drop table bar;
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..fe61ddb
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1809 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pg_audit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pg_audit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * auditLogRelation controls whether all relations are logged for READ and
+ * WRITE classes during session logging.
+ */
+bool auditLogRelation = false;
+
+/*
+ * auditLogNotice raises a notice as well as logging via the standard facility.
+ * This is primarily for the benefit of testing.
+ */
+bool auditLogNotice = false;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pg_audit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_PARAMETER		"PARAMETER"
+#define CLASS_READ			"READ"
+#define CLASS_ROLE			"ROLE"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* Function execution */
+	LOG_PARAMETER = (1 << 4),
+
+	/* SELECT */
+	LOG_READ = (1 << 5),
+
+	/* GRANT, REVOKE, CREATE/ALTER/DROP ROLE */
+	LOG_ROLE = (1 << 6),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 7),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_ALTER_ROLE	"ALTER ROLE"
+#define COMMAND_DROP_ROLE	"CREATE ROLE"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	int64 stackId;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static int64 statementTotal = 0;
+static int64 substatementTotal = 0;
+static int64 stackTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack to the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Create the unique stackId - used to keep the stack sane when memory
+	 * contexts are freed unexpectedly.
+	 */
+	stackItem->stackId = ++stackTotal;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop(int64 stackId)
+{
+	/* Make sure what we want to delete is at the top of the stack */
+	if (auditEventStack != NULL && auditEventStack->stackId == stackId)
+	{
+		MemoryContextDelete(auditEventStack->contextAudit);
+	}
+	else
+	{
+		elog(ERROR, "pg_audit stack item %ld not found on top - cannot pop",
+					stackId);
+	}
+}
+
+/*
+ * Check that an item is on the stack.  If not an error will be raised since
+ * this is a bad state to be in, and it might mean audit records are being
+ * lost.
+ */
+static void
+stack_valid(int64 stackId)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	if (nextItem == NULL)
+	{
+		elog(ERROR, "pg_audit stack item %ld not found - stack is empty",
+					stackId);
+	}
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item searched for */
+		if (nextItem->stackId == stackId)
+		{
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+
+	elog(ERROR, "pg_audit stack item %ld not found - top of stack is %ld",
+				stackId, auditEventStack->stackId);
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent, classifies it, then logs it if permissions were granted
+ * via roles or if the statement belongs in a class that is being logged.
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	MemoryContext contextOld;
+	StringInfoData auditStr;
+
+	/* By default put everything in the MISC class. */
+	enum LogClass class = LOG_MISC;
+	const char *className = CLASS_MISC;
+
+	/* Classify the statement using log stmt level and the command tag */
+	switch (stackItem->auditEvent.logStmtLevel)
+	{
+		/* All mods go in WRITE class */
+		case LOGSTMT_MOD:
+			className = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		/* Separate ROLE from other DDL statements */
+		case LOGSTMT_DDL:
+			/* Identify role statements */
+			if (stackItem->auditEvent.commandTag == T_GrantStmt ||
+				stackItem->auditEvent.commandTag == T_GrantRoleStmt ||
+				stackItem->auditEvent.commandTag == T_CreateRoleStmt ||
+				(stackItem->auditEvent.commandTag == T_RenameStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_ALTER_ROLE) == 0) ||
+				(stackItem->auditEvent.commandTag == T_DropStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_DROP_ROLE) == 0) ||
+				stackItem->auditEvent.commandTag == T_DropRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleSetStmt)
+			{
+				className = CLASS_ROLE;
+				class = LOG_ROLE;
+			}
+			/* Else log as DDL */
+			else
+			{
+				className = CLASS_DDL;
+				class = LOG_DDL;
+			}
+
+		/* Figure out the rest */
+		case LOGSTMT_ALL:
+			switch (stackItem->auditEvent.commandTag)
+			{
+				/* READ statements */
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					className = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				/* Reindex is DDL (because cluster is DDL) */
+				case T_ReindexStmt:
+					className = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				/* FUNCTION statements */
+				case T_DoStmt:
+					className = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * Only log the statement if:
+	 *
+	 * 1. If permissions were granted via roles
+	 * 2. The statement belongs to a class that is being logged
+	 */
+	if (!stackItem->auditEvent.granted && !(auditLogBitmap & class))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogBitmap & LOG_PARAMETER &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	elog(auditLogNotice ? NOTICE : LOG,
+		 "AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			className, auditStr.data);
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+audit_on_acl(Datum aclDatum,
+			 Oid auditOid,
+			 AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+audit_on_relation(Oid relOid,
+				  Oid auditOid,
+				  AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = audit_on_acl(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+audit_on_attribute(Oid relOid,
+				   AttrNumber attNum,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = audit_on_acl(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+audit_on_any_attribute(Oid relOid,
+					   Oid auditOid,
+					   Bitmapset *attributeSet,
+					   AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (audit_on_attribute(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			audit_on_attribute(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/*
+		 * If this is the first rte then session log unless auditLogRelation
+		 * is set.
+		 */
+		if (first && !auditLogRelation)
+		{
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms =
+				(ACL_SELECT | ACL_UPDATE | ACL_INSERT | ACL_DELETE) &
+				rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (audit_on_relation(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->selectedCols,
+											   ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEventStack->auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEventStack->auditEvent.granted =
+							audit_on_any_attribute(relOid, auditOid,
+												   rte->modifiedCols,
+												   auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Do relation level logging if a grant was found */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		/* Do relation level logging if auditLogRelation is set */
+		if (auditLogRelation)
+		{
+			auditEventStack->auditEvent.logged = false;
+			auditEventStack->auditEvent.granted = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop(stackItem->stackId);
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute */
+		case OAT_FUNCTION_EXECUTE:
+			if (auditLogBitmap & LOG_FUNCTION)
+				log_function_execute(objectId);
+			break;
+
+		/* Log create */
+		case OAT_POST_CREATE:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter */
+		case OAT_POST_ALTER:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop */
+		case OAT_DROP:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessDrop *drop = arg;
+
+				if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+
+	/*
+	 * Move the stack memory context to the query memory context.  This needs to
+	 * be done here because the query context does not exist before the call
+	 * to standard_ExecutorStart() but the stack item is required by
+	 * pg_audit_ExecutorCheckPerms_hook() which is called during
+	 * standard_ExecutorStart().
+	 */
+	if (stackItem)
+	{
+		MemoryContextSetParent(stackItem->contextAudit,
+							   queryDesc->estate->es_query_cxt);
+	}
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pg_audit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pg_audit_ProcessUtility_hook(Node *parsetree,
+							 const char *queryString,
+							 ProcessUtilityContext context,
+							 ParamListInfo params,
+							 DestReceiver *dest,
+							 char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+	int64 stackId;
+
+	/* Allocate the audit event */
+	if (!IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackId = stackItem->stackId;
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap != 0 &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/*
+	 * Process the audit event if there is one.  Also check that this event was
+	 * not popped off the stack by a memory context being freed elsewhere.
+	 */
+	if (stackItem)
+	{
+		/*
+		 * Make sure the item we want to log is still on the stack - if not then
+		 * something has gone wrong and an error will be raised.
+		 */
+		stack_valid(stackId);
+
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !stackItem->auditEvent.logged &&
+			!IsAbortedTransactionBlockState())
+			log_audit_event(stackItem);
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pg_audit_object_access_hook(ObjectAccessType access,
+							Oid classId,
+							Oid objectId,
+							int subId,
+							void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&
+		auditLogBitmap & (LOG_DDL | LOG_FUNCTION) && auditEventStack)
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+#ifdef DEPARSE
+	/* Continue only if session DDL logging is enabled */
+	if (auditLogBitmap & LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_ddl_command_end()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		auditEventStack->auditEvent.logStmtLevel =
+			GetCommandLogLevel(eventData->parsetree);
+		auditEventStack->auditEvent.commandTag =
+			nodeTag(eventData->parsetree);
+		auditEventStack->auditEvent.command =
+			CreateCommandTag(eventData->parsetree);
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT UPPER(object_type), identity\n"
+				"  FROM pg_event_trigger_ddl_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply object name and type for audit event */
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 1);
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 2);
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+#endif
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_sql_drop()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT UPPER(object_type),\n"
+				"       object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()\n"
+				" WHERE lower(object_type) <> 'type'\n"
+				"   AND schema_name <> 'pg_toast'";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 1);
+			auditEventStack->auditEvent.objectName =
+					SPI_getvalue(spiTuple, spiTupDesc, 2);
+
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pg_audit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then the token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_PARAMETER) == 0)
+			class = LOG_PARAMETER;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_ROLE) == 0)
+			class = LOG_ROLE;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pg_audit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pg_audit_log from extra (ignoring newval, which has already been
+ * converted to a bitmap above). Note that extra may not be set if the
+ * assignment is to be suppressed.
+ */
+static void
+assign_pg_audit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * Defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable object auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * Controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable session auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pg_audit_log,
+							   assign_pg_audit_log,
+							   NULL);
+
+	/*
+	 * pg_audit.log_relation = on
+	 *
+	 * Controls whether relations get separate log entries during session
+	 * logging of READ and WRITE classes.  This works as if all relations in the
+	 * database had been added to the audit role and provides a shortcut when
+	 * really detailed logging of absolutely every relation is required.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_relation",
+							 "Enable session relation logging",
+							 NULL,
+							 &auditLogRelation,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+
+	/*
+	 * pg_audit.log_notice = on
+	 *
+	 * Audit logging is raised as notices that can be seen on the client.  This is
+	 * intended for testing purposes.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_notice",
+							 "Raise a notice when logging",
+							 NULL,
+							 &auditLogNotice,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pg_audit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pg_audit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pg_audit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.conf b/contrib/pg_audit/pg_audit.conf
new file mode 100644
index 0000000..e9f4a22
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.conf
@@ -0,0 +1 @@
+shared_preload_libraries = pg_audit
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/sql/pg_audit.sql b/contrib/pg_audit/sql/pg_audit.sql
new file mode 100644
index 0000000..4089887
--- /dev/null
+++ b/contrib/pg_audit/sql/pg_audit.sql
@@ -0,0 +1,581 @@
+-- Load pg_audit module
+create extension pg_audit;
+
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+
+--
+-- Create auditor role
+CREATE ROLE auditor;
+
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = on;
+
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+DROP TABLE test;
+
+--
+-- Create second test user
+\connect contrib_regression super
+
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_notice = on;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+
+SELECT *
+  FROM test3, test2;
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+
+\connect contrib_regression user2
+
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+				  VALUES (1);
+
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+				  VALUES ('test');
+
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+
+--
+-- Create test schema
+CREATE SCHEMA test;
+
+--
+-- Copy pg_class to stdout
+COPY account TO stdout;
+
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+1	user1	HASH2	yada, yada
+\.
+
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+
+EXECUTE pgclassstmt (1);
+DEALLOCATE pgclassstmt;
+
+--
+-- Test cursor - no tables will be logged since pg_class is a system table
+BEGIN;
+
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+
+FETCH NEXT FROM ctest;
+CLOSE ctest;
+COMMIT;
+
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);
+EXECUTE pgclassstmt (1);
+
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+
+--
+-- Check that analyze is logged
+ANALYZE test;
+
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+
+SELECT *
+  FROM test;
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+
+SELECT 1,
+	   current_user;
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+
+explain select 1;
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+INSERT INTO TEST (id)
+		  VALUES (2);
+INSERT INTO TEST (id)
+		  VALUES (3);
+
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+
+ALTER TABLE public.test
+	RENAME TO test2;
+
+ALTER TABLE public.test2
+	SET SCHEMA test;
+
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+
+ALTER TABLE test.test2
+	DROP COLUMN description;
+
+DROP TABLE test.test2;
+
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+
+SELECT int_add(1, 1);
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+
+SELECT test();
+
+--
+-- Delete all rows then delete 1 row
+SET pg_audit.log = 'write';
+SET pg_audit.role = 'auditor';
+
+create table bar
+(
+	col int
+);
+
+grant delete
+   on bar
+   to auditor;
+
+insert into bar (col)
+		 values (1);
+delete from bar;
+
+insert into bar (col)
+		 values (1);
+delete from bar
+ where col = 1;
+
+drop table bar;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 49a6ce8..0a2bae8 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 26aa7ee..f32a1ed 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -125,6 +125,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
 <!ENTITY pgcrypto        SYSTEM "pgcrypto.sgml">
 <!ENTITY pgfreespacemap  SYSTEM "pgfreespacemap.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..ba04e7b
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,614 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> extenstion provides detailed session
+    and/or object audit logging via the standard logging facility.  The goal
+    is to provide the tools needed to produce audit logs required to pass any
+    goverment, financial, or ISO certification audit.
+  </para>
+
+  <para>
+    An audit is an official inspection of an individual's or organization's
+    accounts, typically by an independent body.  The information gathered by
+    <filename>pg_audit</filename> is properly called an audit trail or audit
+    log.  The term audit log is used in this documentation.
+  </para>
+
+  <sect2>
+    <title>Why <literal>pg_audit</>?</title>
+  
+    <para>
+      Basic statement logging can be provided by the standard logging facility
+      using <literal>log_statements = all</>.  This is acceptable for monitoring
+      and other usages but does not provide the level of detail generally
+      required for an audit.  It is not enough to have a list of all the
+      operations performed against the database. It must also be possible to
+      find particular statements that are of interest to an auditor.
+    </para>
+
+    <para>
+      For example, an auditor may want to verify that a particular table was
+      created inside a documented maintence window.  This might seem like a
+      simple job for grep, but what if you are presented with something like
+      this (intentionally obfuscated) example:
+    </para>
+
+    <programlisting>
+DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+    
+    <para>
+      Standard logging will give you this:
+    </para>
+
+    <programlisting>
+LOG:  statement: DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+    
+    <para>
+      It appears that finding the table of interest may require some knowledge
+      of the code in cases where tables are created dynamically.  This is not
+      ideal since it would be preferrable to just search on the table name.
+      This is where <literal>pg_audit</> comes in.  For the same input,
+      it will produce this output in the log:
+    </para>
+
+    <programlisting>
+AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;"
+AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.important_table,CREATE TABLE important_table (id INT)
+    </programlisting>
+
+    <para>
+      Not only is the <literal>DO</> block logged, but substatement 2 contains
+      the full text of the <literal>CREATE TABLE</> with the statement type,
+      object type, and full-qualified name to make searches easy.
+    </para>
+
+    <para>
+      When logging <literal>SELECT</> and <literal>DML</> statements,
+      <literal>pg_audit</> can be configured to log a separate entry for each
+      relation referenced in a statement.  No parsing is required to find all
+      statements that touch a particular table.  In fact, the goal is that the
+      statement text is provided primarily for deep forensics and should not be
+      the directly required for any search.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Usage Considerations</title>
+    
+    <para>
+      Depending on settings, it is possible for <literal>pg_audit</literal> to
+      generate an enormous volume of logging.  Be careful to determine
+      exactly what needs to be audit logged in your environment to avoid
+      logging too much.
+    </para>
+    
+    <para>
+      For example, when working in an OLAP environment it would probably not be
+      wise to audit log inserts into a large fact table.  The size of the log
+      file will likely be many times the actual data size of the inserts because
+      the log file is expressed as text.  Since logs are generally stored with
+      the OS this may lead to disk space being exhausted very
+      quickly.  In cases where it is not possible to limit audit logging to
+      certain tables, be sure to assess the performance impact while testing
+      and allocate plenty of space on the log volume.  This may also be true for
+      OLTP environments.  Even if the insert volume is not as high, the
+      performance impact of audit logging may still noticeably affect latency.
+    </para>
+    
+    <para>
+      To limit the number of relations audit logged for <literal>SELECT</>
+      and <literal>DML</> statments, consider using object audit logging
+      (see <xref linkend="pgaudit-object-audit-logging">).  Object audit logging
+      allows selection of the relations to be logged allowing for reduction
+      of the overall log volume.  However, when new relations are added they
+      must be explicitly added to object audit logging.  A programmatic
+      solution where specified tables are excluded from logging and all others
+      are included may be a good option in this case.
+    </para>
+  </sect2>
+  
+  <sect2>
+    <title>Settings</title>
+    
+    <para>
+      Settings may be modified only by a superuser. Allowing normal users to
+      change their settings would defeat the point of an audit log.
+    </para>
+      
+    <para>
+      Settings can be specified globally (in
+      <filename>postgresql.conf</filename> or using
+      <literal>ALTER SYSTEM ... SET</>), at the database level (using
+      <literal>ALTER DATABASE ... SET</literal>), or at the role level (using
+      <literal>ALTER ROLE ... SET</literal>).  Note that settings are not
+      inherited through normal role inheritance and <literal>SET ROLE</> will
+      not alter a user's <literal>pg_audit</> settings.  This is a limitation
+      of the roles system and not inherent to <literal>pg_audit</>.
+    </para>
+      
+    <para>
+      The <literal>pg_audit</> extension must be loaded in
+      <xref linkend="guc-shared-preload-libraries">.  Otherwise, an error
+      will be raised at load time and no audit logging will occur.
+    </para>
+
+    <variablelist>
+      <varlistentry id="guc-pgaudit-log" xreflabel="pg_audit.log">
+        <term><varname>pg_audit.log</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies which classes of statements will be logged by session
+            audit logging.  Possible values are:
+          </para>
+
+          <itemizedlist>
+            <listitem>
+              <para>
+                <literal>READ</literal> - <literal>SELECT</literal> and
+                <literal>COPY</literal> when the source is a relation or a
+                query.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>WRITE</literal> - <literal>INSERT</literal>,
+                <literal>UPDATE</literal>, <literal>DELETE</literal>,
+                <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+                destination is a relation.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>FUNCTION</literal> - Function calls and
+                <literal>DO</literal> blocks.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>ROLE</literal> - Statements related to roles and
+                privileges: <literal>GRANT</literal>,
+                <literal>REVOKE</literal>,
+                <literal>CREATE/ALTER/DROP ROLE</literal>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>DDL</literal> - All <literal>DDL</> that is not included
+                in the <literal>ROLE</> class plus <literal>REINDEX</>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>PARAMETER</literal> - Parameters that were passed for the
+                statement.  Parameters immediately follow the statement text.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>MISC</literal> - Miscellaneous commands, e.g.
+                <literal>DISCARD</literal>, <literal>FETCH</literal>,
+                <literal>CHECKPOINT</literal>, <literal>VACUUM</literal>.
+              </para>
+            </listitem>
+          </itemizedlist>
+            
+          <para>
+            Multiple classes can be provided using a comma-separated list and
+            classes can be subtracted by prefacing the class with a
+            <literal>-</> sign (see <xref linkend="pgaudit-session-audit-logging">).
+            The default is <literal>none</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-notice" xreflabel="pg_audit.log_notice">
+        <term><varname>pg_audit.log_notice</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_notice</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that the audit log messages should be raised as
+            <literal>NOTICE</> instead of <literal>LOG</>.  The primary
+            advantage is that <literal>NOTICE</> messages can be exposed
+            through the user interface.  This setting is used for regression
+            testing and may also be useful to end users for testing.  It is not
+            intended to be used in a production environment as it will leak
+            which statements are being logged to the user. The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-relation" xreflabel="pg_audit.log_relation">
+        <term><varname>pg_audit.log_relation</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_relation</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies whether session audit logging should create a separate
+            log entry for each relation (<literal>TABLE</>, <literal>VIEW</>,
+            etc.) referenced in a <literal>SELECT</> or <literal>DML</>
+            statement.  This is a useful shortcut for exhaustive logging
+            without using object audit logging.  The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-role" xreflabel="pg_audit.role">
+        <term><varname>pg_audit.role</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.role</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies the master role to use for object audit logging.  Muliple
+            audit roles can be defined by granting them to the master role.
+            This allows multiple groups to be in charge of different aspects
+            of audit logging.  There is no default.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </sect2>
+
+  <sect2 id="pgaudit-session-audit-logging">
+    <title>Session Audit Logging</title>
+
+    <para>
+      Session audit logging provides detailed logs of all statements executed
+      by a user in the backend.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is enabled with the <xref linkend="guc-pgaudit-log">
+        setting.
+
+        Enable session logging for all <literal>DML</> and <literal>DDL</> and
+        log all relations in <literal>DML</> statements:
+          <programlisting>
+set pg_audit.log = 'write, ddl';
+set pg_audit.log_relation = on;
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except <literal>MISC</> and
+        raise audit log messages as <literal>NOTICE</>:
+          <programlisting>
+set pg_audit.log = 'all, -misc';
+set pg_audit.log_notice = on;
+          </programlisting>
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example session audit logging is used to for logging
+        <literal>DDL</> and <literal>SELECT</> statements.  Note that the
+        insert statement is not logged since the <literal>WRITE</> class
+        is not enabled
+      </para>
+
+      <para>
+        SQL:
+      </para>
+      <programlisting>
+set pg_audit.log = 'read, ddl';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+
+select *
+    from account;
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,2,1,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2 id="pgaudit-object-audit-logging">
+    <title>Object Auditing</title>
+
+    <para>
+      Object audit logging logs statements that affect a particular relation.
+      Only <literal>SELECT</>, <literal>INSERT</>, <literal>UPDATE</> and
+      <literal>DELETE</> commands are supported.  <literal>TRUNCATE</> is not
+      included because there is no specific privilege for it.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level audit logging is implemented via the roles system.  The
+        <xref linkend="guc-pgaudit-role"> setting defines the role that
+        will be used for audit logging.  A relation (<literal>TABLE</>,
+        <literal>VIEW</>, etc.) will be audit logged when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.  This allows you to effectively have multiple audit roles
+        even though there is a single master role in any context.
+      </para>
+
+      <para>
+      Set <xref linkend="guc-pgaudit-role"> to <literal>auditor</> and
+      grant <literal>SELECT</> and <literal>DELETE</> privileges on the
+      <literal>account</> table.  Any <literal>SELECT</> or
+      <literal>DELETE</> statements on <literal>account</> will now be
+      logged:
+      </para>
+      
+      <programlisting>
+set pg_audit.role = 'auditor';
+
+grant select, delete
+   on public.account
+   to auditor;
+      </programlisting>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example object audit logging is used to illustrate how a
+        granular approach may be taken towards logging of <literal>SELECT</>
+        and <literal>DML</> statements.  Note that logging on the
+        <literal>account</> table is controlled by column-level permissions,
+        while logging on <literal>account_role_map</> is table-level.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+set pg_audit.role = 'auditor';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to auditor;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to auditor;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to auditor;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is compliant CSV format only if the log line prefix portion
+          of each log entry is removed.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</> - <literal>SESSION</> or
+            <literal>OBJECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT_ID</> - Unique statement ID for this session.
+            Each statement ID represents a backend call.  Statement IDs are
+            sequental even if some statements are not logged.  There may be
+            multiple entries for a statement ID when more than one relation
+            is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>SUBSTATEMENT_ID</> - Sequential ID for each
+            substatement within the main statement.  For example, calling
+            a function from a query.  Substatement IDs are continuous
+            even if some substatements are not logged.  There may be multiple
+            entries for a substatement ID when more than one relation is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</> - e.g. (<literal>READ</>,
+            <literal>ROLE</>) (see <xref linkend="guc-pgaudit-log">).
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</> - e.g. <literal>ALTER TABLE</>,
+            <literal>SELECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</> - <literal>TABLE</>,
+            <literal>INDEX</>, <literal>VIEW</>, etc.
+            Available for <literal>SELECT</>, <literal>DML</> and most
+            <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</> - The fully-qualified object name
+            (e.g. public.account).  Available for <literal>SELECT</>,
+            <literal>DML</> and most <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</> - Statement executed on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+
+    <para>
+      Use <xref linkend="guc-log-line-prefix"> to add any other fields that
+      are needed to satisfy your audit log requirements.  A typical log line
+      prefix might be <literal>'%m %u %d: '</> which would provide the date/time,
+      user name, and database name for each audit log.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Caveats</title>
+    
+    <itemizedlist>
+      <listitem>
+        <para>
+          Object renames are logged under the name they were renamed to.
+          For example, renaming a table will produce the following result:
+        </para>
+
+        <programlisting>
+ALTER TABLE test RENAME TO test2;
+          
+AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,ALTER TABLE test RENAME TO test2
+        </programlisting>
+      </listitem>
+      
+      <listitem>
+        <para>
+          It is possible to have a command logged more than once.  For example,
+          when a table is created with a primary key specified at creation time
+          the index for the primary key will be logged independently and another
+          audit log will be made for the index under the create entry.  The
+          multiple entries will however be contained within one statement ID.
+        </para>
+      </listitem>
+
+      <listitem>
+        <para>
+          Autovacuum and Autoanalyze are not logged, nor are they intended to be.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#44Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#43)
Re: Auditing extension for PostgreSQL (Take 2)

On Fri, Apr 24, 2015 at 3:23 AM, David Steele <david@pgmasters.net> wrote:

On 4/23/15 5:49 AM, Sawada Masahiko wrote:

I'm concerned that behaviour of pg_audit has been changed at a few
times as far as I remember. Did we achieve consensus on this design?

The original author Abhijit expressed support for the SESSION/OBJECT
concept before I started working on the code and so has Stephen Frost.
As far as I know all outstanding comments from the community have been
addressed.

Overall behavior has not changed very much since being submitted to the
CF in February - mostly just tweaks and additional options.

And one question; OBJECT logging of all tuple deletion (i.g. DELETE
FROM hoge) seems like not work as follows.

=# grant all on bar TO masahiko;

(1) Delete all tuple
=# insert into bar values(1);
=# delete from bar ;
NOTICE: AUDIT: SESSION,47,1,WRITE,DELETE,TABLE,public.bar,delete from bar ;
DELETE 1

(2) Delete specified tuple (but same result as (1))
=# insert into bar values(1);
=# delete from bar where col = 1;
NOTICE: AUDIT: OBJECT,48,1,WRITE,DELETE,TABLE,public.bar,delete from
bar where col = 1;
NOTICE: AUDIT: SESSION,48,1,WRITE,DELETE,TABLE,public.bar,delete from
bar where col = 1;
DELETE 1

Definitely a bug. Object logging works in the second case because the
select privileges on the "col" column trigger logging. I have fixed
this and added a regression test.

I also found a way to get the stack memory context under the query
memory context. Because of the order of execution it requires moving
the memory context but I still think it's a much better solution. I was
able to remove most of the stack pops (except function logging) and the
output remained stable.

I've also added some checking to make sure that if anything looks funny
on the stack an error will be generated.

Thanks for the feedback!

Thank you for updating the patch!
I ran the postgres regression test on database which is enabled
pg_audit, it works fine.
Looks good to me.

If someone don't have review comment or bug report, I will mark this
as "Ready for Committer".

Regards,

-------
Sawada Masahiko

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

#45David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#44)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/28/15 2:14 AM, Sawada Masahiko wrote:

On Fri, Apr 24, 2015 at 3:23 AM, David Steele <david@pgmasters.net> wrote:

I've also added some checking to make sure that if anything looks funny
on the stack an error will be generated.

Thanks for the feedback!

Thank you for updating the patch!
I ran the postgres regression test on database which is enabled
pg_audit, it works fine.
Looks good to me.

If someone don't have review comment or bug report, I will mark this
as "Ready for Committer".

Thank you! I appreciate all your work reviewing this patch and of
course everyone else who commented on, reviewed or tested the patch
along the way.

--
- David Steele
david@pgmasters.net

#46Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#45)
Re: Auditing extension for PostgreSQL (Take 2)

On Wed, Apr 29, 2015 at 12:17 AM, David Steele <david@pgmasters.net> wrote:

On 4/28/15 2:14 AM, Sawada Masahiko wrote:

On Fri, Apr 24, 2015 at 3:23 AM, David Steele <david@pgmasters.net> wrote:

I've also added some checking to make sure that if anything looks funny
on the stack an error will be generated.

Thanks for the feedback!

Thank you for updating the patch!
I ran the postgres regression test on database which is enabled
pg_audit, it works fine.
Looks good to me.

If someone don't have review comment or bug report, I will mark this
as "Ready for Committer".

Thank you! I appreciate all your work reviewing this patch and of
course everyone else who commented on, reviewed or tested the patch
along the way.

I have changed the status this to "Ready for Committer".

Regards,

-------
Sawada Masahiko

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

#47Fujii Masao
masao.fujii@gmail.com
In reply to: Sawada Masahiko (#46)
Re: Auditing extension for PostgreSQL (Take 2)

On Thu, Apr 30, 2015 at 12:57 PM, Sawada Masahiko <sawada.mshk@gmail.com> wrote:

On Wed, Apr 29, 2015 at 12:17 AM, David Steele <david@pgmasters.net> wrote:

On 4/28/15 2:14 AM, Sawada Masahiko wrote:

On Fri, Apr 24, 2015 at 3:23 AM, David Steele <david@pgmasters.net> wrote:

I've also added some checking to make sure that if anything looks funny
on the stack an error will be generated.

Thanks for the feedback!

Thank you for updating the patch!
I ran the postgres regression test on database which is enabled
pg_audit, it works fine.
Looks good to me.

If someone don't have review comment or bug report, I will mark this
as "Ready for Committer".

Thank you! I appreciate all your work reviewing this patch and of
course everyone else who commented on, reviewed or tested the patch
along the way.

I have changed the status this to "Ready for Committer".

The specification of "session audit logging" seems to be still unclear to me.
For example, why doesn't "session audit logging" log queries accessing to
a catalog like pg_class? Why doesn't it log any queries executed in aborted
transaction state? Since there is no such information in the document,
I'm afraid that users would easily get confused with it. Even if we document it,
I'm not sure if the current behavior is good for the audit purpose. For example,
some users may want to log even queries accessing to the catalogs.

I have no idea about when the current CommitFest will end. But probably
we don't have that much time left. So I'm thinking that maybe we should pick up
small, self-contained and useful part from the patch and focus on that.
If we try to commit every features that the patch provides, we might get
nothing at least in 9.5, I'm afraid.

Regards,

--
Fujii Masao

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

#48David Steele
david@pgmasters.net
In reply to: Fujii Masao (#47)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/30/15 6:05 AM, Fujii Masao wrote:

On Thu, Apr 30, 2015 at 12:57 PM, Sawada Masahiko <sawada.mshk@gmail.com> wrote:

I have changed the status this to "Ready for Committer".

The specification of "session audit logging" seems to be still unclear to me.
For example, why doesn't "session audit logging" log queries accessing to

The idea was that queries consisting of *only* catalog relations are
essentially noise. This makes the log much quieter when tools like psql
or PgAdmin are in use. Queries that contain a mix of catalog and user
tables are logged.

However, you make a good point, so in the spirit of cautious defaults
I've changed the behavior to log in this case and allow admins to turn
off the behavior if they choose with a new GUC, pg_audit.log_catalog.

a catalog like pg_class? Why doesn't it log any queries executed in aborted
transaction state? Since there is no such information in the document,

The error that aborts a transaction can easily be logged via the
standard logging facility. All prior statements in the transaction will
be logged with pg_audit. This is acceptable from an audit logging
perspective as long as it is documented behavior, which as you point out
it currently is not.

This has now been documented in the caveats sections which should make
it clearer to users.

I'm afraid that users would easily get confused with it. Even if we document it,
I'm not sure if the current behavior is good for the audit purpose. For example,
some users may want to log even queries accessing to the catalogs.

Agreed, and this is now the default.

I have no idea about when the current CommitFest will end. But probably
we don't have that much time left. So I'm thinking that maybe we should pick up
small, self-contained and useful part from the patch and focus on that.
If we try to commit every features that the patch provides, we might get
nothing at least in 9.5, I'm afraid.

May 15th is the feature freeze, so that does give a little time. It's
not clear to me what a "self-contained" part of the patch would be. If
you have specific ideas on what could be broken out I'm interested to
hear them.

Patch v11 is attached with the changes discussed here plus some other
improvements to the documentation suggested by Erik.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v11.patchtext/plain; charset=UTF-8; name=pg_audit-v11.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index f84e684..1f3d3f1 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -28,6 +28,7 @@ SUBDIRS = \
 		oid2name	\
 		pageinspect	\
 		passwordcheck	\
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/.gitignore b/contrib/pg_audit/.gitignore
new file mode 100644
index 0000000..a5267cf
--- /dev/null
+++ b/contrib/pg_audit/.gitignore
@@ -0,0 +1,5 @@
+log/
+results/
+tmp_check/
+regression.diffs
+regression.out
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..7b36011
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,21 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+REGRESS = pg_audit
+REGRESS_OPTS = --temp-config=$(top_srcdir)/contrib/pg_audit/pg_audit.conf
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/expected/pg_audit.out b/contrib/pg_audit/expected/pg_audit.out
new file mode 100644
index 0000000..d201420
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit.out
@@ -0,0 +1,969 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = ON;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_catalog = OFF;
+ALTER ROLE user2 SET pg_audit.log_notice = ON;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,VIEW,public.vw_test3,"SELECT *
+  FROM vw_test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,6,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,6,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+				  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+				  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+				  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+SET pg_audit.log_parameter = ON;
+NOTICE:  AUDIT: SESSION,4,1,MISC,SET,,,SET pg_audit.log_parameter = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,5,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,6,1,DDL,CREATE SCHEMA,,,CREATE SCHEMA test;
+--
+-- Copy account to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,8,2,DDL,CREATE TABLE AS,,,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,9,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,10,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,11,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,11,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,12,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor
+BEGIN;
+NOTICE:  AUDIT: SESSION,13,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+NOTICE:  AUDIT: SESSION,14,1,READ,SELECT,TABLE,pg_catalog.pg_class,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+NOTICE:  AUDIT: SESSION,14,2,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,15,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+NOTICE:  AUDIT: SESSION,16,1,MISC,CLOSE CURSOR,,,CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,17,1,MISC,COMMIT,,,COMMIT;
+--
+-- Turn off log_catalog and pg_class will not be logged
+SET pg_audit.log_catalog = OFF;
+NOTICE:  AUDIT: SESSION,18,1,MISC,SET,,,SET pg_audit.log_catalog = OFF;
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+ count 
+-------
+     1
+(1 row)
+
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,19,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);
+NOTICE:  AUDIT: SESSION,20,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,21,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,21,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,22,1,DDL,CREATE INDEX,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,22,2,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,23,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,24,1,ROLE,GRANT,,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,25,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,26,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,27,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,28,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,28,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,29,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,29,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,30,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,31,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,32,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,33,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,33,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,33,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,33,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,34,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,34,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,34,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,35,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,37,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,38,1,DDL,ALTER TABLE,INDEX,public.test_pkey,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,40,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,41,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,41,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,41,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+NOTICE:  AUDIT: SESSION,42,1,DDL,CREATE TABLE,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,42,2,DDL,CREATE TABLE,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,42,3,DDL,CREATE SCHEMA,,,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,43,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,44,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,44,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,45,1,DDL,CREATE AGGREGATE,,,"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,46,1,DDL,ALTER AGGREGATE,,,ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,47,1,DDL,CREATE CONVERSION,,,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,48,1,DDL,ALTER CONVERSION,,,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,49,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,50,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,51,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+NOTICE:  AUDIT: SESSION,52,1,DDL,CREATE TABLE,TABLE,public.hoge,"CREATE TABLE hoge
+(
+	id int
+);"
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+NOTICE:  AUDIT: SESSION,53,1,DDL,CREATE FUNCTION,,,"CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;"
+SELECT test();
+NOTICE:  AUDIT: SESSION,54,1,READ,SELECT,,,SELECT test();
+NOTICE:  AUDIT: SESSION,54,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT test();
+NOTICE:  AUDIT: SESSION,54,3,READ,SELECT,TABLE,public.hoge,select * from hoge
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+     
+(1 row)
+
+--
+-- Delete all rows then delete 1 row
+SET pg_audit.log = 'write';
+SET pg_audit.role = 'auditor';
+create table bar
+(
+	col int
+);
+grant delete
+   on bar
+   to auditor;
+insert into bar (col)
+		 values (1);
+NOTICE:  AUDIT: SESSION,55,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col)
+		 values (1);"
+delete from bar;
+NOTICE:  AUDIT: OBJECT,56,1,WRITE,DELETE,TABLE,public.bar,delete from bar;
+NOTICE:  AUDIT: SESSION,56,1,WRITE,DELETE,TABLE,public.bar,delete from bar;
+insert into bar (col)
+		 values (1);
+NOTICE:  AUDIT: SESSION,57,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col)
+		 values (1);"
+delete from bar
+ where col = 1;
+NOTICE:  AUDIT: OBJECT,58,1,WRITE,DELETE,TABLE,public.bar,"delete from bar
+ where col = 1;"
+NOTICE:  AUDIT: SESSION,58,1,WRITE,DELETE,TABLE,public.bar,"delete from bar
+ where col = 1;"
+drop table bar;
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..d686cb6
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1843 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pg_audit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pg_audit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * auditLogParameter controls whether parameters passed with the statement are
+ * included in the audit log.
+ */
+bool auditLogParameter = false;
+
+/*
+ * auditLogRelation controls whether all relations are logged for READ and
+ * WRITE classes during session logging.
+ */
+bool auditLogRelation = false;
+
+/*
+ * auditLogCatalog allows logging of relations in pg_catalog when a query
+ * contains *all* pg_catalog relations.
+ */
+bool auditLogCatalog = false;
+
+/*
+ * auditLogNotice raises a notice as well as logging via the standard facility.
+ * This is primarily for the benefit of testing.
+ */
+bool auditLogNotice = false;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pg_audit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_READ			"READ"
+#define CLASS_ROLE			"ROLE"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* SELECT */
+	LOG_READ = (1 << 4),
+
+	/* GRANT, REVOKE, CREATE/ALTER/DROP ROLE */
+	LOG_ROLE = (1 << 5),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 6),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_ALTER_ROLE	"ALTER ROLE"
+#define COMMAND_DROP_ROLE	"CREATE ROLE"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	int64 stackId;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static int64 statementTotal = 0;
+static int64 substatementTotal = 0;
+static int64 stackTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack to the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Create the unique stackId - used to keep the stack sane when memory
+	 * contexts are freed unexpectedly.
+	 */
+	stackItem->stackId = ++stackTotal;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop(int64 stackId)
+{
+	/* Make sure what we want to delete is at the top of the stack */
+	if (auditEventStack != NULL && auditEventStack->stackId == stackId)
+	{
+		MemoryContextDelete(auditEventStack->contextAudit);
+	}
+	else
+	{
+		elog(ERROR, "pg_audit stack item %ld not found on top - cannot pop",
+					stackId);
+	}
+}
+
+/*
+ * Check that an item is on the stack.  If not an error will be raised since
+ * this is a bad state to be in, and it might mean audit records are being
+ * lost.
+ */
+static void
+stack_valid(int64 stackId)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	if (nextItem == NULL)
+	{
+		elog(ERROR, "pg_audit stack item %ld not found - stack is empty",
+					stackId);
+	}
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item searched for */
+		if (nextItem->stackId == stackId)
+		{
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+
+	elog(ERROR, "pg_audit stack item %ld not found - top of stack is %ld",
+				stackId, auditEventStack->stackId);
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent, classifies it, then logs it if permissions were granted
+ * via roles or if the statement belongs in a class that is being logged.
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	MemoryContext contextOld;
+	StringInfoData auditStr;
+
+	/* By default put everything in the MISC class. */
+	enum LogClass class = LOG_MISC;
+	const char *className = CLASS_MISC;
+
+	/* Classify the statement using log stmt level and the command tag */
+	switch (stackItem->auditEvent.logStmtLevel)
+	{
+		/* All mods go in WRITE class */
+		case LOGSTMT_MOD:
+			className = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		/* Separate ROLE from other DDL statements */
+		case LOGSTMT_DDL:
+			/* Identify role statements */
+			if (stackItem->auditEvent.commandTag == T_GrantStmt ||
+				stackItem->auditEvent.commandTag == T_GrantRoleStmt ||
+				stackItem->auditEvent.commandTag == T_CreateRoleStmt ||
+				(stackItem->auditEvent.commandTag == T_RenameStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_ALTER_ROLE) == 0) ||
+				(stackItem->auditEvent.commandTag == T_DropStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_DROP_ROLE) == 0) ||
+				stackItem->auditEvent.commandTag == T_DropRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleSetStmt)
+			{
+				className = CLASS_ROLE;
+				class = LOG_ROLE;
+			}
+			/* Else log as DDL */
+			else
+			{
+				className = CLASS_DDL;
+				class = LOG_DDL;
+			}
+
+		/* Figure out the rest */
+		case LOGSTMT_ALL:
+			switch (stackItem->auditEvent.commandTag)
+			{
+				/* READ statements */
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					className = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				/* Reindex is DDL (because cluster is DDL) */
+				case T_ReindexStmt:
+					className = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				/* FUNCTION statements */
+				case T_DoStmt:
+					className = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * Only log the statement if:
+	 *
+	 * 1. If permissions were granted via roles
+	 * 2. The statement belongs to a class that is being logged
+	 */
+	if (!stackItem->auditEvent.granted && !(auditLogBitmap & class))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogParameter &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	elog(auditLogNotice ? NOTICE : LOG,
+		 "AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			className, auditStr.data);
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+audit_on_acl(Datum aclDatum,
+			 Oid auditOid,
+			 AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+audit_on_relation(Oid relOid,
+				  Oid auditOid,
+				  AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = audit_on_acl(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+audit_on_attribute(Oid relOid,
+				   AttrNumber attNum,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = audit_on_acl(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+audit_on_any_attribute(Oid relOid,
+					   Oid auditOid,
+					   Bitmapset *attributeSet,
+					   AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (audit_on_attribute(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			audit_on_attribute(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (!auditLogCatalog && IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/*
+		 * If this is the first rte then session log unless auditLogRelation
+		 * is set.
+		 */
+		if (first && !auditLogRelation)
+		{
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms =
+				(ACL_SELECT | ACL_UPDATE | ACL_INSERT | ACL_DELETE) &
+				rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (audit_on_relation(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns to see if the audit role has
+				 * priveleges on any of them.
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->selectedCols,
+											   ACL_SELECT);
+				}
+
+				/*
+				 * Check the modified columns to see if the audit role has
+				 * privileges on any of them.
+				 */
+				if (!auditEventStack->auditEvent.granted)
+				{
+					auditPerms &= (ACL_INSERT | ACL_UPDATE);
+
+					if (auditPerms)
+					{
+						auditEventStack->auditEvent.granted =
+							audit_on_any_attribute(relOid, auditOid,
+												   rte->modifiedCols,
+												   auditPerms);
+					}
+				}
+			}
+		}
+
+		/* Do relation level logging if a grant was found */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		/* Do relation level logging if auditLogRelation is set */
+		if (auditLogRelation)
+		{
+			auditEventStack->auditEvent.logged = false;
+			auditEventStack->auditEvent.granted = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for certain kinds of CREATE, ALTER, and DELETE statements
+ * where the object can be logged.
+ */
+static void
+log_create_alter_drop(Oid classId,
+					  Oid objectId)
+{
+	/* Only perform when class is relation */
+	if (classId == RelationRelationId)
+	{
+		Relation rel;
+		Form_pg_class class;
+
+		/* Open the relation */
+		rel = relation_open(objectId, NoLock);
+
+		/* Filter out any system relations */
+		if (IsToastNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			return;
+		}
+
+		/* Get rel information and close it */
+		class = RelationGetForm(rel);
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Set object type based on relkind */
+		switch (class->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			/*
+			 * Any other cases will be handled by log_utility_command().
+			 */
+			default:
+				return;
+				break;
+		}
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop(stackItem->stackId);
+}
+
+/*
+ * Log object accesses (which is more about DDL than DML, even though it
+ * sounds like the latter).
+ */
+static void
+log_object_access(ObjectAccessType access,
+				  Oid classId,
+				  Oid objectId,
+				  int subId,
+				  void *arg)
+{
+	switch (access)
+	{
+		/* Log execute */
+		case OAT_FUNCTION_EXECUTE:
+			if (auditLogBitmap & LOG_FUNCTION)
+				log_function_execute(objectId);
+			break;
+
+		/* Log create */
+		case OAT_POST_CREATE:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostCreate *pc = arg;
+
+				if (pc->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log alter */
+		case OAT_POST_ALTER:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessPostAlter *pa = arg;
+
+				if (pa->is_internal)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* Log drop */
+		case OAT_DROP:
+			if (auditLogBitmap & LOG_DDL)
+			{
+				ObjectAccessDrop *drop = arg;
+
+				if (drop->dropflags & PERFORM_DELETION_INTERNAL)
+					return;
+
+				log_create_alter_drop(classId, objectId);
+			}
+			break;
+
+		/* All others processed by log_utility_command() */
+		default:
+			break;
+	}
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+
+	/*
+	 * Move the stack memory context to the query memory context.  This needs to
+	 * be done here because the query context does not exist before the call
+	 * to standard_ExecutorStart() but the stack item is required by
+	 * pg_audit_ExecutorCheckPerms_hook() which is called during
+	 * standard_ExecutorStart().
+	 */
+	if (stackItem)
+	{
+		MemoryContextSetParent(stackItem->contextAudit,
+							   queryDesc->estate->es_query_cxt);
+	}
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pg_audit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pg_audit_ProcessUtility_hook(Node *parsetree,
+							 const char *queryString,
+							 ProcessUtilityContext context,
+							 ParamListInfo params,
+							 DestReceiver *dest,
+							 char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+	int64 stackId;
+
+	/* Allocate the audit event */
+	if (!IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackId = stackItem->stackId;
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap != 0 &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/*
+	 * Process the audit event if there is one.  Also check that this event was
+	 * not popped off the stack by a memory context being freed elsewhere.
+	 */
+	if (stackItem)
+	{
+		/*
+		 * Make sure the item we want to log is still on the stack - if not then
+		 * something has gone wrong and an error will be raised.
+		 */
+		stack_valid(stackId);
+
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !stackItem->auditEvent.logged &&
+			!IsAbortedTransactionBlockState())
+			log_audit_event(stackItem);
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for execute,
+ * create, drop, and alter commands.  Most of the audit information is filled in
+ * by log_utility_command().
+ */
+static void
+pg_audit_object_access_hook(ObjectAccessType access,
+							Oid classId,
+							Oid objectId,
+							int subId,
+							void *arg)
+{
+	if (auditLogBitmap != 0 && !IsAbortedTransactionBlockState() &&
+		auditLogBitmap & (LOG_DDL | LOG_FUNCTION) && auditEventStack)
+		log_object_access(access, classId, objectId, subId, arg);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+#ifdef DEPARSE
+	/* Continue only if session DDL logging is enabled */
+	if (auditLogBitmap & LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_ddl_command_end()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		auditEventStack->auditEvent.logStmtLevel =
+			GetCommandLogLevel(eventData->parsetree);
+		auditEventStack->auditEvent.commandTag =
+			nodeTag(eventData->parsetree);
+		auditEventStack->auditEvent.command =
+			CreateCommandTag(eventData->parsetree);
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT UPPER(object_type), identity\n"
+				"  FROM pg_event_trigger_ddl_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply object name and type for audit event */
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 1);
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 2);
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+#endif
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to pg_audit_sql_drop()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT UPPER(object_type),\n"
+				"       object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()\n"
+				" WHERE lower(object_type) <> 'type'\n"
+				"   AND schema_name <> 'pg_toast'";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 1);
+			auditEventStack->auditEvent.objectName =
+					SPI_getvalue(spiTuple, spiTupDesc, 2);
+
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pg_audit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then the token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_ROLE) == 0)
+			class = LOG_ROLE;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pg_audit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pg_audit_log from extra (ignoring newval, which has already been
+ * converted to a bitmap above). Note that extra may not be set if the
+ * assignment is to be suppressed.
+ */
+static void
+assign_pg_audit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * Defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable object auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * Controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable session auditing for classes of commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pg_audit_log,
+							   assign_pg_audit_log,
+							   NULL);
+
+	/*
+	 * pg_audit.log_parameter = on
+	 *
+	 * Controls whether parameters passed with a statement are logged.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_parameter",
+							 "Enable statement parameter logging",
+							 NULL,
+							 &auditLogParameter,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log_relation = on
+	 *
+	 * Controls whether relations get separate log entries during session
+	 * logging of READ and WRITE classes.  This works as if all relations in the
+	 * database had been added to the audit role and provides a shortcut when
+	 * really detailed logging of absolutely every relation is required.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_relation",
+							 "Enable session relation logging",
+							 NULL,
+							 &auditLogRelation,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log_catalog = on
+	 *
+	 * Controls whether relations in pg_catalog are session logged in queries
+	 * that contain *only* pg_catalog relations.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_catalog",
+							 "Enable logging of relations in pg_catalog",
+							 NULL,
+							 &auditLogCatalog,
+							 true,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log_notice = on
+	 *
+	 * Audit logging is raised as notices that can be seen on the client.  This is
+	 * intended for testing purposes.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_notice",
+							 "Raise a notice when logging",
+							 NULL,
+							 &auditLogNotice,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pg_audit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pg_audit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pg_audit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.conf b/contrib/pg_audit/pg_audit.conf
new file mode 100644
index 0000000..e9f4a22
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.conf
@@ -0,0 +1 @@
+shared_preload_libraries = pg_audit
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/sql/pg_audit.sql b/contrib/pg_audit/sql/pg_audit.sql
new file mode 100644
index 0000000..226f60d
--- /dev/null
+++ b/contrib/pg_audit/sql/pg_audit.sql
@@ -0,0 +1,595 @@
+-- Load pg_audit module
+create extension pg_audit;
+
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+\connect contrib_regression super;
+
+--
+-- Create auditor role
+CREATE ROLE auditor;
+
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = ON;
+
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+DROP TABLE test;
+
+--
+-- Create second test user
+\connect contrib_regression super
+
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_catalog = OFF;
+ALTER ROLE user2 SET pg_audit.log_notice = ON;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+
+SELECT *
+  FROM test3, test2;
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+
+\connect contrib_regression user2
+
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+				  VALUES (1);
+
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+				  VALUES ('test');
+
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+SET pg_audit.log_parameter = ON;
+
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+
+--
+-- Create test schema
+CREATE SCHEMA test;
+
+--
+-- Copy account to stdout
+COPY account TO stdout;
+
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+1	user1	HASH2	yada, yada
+\.
+
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+
+EXECUTE pgclassstmt (1);
+DEALLOCATE pgclassstmt;
+
+--
+-- Test cursor
+BEGIN;
+
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+
+FETCH NEXT FROM ctest;
+CLOSE ctest;
+COMMIT;
+
+--
+-- Turn off log_catalog and pg_class will not be logged
+SET pg_audit.log_catalog = OFF;
+
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);
+EXECUTE pgclassstmt (1);
+
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+
+--
+-- Check that analyze is logged
+ANALYZE test;
+
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+
+SELECT *
+  FROM test;
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+
+SELECT 1,
+	   current_user;
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+
+explain select 1;
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+INSERT INTO TEST (id)
+		  VALUES (2);
+INSERT INTO TEST (id)
+		  VALUES (3);
+
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+
+ALTER TABLE public.test
+	RENAME TO test2;
+
+ALTER TABLE public.test2
+	SET SCHEMA test;
+
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+
+ALTER TABLE test.test2
+	DROP COLUMN description;
+
+DROP TABLE test.test2;
+
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+
+SELECT int_add(1, 1);
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+
+SELECT test();
+
+--
+-- Delete all rows then delete 1 row
+SET pg_audit.log = 'write';
+SET pg_audit.role = 'auditor';
+
+create table bar
+(
+	col int
+);
+
+grant delete
+   on bar
+   to auditor;
+
+insert into bar (col)
+		 values (1);
+delete from bar;
+
+insert into bar (col)
+		 values (1);
+delete from bar
+ where col = 1;
+
+drop table bar;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 49a6ce8..0a2bae8 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 6268d54..03fea32 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -126,6 +126,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
 <!ENTITY pgcrypto        SYSTEM "pgcrypto.sgml">
 <!ENTITY pgfreespacemap  SYSTEM "pgfreespacemap.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..e2f8f31
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,657 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> extension provides detailed session
+    and/or object audit logging via the standard logging facility.  The goal
+    is to provide the tools needed to produce audit logs required to pass any
+    government, financial, or ISO certification audit.
+  </para>
+
+  <para>
+    An audit is an official inspection of an individual's or organization's
+    accounts, typically by an independent body.  The information gathered by
+    <filename>pg_audit</filename> is properly called an audit trail or audit
+    log.  The term audit log is used in this documentation.
+  </para>
+
+  <sect2>
+    <title>Why <literal>pg_audit</>?</title>
+
+    <para>
+      Basic statement logging can be provided by the standard logging facility
+      using <literal>log_statement = all</>.  This is acceptable for monitoring
+      and other usages but does not provide the level of detail generally
+      required for an audit.  It is not enough to have a list of all the
+      operations performed against the database. It must also be possible to
+      find particular statements that are of interest to an auditor.
+    </para>
+
+    <para>
+      For example, an auditor may want to verify that a particular table was
+      created inside a documented maintenance window.  This might seem like a
+      simple job for grep, but what if you are presented with something like
+      this (intentionally obfuscated) example:
+    </para>
+
+    <programlisting>
+DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+
+    <para>
+      Standard logging will give you this:
+    </para>
+
+    <programlisting>
+LOG:  statement: DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+
+    <para>
+      It appears that finding the table of interest may require some knowledge
+      of the code in cases where tables are created dynamically.  This is not
+      ideal since it would be preferrable to just search on the table name.
+      This is where <literal>pg_audit</> comes in.  For the same input,
+      it will produce this output in the log:
+    </para>
+
+    <programlisting>
+AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;"
+AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.important_table,CREATE TABLE important_table (id INT)
+    </programlisting>
+
+    <para>
+      Not only is the <literal>DO</> block logged, but substatement 2 contains
+      the full text of the <literal>CREATE TABLE</> with the statement type,
+      object type, and full-qualified name to make searches easy.
+    </para>
+
+    <para>
+      When logging <literal>SELECT</> and <literal>DML</> statements,
+      <literal>pg_audit</> can be configured to log a separate entry for each
+      relation referenced in a statement.  No parsing is required to find all
+      statements that touch a particular table.  In fact, the goal is that the
+      statement text is provided primarily for deep forensics and should not be
+      the required for an audit.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Usage Considerations</title>
+
+    <para>
+      Depending on settings, it is possible for <literal>pg_audit</literal> to
+      generate an enormous volume of logging.  Be careful to determine
+      exactly what needs to be audit logged in your environment to avoid
+      logging too much.
+    </para>
+
+    <para>
+      For example, when working in an OLAP environment it would probably not be
+      wise to audit log inserts into a large fact table.  The size of the log
+      file will likely be many times the actual data size of the inserts because
+      the log file is expressed as text.  Since logs are generally stored with
+      the OS this may lead to disk space being exhausted very
+      quickly.  In cases where it is not possible to limit audit logging to
+      certain tables, be sure to assess the performance impact while testing
+      and allocate plenty of space on the log volume.  This may also be true for
+      OLTP environments.  Even if the insert volume is not as high, the
+      performance impact of audit logging may still noticeably affect latency.
+    </para>
+
+    <para>
+      To limit the number of relations audit logged for <literal>SELECT</>
+      and <literal>DML</> statments, consider using object audit logging
+      (see <xref linkend="pgaudit-object-audit-logging">).  Object audit logging
+      allows selection of the relations to be logged allowing for reduction
+      of the overall log volume.  However, when new relations are added they
+      must be explicitly added to object audit logging.  A programmatic
+      solution where specified tables are excluded from logging and all others
+      are included may be a good option in this case.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Settings</title>
+
+    <para>
+      Settings may be modified only by a superuser. Allowing normal users to
+      change their settings would defeat the point of an audit log.
+    </para>
+
+    <para>
+      Settings can be specified globally (in
+      <filename>postgresql.conf</filename> or using
+      <literal>ALTER SYSTEM ... SET</>), at the database level (using
+      <literal>ALTER DATABASE ... SET</literal>), or at the role level (using
+      <literal>ALTER ROLE ... SET</literal>).  Note that settings are not
+      inherited through normal role inheritance and <literal>SET ROLE</> will
+      not alter a user's <literal>pg_audit</> settings.  This is a limitation
+      of the roles system and not inherent to <literal>pg_audit</>.
+    </para>
+
+    <para>
+      The <literal>pg_audit</> extension must be loaded in
+      <xref linkend="guc-shared-preload-libraries">.  Otherwise, an error
+      will be raised at load time and no audit logging will occur.
+    </para>
+
+    <variablelist>
+      <varlistentry id="guc-pgaudit-log" xreflabel="pg_audit.log">
+        <term><varname>pg_audit.log</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies which classes of statements will be logged by session
+            audit logging.  Possible values are:
+          </para>
+
+          <itemizedlist>
+            <listitem>
+              <para>
+                <literal>READ</literal> - <literal>SELECT</literal> and
+                <literal>COPY</literal> when the source is a relation or a
+                query.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>WRITE</literal> - <literal>INSERT</literal>,
+                <literal>UPDATE</literal>, <literal>DELETE</literal>,
+                <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+                destination is a relation.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>FUNCTION</literal> - Function calls and
+                <literal>DO</literal> blocks.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>ROLE</literal> - Statements related to roles and
+                privileges: <literal>GRANT</literal>,
+                <literal>REVOKE</literal>,
+                <literal>CREATE/ALTER/DROP ROLE</literal>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>DDL</literal> - All <literal>DDL</> that is not included
+                in the <literal>ROLE</> class plus <literal>REINDEX</>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>MISC</literal> - Miscellaneous commands, e.g.
+                <literal>DISCARD</literal>, <literal>FETCH</literal>,
+                <literal>CHECKPOINT</literal>, <literal>VACUUM</literal>.
+              </para>
+            </listitem>
+          </itemizedlist>
+
+          <para>
+            Multiple classes can be provided using a comma-separated list and
+            classes can be subtracted by prefacing the class with a
+            <literal>-</> sign (see <xref linkend="pgaudit-session-audit-logging">).
+            The default is <literal>none</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-catalog" xreflabel="pg_audit.log_catalog">
+        <term><varname>pg_audit.log_catalog</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_catalog</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that session logging should be enabled in the case where all
+            relations in a statement are in pg_catalog.  Disabling this setting
+            will reduce noise in the log from tools like psql and PgAdmin that query
+            the catalog frequently and heavily. The default is <literal>on</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-notice" xreflabel="pg_audit.log_notice">
+        <term><varname>pg_audit.log_notice</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_notice</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that the audit log messages should be raised as
+            <literal>NOTICE</> instead of <literal>LOG</>.  The primary
+            advantage is that <literal>NOTICE</> messages can be exposed
+            through the user interface.  This setting is used for regression
+            testing and may also be useful to end users for testing.  It is not
+            intended to be used in a production environment as it will leak
+            which statements are being logged to the user. The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-parameter" xreflabel="pg_audit.log_parameter">
+        <term><varname>pg_audit.log_parameter</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_parameter</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that audit logging should include the parameters that
+            were passed with the statement.  When parameters are present they will
+            be included in CSV format after the statement text. The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-relation" xreflabel="pg_audit.log_relation">
+        <term><varname>pg_audit.log_relation</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_relation</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies whether session audit logging should create a separate
+            log entry for each relation (<literal>TABLE</>, <literal>VIEW</>,
+            etc.) referenced in a <literal>SELECT</> or <literal>DML</>
+            statement.  This is a useful shortcut for exhaustive logging
+            without using object audit logging.  The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-role" xreflabel="pg_audit.role">
+        <term><varname>pg_audit.role</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.role</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies the master role to use for object audit logging.  Muliple
+            audit roles can be defined by granting them to the master role.
+            This allows multiple groups to be in charge of different aspects
+            of audit logging.  There is no default.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </sect2>
+
+  <sect2 id="pgaudit-session-audit-logging">
+    <title>Session Audit Logging</title>
+
+    <para>
+      Session audit logging provides detailed logs of all statements executed
+      by a user in the backend.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is enabled with the <xref linkend="guc-pgaudit-log">
+        setting.
+
+        Enable session logging for all <literal>DML</> and <literal>DDL</> and
+        log all relations in <literal>DML</> statements:
+          <programlisting>
+set pg_audit.log = 'write, ddl';
+set pg_audit.log_relation = on;
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except <literal>MISC</> and
+        raise audit log messages as <literal>NOTICE</>:
+          <programlisting>
+set pg_audit.log = 'all, -misc';
+set pg_audit.log_notice = on;
+          </programlisting>
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example session audit logging is used for logging
+        <literal>DDL</> and <literal>SELECT</> statements.  Note that the
+        insert statement is not logged since the <literal>WRITE</> class
+        is not enabled
+      </para>
+
+      <para>
+        SQL:
+      </para>
+      <programlisting>
+set pg_audit.log = 'read, ddl';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+
+select *
+    from account;
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,2,1,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2 id="pgaudit-object-audit-logging">
+    <title>Object Auditing</title>
+
+    <para>
+      Object audit logging logs statements that affect a particular relation.
+      Only <literal>SELECT</>, <literal>INSERT</>, <literal>UPDATE</> and
+      <literal>DELETE</> commands are supported.  <literal>TRUNCATE</> is not
+      included because there is no specific privilege for it.
+    </para>
+
+    <para>
+      Object audit logging is intended to be a finer-grained replacement for
+      <literal>pg_audit.log = 'read, write'</literal>.  As such, it may not
+      make sense to use them in conjunction but one possible scenario would
+      be to use session logging to capture each statement and then supplement
+      that with object logging to get more detail about specific relations. 
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level audit logging is implemented via the roles system.  The
+        <xref linkend="guc-pgaudit-role"> setting defines the role that
+        will be used for audit logging.  A relation (<literal>TABLE</>,
+        <literal>VIEW</>, etc.) will be audit logged when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.  This allows you to effectively have multiple audit roles
+        even though there is a single master role in any context.
+      </para>
+
+      <para>
+      Set <xref linkend="guc-pgaudit-role"> to <literal>auditor</> and
+      grant <literal>SELECT</> and <literal>DELETE</> privileges on the
+      <literal>account</> table.  Any <literal>SELECT</> or
+      <literal>DELETE</> statements on <literal>account</> will now be
+      logged:
+      </para>
+
+      <programlisting>
+set pg_audit.role = 'auditor';
+
+grant select, delete
+   on public.account
+   to auditor;
+      </programlisting>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example object audit logging is used to illustrate how a
+        granular approach may be taken towards logging of <literal>SELECT</>
+        and <literal>DML</> statements.  Note that logging on the
+        <literal>account</> table is controlled by column-level permissions,
+        while logging on <literal>account_role_map</> is table-level.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+set pg_audit.role = 'auditor';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to auditor;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to auditor;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to auditor;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is compliant CSV format only if the log line prefix portion
+          of each log entry is removed.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</> - <literal>SESSION</> or
+            <literal>OBJECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT_ID</> - Unique statement ID for this session.
+            Each statement ID represents a backend call.  Statement IDs are
+            sequential even if some statements are not logged.  There may be
+            multiple entries for a statement ID when more than one relation
+            is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>SUBSTATEMENT_ID</> - Sequential ID for each
+            substatement within the main statement.  For example, calling
+            a function from a query.  Substatement IDs are continuous
+            even if some substatements are not logged.  There may be multiple
+            entries for a substatement ID when more than one relation is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</> - e.g. (<literal>READ</>,
+            <literal>ROLE</>) (see <xref linkend="guc-pgaudit-log">).
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</> - e.g. <literal>ALTER TABLE</>,
+            <literal>SELECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</> - <literal>TABLE</>,
+            <literal>INDEX</>, <literal>VIEW</>, etc.
+            Available for <literal>SELECT</>, <literal>DML</> and most
+            <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</> - The fully-qualified object name
+            (e.g. public.account).  Available for <literal>SELECT</>,
+            <literal>DML</> and most <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</> - Statement executed on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+
+    <para>
+      Use <xref linkend="guc-log-line-prefix"> to add any other fields that
+      are needed to satisfy your audit log requirements.  A typical log line
+      prefix might be <literal>'%m %u %d: '</> which would provide the date/time,
+      user name, and database name for each audit log.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Caveats</title>
+
+    <itemizedlist>
+      <listitem>
+        <para>
+          Object renames are logged under the name they were renamed to.
+          For example, renaming a table will produce the following result:
+        </para>
+
+        <programlisting>
+ALTER TABLE test RENAME TO test2;
+
+AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,ALTER TABLE test RENAME TO test2
+        </programlisting>
+      </listitem>
+
+      <listitem>
+        <para>
+          It is possible to have a command logged more than once.  For example,
+          when a table is created with a primary key specified at creation time
+          the index for the primary key will be logged independently and another
+          audit log will be made for the index under the create entry.  The
+          multiple entries will however be contained within one statement ID.
+        </para>
+      </listitem>
+
+      <listitem>
+        <para>
+          Autovacuum and Autoanalyze are not logged, nor are they intended to be.
+        </para>
+      </listitem>
+
+      <listitem>
+        <para>
+          Statements that are executed after a transaction enters an aborted state
+          will not be audit logged.  However, the statement that caused the error
+          and any subsequent statements executed in the aborted transaction will
+          be logged as ERRORs by the standard logging facility.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#49Sawada Masahiko
sawada.mshk@gmail.com
In reply to: David Steele (#48)
Re: Auditing extension for PostgreSQL (Take 2)

On Fri, May 1, 2015 at 6:24 AM, David Steele <david@pgmasters.net> wrote:

On 4/30/15 6:05 AM, Fujii Masao wrote:

On Thu, Apr 30, 2015 at 12:57 PM, Sawada Masahiko <sawada.mshk@gmail.com> wrote:

I have changed the status this to "Ready for Committer".

The specification of "session audit logging" seems to be still unclear to me.
For example, why doesn't "session audit logging" log queries accessing to

The idea was that queries consisting of *only* catalog relations are
essentially noise. This makes the log much quieter when tools like psql
or PgAdmin are in use. Queries that contain a mix of catalog and user
tables are logged.

However, you make a good point, so in the spirit of cautious defaults
I've changed the behavior to log in this case and allow admins to turn
off the behavior if they choose with a new GUC, pg_audit.log_catalog.

a catalog like pg_class? Why doesn't it log any queries executed in aborted
transaction state? Since there is no such information in the document,

The error that aborts a transaction can easily be logged via the
standard logging facility. All prior statements in the transaction will
be logged with pg_audit. This is acceptable from an audit logging
perspective as long as it is documented behavior, which as you point out
it currently is not.

This has now been documented in the caveats sections which should make
it clearer to users.

I'm afraid that users would easily get confused with it. Even if we document it,
I'm not sure if the current behavior is good for the audit purpose. For example,
some users may want to log even queries accessing to the catalogs.

Agreed, and this is now the default.

I have no idea about when the current CommitFest will end. But probably
we don't have that much time left. So I'm thinking that maybe we should pick up
small, self-contained and useful part from the patch and focus on that.
If we try to commit every features that the patch provides, we might get
nothing at least in 9.5, I'm afraid.

May 15th is the feature freeze, so that does give a little time. It's
not clear to me what a "self-contained" part of the patch would be. If
you have specific ideas on what could be broken out I'm interested to
hear them.

Patch v11 is attached with the changes discussed here plus some other
improvements to the documentation suggested by Erik.

For now, since pg_audit patch has a pg_audit_ddl_command_end()
function which uses part of un-committed "deparsing utility commands"
patch API,
pg_audit patch is completely depend on that patch.
If API name, interface are changed, it would affect for pg_audit patch.
I'm not sure about progress of "deparsing utility command" patch but
you have any idea if that patch is not committed into 9.5 until May
15?

Regards,

-------
Sawada Masahiko

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

#50David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#49)
Re: Auditing extension for PostgreSQL (Take 2)

On 5/1/15 5:58 AM, Sawada Masahiko wrote:

On Fri, May 1, 2015 at 6:24 AM, David Steele <david@pgmasters.net> wrote:

May 15th is the feature freeze, so that does give a little time. It's
not clear to me what a "self-contained" part of the patch would be. If
you have specific ideas on what could be broken out I'm interested to
hear them.

Patch v11 is attached with the changes discussed here plus some other
improvements to the documentation suggested by Erik.

For now, since pg_audit patch has a pg_audit_ddl_command_end()
function which uses part of un-committed "deparsing utility commands"
patch API,
pg_audit patch is completely depend on that patch.
If API name, interface are changed, it would affect for pg_audit patch.
I'm not sure about progress of "deparsing utility command" patch but
you have any idea if that patch is not committed into 9.5 until May
15?

Currently the deparse dependent code is ifdef'd so pg_audit compiles and
operates just fine against master. Having the deparse code is nice
because it allows less common object types to log with full-qualified
names but it is in not a requirement for pg_audit.

I'd like to see at least patch 0001 of deparse committed. Not only
because it provides the functionality that deparse uses, but because it
makes event triggers truly useful in pl/pgsql.

--
- David Steele
david@pgmasters.net

#51Peter Eisentraut
peter_e@gmx.net
In reply to: Fujii Masao (#47)
Re: Auditing extension for PostgreSQL (Take 2)

On 4/30/15 6:05 AM, Fujii Masao wrote:

The specification of "session audit logging" seems to be still unclear to me.

As I had mentioned previously, I would prefer session audit logging to
be integrated with the normal statement logging configuration.

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

#52Stephen Frost
sfrost@snowman.net
In reply to: Peter Eisentraut (#51)
Re: Auditing extension for PostgreSQL (Take 2)

* Peter Eisentraut (peter_e@gmx.net) wrote:

On 4/30/15 6:05 AM, Fujii Masao wrote:

The specification of "session audit logging" seems to be still unclear to me.

As I had mentioned previously, I would prefer session audit logging to
be integrated with the normal statement logging configuration.

I'd certainly love to see the logging in core be improved, but that's
also rather tangential to this extension and we could certainly have
both quite happily. Further, being able to configure and have
consistent information from the extension is valuable even if options
are added to the in-core logging to make it more flexible.

One particular advantage of having the extension is that having it
doesn't impact existing users of the in-core logging system. I don't
recall any specific proposals for improving the in-core logging system
to add the capabilities for session logging that the extension
provides, but it seems likely to require changes to at least the CSV
format, new log_line_prefix escape codes (which we're using quite a
few of already...), new GUCs, and potentially behavior changes to make
it work. As I say above, I'm happy to have those discussions (and
I've been party to them quite a few times in the past...), but it
seems unlikely to seriously reduce the usefulness of session logging
being in the extension and producing consistent output with the object
logging.

Thanks!

Stephen

#53Peter Eisentraut
peter_e@gmx.net
In reply to: Stephen Frost (#52)
Re: Auditing extension for PostgreSQL (Take 2)

On 5/4/15 3:00 PM, Stephen Frost wrote:

One particular advantage of having the extension is that having it
doesn't impact existing users of the in-core logging system. I don't
recall any specific proposals for improving the in-core logging system
to add the capabilities for session logging that the extension
provides, but it seems likely to require changes to at least the CSV
format, new log_line_prefix escape codes (which we're using quite a
few of already...), new GUCs, and potentially behavior changes to make
it work. As I say above, I'm happy to have those discussions (and
I've been party to them quite a few times in the past...),

Well yeah, from my perspective, the reason not much has happened in the
area of logging is that you and Magnus have repeatedly said you were
planning some things.

The reasons given above against changing logging just as easily apply to
auditing. This implementation is only a starting point. We don't know
whether it will fulfill anyone's requirements. The requirements for
logging are "it feels good enough for an admin". The requirements for
auditing are "it satisfies this checklist". We need to be prepared to
aggressively evolve this as we gather more information about
requirements. Otherwise this will become something like contrib/isn,
where we know it doesn't satisfy real-world uses anymore, but we're
afraid to touch it because someone might be using it and we don't have
the domain knowledge to tell them to stop.

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

#54Stephen Frost
sfrost@snowman.net
In reply to: Peter Eisentraut (#53)
Re: Auditing extension for PostgreSQL (Take 2)

* Peter Eisentraut (peter_e@gmx.net) wrote:

On 5/4/15 3:00 PM, Stephen Frost wrote:

One particular advantage of having the extension is that having it
doesn't impact existing users of the in-core logging system. I don't
recall any specific proposals for improving the in-core logging system
to add the capabilities for session logging that the extension
provides, but it seems likely to require changes to at least the CSV
format, new log_line_prefix escape codes (which we're using quite a
few of already...), new GUCs, and potentially behavior changes to make
it work. As I say above, I'm happy to have those discussions (and
I've been party to them quite a few times in the past...),

Well yeah, from my perspective, the reason not much has happened in the
area of logging is that you and Magnus have repeatedly said you were
planning some things.

If that was holding anyone back from working on it, then I'm truely
sorry. I would encourage anyone interesting in any particular feature
to speak up and ask what the status is and if others are working on
something, especially if they have time to spend advancing it. I was
certainly quite happy when Abhijit posted the initial version of this
nearly a year ago as it showed that there were individuals able to spend
substantial time on it, as well as a use-case which would be solved
through such an extension.

I don't wish to lay claim to any particular feature nor to make any
guarantees, but I will say that I'm happy to have moved into a position
in the past year where I can devote a great deal more in time and
resources towards PostgreSQL than I've ever been able to in the past.

The reasons given above against changing logging just as easily apply to
auditing.

I don't follow this logic. The concerns raised above are about changing
our in-core logging. We haven't got in-core auditing and so I don't see
how they apply to it.

This implementation is only a starting point. We don't know
whether it will fulfill anyone's requirements. The requirements for
logging are "it feels good enough for an admin". The requirements for
auditing are "it satisfies this checklist". We need to be prepared to
aggressively evolve this as we gather more information about
requirements. Otherwise this will become something like contrib/isn,
where we know it doesn't satisfy real-world uses anymore, but we're
afraid to touch it because someone might be using it and we don't have
the domain knowledge to tell them to stop.

I agree that this is a starting point. From the discussions which I've
had with PostgreSQL users (both our clients and others), this does
fulfill a set of requirements for them. That isn't to say that it's a
complete and total solution, nor that we will stop here (we certainly
won't!), but I'm confident it *is* solving a real use-case for our
users. I don't mean to speak for them, but my understanding is that the
work which was done by Abhijit and 2ndQ was sponsored by an EU grant
which had a specific set of requirements which this is intended to
satisfy.

Further, we are absolutely prepared to aggressively evolve this as we
learn and understand how it's being used out in the field- but I don't
believe we're ever going to really understand that until we put
something out there.

Thanks!

Stephen

#55Peter Eisentraut
peter_e@gmx.net
In reply to: Stephen Frost (#54)
Re: Auditing extension for PostgreSQL (Take 2)

On 5/4/15 8:37 PM, Stephen Frost wrote:

I don't follow this logic. The concerns raised above are about changing
our in-core logging. We haven't got in-core auditing and so I don't see
how they apply to it.

How is session "auditing" substantially different from statement logging?

I think it is not, and we could tweak the logging facilities a bit to
satisfy the audit trail case while making often-requested enhancement to
the traditional logging use case as well at the same time.

At least no one has disputed that yet. The only argument against has
been that they don't want to touch the logging.

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

#56Stephen Frost
sfrost@snowman.net
In reply to: Peter Eisentraut (#55)
Re: Auditing extension for PostgreSQL (Take 2)

* Peter Eisentraut (peter_e@gmx.net) wrote:

On 5/4/15 8:37 PM, Stephen Frost wrote:

I don't follow this logic. The concerns raised above are about changing
our in-core logging. We haven't got in-core auditing and so I don't see
how they apply to it.

How is session "auditing" substantially different from statement logging?

David had previously outlined the technical differences between the
statement logging we have today and what pgAudit does, but I gather
you're asking about it definitionally, though it ends up amounting to
much the same to me. Auditing is about "what happened" whereas
statement logging is "log whatever statement the user sent." pgAudit
bears this out by logging internal SQL statements and object
information, unlike what we do with statement logging today.

I think it is not, and we could tweak the logging facilities a bit to
satisfy the audit trail case while making often-requested enhancement to
the traditional logging use case as well at the same time.

Changing our existing statement logging to be a "what happened" auditing
system is possible, but I don't see it as a "tweak."

At least no one has disputed that yet. The only argument against has
been that they don't want to touch the logging.

I'm afraid we've been talking past each other here- I'm fully on-board
with enhancing our in-core logging capabilities and even looking to the
future at having object auditing included in core. It's not my intent
to dispute that or to argue against it.

Perhaps I've misunderstood the thrust of this sub-thread, so let me
explain what I thought the discussion was. My understanding was that
you were concerned about having session auditing included in pgAudit
and, further, that you wanted to see our in-core statement logging be
improved. I agree that we want to improve the in-core statement logging
and, ideally, have an in-core auditing solution in the future. I was
attempting to address the concern about having session logging in
pgAudit by pointing out that it's valuable to have even if our in-core
statement logging is augmented, and further, having it in pgAudit does
not preclude or reduce our ability to improve the in-core statement
logging in the future; indeed, it's my hope that we'll get good feedback
from users of pgAudit which could guide our in-core implementation. As
for the concern that pgAudit may end up "rotting" in the tree as some
other contrib modules have, I can say with confidence that we will have
users of it just as soon as they're able to move to a version of PG
which includes it and therefore will be supporting it and addressing
issues as we discover them, as I suspect the others who have been
involved in this discussion will be also. Additionally, as discussed
last summer, we can provide a migration path (which does not need to be
automated or even feature compatible) from pgAudit to an in-core
solution and then sunset pgAudit.

Building an in-core solution, in my estimation at least, is going to
require at least a couple of release cycles and having the feedback from
users of pgAudit will be very valuable to building a good solution, but
I don't believe we'll get that feedback without including it.

Lastly, from the perspective of the session logging piece, the code
footprint for that in pgAudit isn't the bulk or even terribly
significant, most of the code would still be required for the object
auditing capability.

Thanks!

Stephen

#57David Steele
david@pgmasters.net
In reply to: Stephen Frost (#56)
Re: Auditing extension for PostgreSQL (Take 2)

On 5/7/15 8:26 AM, Stephen Frost wrote:

* Peter Eisentraut (peter_e@gmx.net) wrote:

On 5/4/15 8:37 PM, Stephen Frost wrote:

I don't follow this logic. The concerns raised above are about changing
our in-core logging. We haven't got in-core auditing and so I don't see
how they apply to it.

How is session "auditing" substantially different from statement logging?

David had previously outlined the technical differences between the
statement logging we have today and what pgAudit does, but I gather
you're asking about it definitionally, though it ends up amounting to
much the same to me. Auditing is about "what happened" whereas
statement logging is "log whatever statement the user sent." pgAudit
bears this out by logging internal SQL statements and object
information, unlike what we do with statement logging today.

That's a great way to describe it and I'll see if I can incorporate this
idea into the docs to hopefully make the topic a bit clearer.

I think it is not, and we could tweak the logging facilities a bit to
satisfy the audit trail case while making often-requested enhancement to
the traditional logging use case as well at the same time.

Changing our existing statement logging to be a "what happened" auditing
system is possible, but I don't see it as a "tweak."

Agreed - not the least of which would be figuring out a more detailed
statement classification system for core which would probably be the
first step.

Lastly, from the perspective of the session logging piece, the code
footprint for that in pgAudit isn't the bulk or even terribly
significant, most of the code would still be required for the object
auditing capability.

Both have a decent footprint but also a lot in common so it's fair to
say that removing session audit logging would not reduce the amount of
code significantly.

--
- David Steele
david@pgmasters.net

#58Bruce Momjian
bruce@momjian.us
In reply to: Stephen Frost (#56)
Re: Auditing extension for PostgreSQL (Take 2)

On Thu, May 7, 2015 at 10:26:55AM -0400, Stephen Frost wrote:

* Peter Eisentraut (peter_e@gmx.net) wrote:

On 5/4/15 8:37 PM, Stephen Frost wrote:

I don't follow this logic. The concerns raised above are about changing
our in-core logging. We haven't got in-core auditing and so I don't see
how they apply to it.

How is session "auditing" substantially different from statement logging?

David had previously outlined the technical differences between the
statement logging we have today and what pgAudit does, but I gather
you're asking about it definitionally, though it ends up amounting to
much the same to me. Auditing is about "what happened" whereas
statement logging is "log whatever statement the user sent." pgAudit
bears this out by logging internal SQL statements and object
information, unlike what we do with statement logging today.

Well, what I was looking for is how auditing is _conceptually_ different
from logging, e.g. I can clearly explain how authentication (prove who
you are) and authorization (what you are allowed to do) are different.
Your definition above seems to be more behavioral, e.g. what arrived vs.
what happened. It is not clear to me why reporting such information is
conceptually different and requires different infrastructure, i.e. we
could not easily combine authentication and authorization into the same
infrastructure, but logging and auditing seems similar. (Actually,
pg_hba.conf contains authentication and some course-grained
authorization only because it is a good place to do low-overhead
authorization; there was a performance requirement to do it in
pg_hba.conf to prevent DOS attacks.)

At least no one has disputed that yet. The only argument against has
been that they don't want to touch the logging.

I'm afraid we've been talking past each other here- I'm fully on-board
with enhancing our in-core logging capabilities and even looking to the
future at having object auditing included in core. It's not my intent
to dispute that or to argue against it.

Perhaps I've misunderstood the thrust of this sub-thread, so let me
explain what I thought the discussion was. My understanding was that
you were concerned about having session auditing included in pgAudit
and, further, that you wanted to see our in-core statement logging be
improved. I agree that we want to improve the in-core statement logging
and, ideally, have an in-core auditing solution in the future. I was
attempting to address the concern about having session logging in
pgAudit by pointing out that it's valuable to have even if our in-core
statement logging is augmented, and further, having it in pgAudit does
not preclude or reduce our ability to improve the in-core statement
logging in the future; indeed, it's my hope that we'll get good feedback
from users of pgAudit which could guide our in-core implementation. As

What is our history of doing things in contrib because we are not sure
what we want, then moving it into core? My general recollection is that
there is usually something in the contrib version we don't want to add
to core and people are locked into the contrib API, so we are left
supporting it, e.g. xml2, though you could argue that auditing doesn't
have application lock-in and xml2 was tied to an external library
feature.

for the concern that pgAudit may end up "rotting" in the tree as some
other contrib modules have, I can say with confidence that we will have
users of it just as soon as they're able to move to a version of PG
which includes it and therefore will be supporting it and addressing
issues as we discover them, as I suspect the others who have been

Uh, why are they not using the PGXN version of pg_audit, and if it is
because it isn't shipped with Postgres, then these seem like unmotivated
users who will complain for some reason if we ever move it out of
contrib.

I guess the over-arching question is whether we have to put this into
contrib so we can get feedback and change the API, or whether using from
PGXN or incrementally adding it to core is the right approach.

involved in this discussion will be also. Additionally, as discussed
last summer, we can provide a migration path (which does not need to be
automated or even feature compatible) from pgAudit to an in-core
solution and then sunset pgAudit.

Uh, that usually ends badly too.

Building an in-core solution, in my estimation at least, is going to
require at least a couple of release cycles and having the feedback from
users of pgAudit will be very valuable to building a good solution, but
I don't believe we'll get that feedback without including it.

See above --- is it jump through the user hoops and only then they will
use it and give us feedback? How motivated can they be if they can't
use the PGXN version?

The bottom line is that for the _years_ we ship pg_audit in /contrib, we
will have some logging stuff in postgresql.conf and some in
contrib/pg_audit and that distinction is going to look quite odd. To
the extent you incrementally add to core, you will have duplicate
functionality in both places.

--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com

+ Everyone has their own god. +

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

#59Stephen Frost
sfrost@snowman.net
In reply to: Bruce Momjian (#58)
Re: Auditing extension for PostgreSQL (Take 2)

Bruce,

* Bruce Momjian (bruce@momjian.us) wrote:

On Thu, May 7, 2015 at 10:26:55AM -0400, Stephen Frost wrote:

* Peter Eisentraut (peter_e@gmx.net) wrote:

On 5/4/15 8:37 PM, Stephen Frost wrote:

I don't follow this logic. The concerns raised above are about changing
our in-core logging. We haven't got in-core auditing and so I don't see
how they apply to it.

How is session "auditing" substantially different from statement logging?

David had previously outlined the technical differences between the
statement logging we have today and what pgAudit does, but I gather
you're asking about it definitionally, though it ends up amounting to
much the same to me. Auditing is about "what happened" whereas
statement logging is "log whatever statement the user sent." pgAudit
bears this out by logging internal SQL statements and object
information, unlike what we do with statement logging today.

Well, what I was looking for is how auditing is _conceptually_ different
from logging, e.g. I can clearly explain how authentication (prove who
you are) and authorization (what you are allowed to do) are different.

I'd say that auditing is one category of logging, just as
statement/session logging is another category of logging. What we
currently have in core is well defined and used extensively- but it's a
specific kind of logging which is statement logging, and that's all we
provide currently. That isn't to say that there isn't commonality,
there certainly is, but pgAudit leverages that to a large extent already
by using the logging infrastructure in the backend for everything which
is logged.

Your definition above seems to be more behavioral, e.g. what arrived vs.
what happened. It is not clear to me why reporting such information is
conceptually different and requires different infrastructure, i.e. we
could not easily combine authentication and authorization into the same
infrastructure, but logging and auditing seems similar.

Similar to logging, Authentication and Authorization also fall under a
more general category- access control.

At least no one has disputed that yet. The only argument against has
been that they don't want to touch the logging.

I'm afraid we've been talking past each other here- I'm fully on-board
with enhancing our in-core logging capabilities and even looking to the
future at having object auditing included in core. It's not my intent
to dispute that or to argue against it.

Perhaps I've misunderstood the thrust of this sub-thread, so let me
explain what I thought the discussion was. My understanding was that
you were concerned about having session auditing included in pgAudit
and, further, that you wanted to see our in-core statement logging be
improved. I agree that we want to improve the in-core statement logging
and, ideally, have an in-core auditing solution in the future. I was
attempting to address the concern about having session logging in
pgAudit by pointing out that it's valuable to have even if our in-core
statement logging is augmented, and further, having it in pgAudit does
not preclude or reduce our ability to improve the in-core statement
logging in the future; indeed, it's my hope that we'll get good feedback
from users of pgAudit which could guide our in-core implementation. As

What is our history of doing things in contrib because we are not sure
what we want, then moving it into core? My general recollection is that
there is usually something in the contrib version we don't want to add
to core and people are locked into the contrib API, so we are left
supporting it, e.g. xml2, though you could argue that auditing doesn't
have application lock-in and xml2 was tied to an external library
feature.

That's exactly the argument that I'd make there. My recollection is
that we did move pieces of hstore and have moved pieces of other contrib
modules into core; perhaps we've not yet had a case where we've
completely pulled one in, but given the relatively low level of
dependency associated with pgAudit, I'm certainly hopeful that we'll be
able to here. Lack of history which could be pointed to that's exactly
what I'm suggesting here doesn't seem like a reason to not move forward
here though; the concept of having a capability initially in contrib and
then bringing it into core has certainly been discussed a number of
times on other threads and generally makes sense, at least to me,
especially when there's little API associated with the extension.

for the concern that pgAudit may end up "rotting" in the tree as some
other contrib modules have, I can say with confidence that we will have
users of it just as soon as they're able to move to a version of PG
which includes it and therefore will be supporting it and addressing
issues as we discover them, as I suspect the others who have been

Uh, why are they not using the PGXN version of pg_audit, and if it is
because it isn't shipped with Postgres, then these seem like unmotivated
users who will complain for some reason if we ever move it out of
contrib.

Put bluntly, but I believe accurately, there's very few organizations
who have auditing requirements who want anything to do with PGXN.
That's an entirely understandable and defensible position, as there's
very little control in that environment (intentionally so, which is what
makes it great for some users but not acceptable for others). I'd
hardly call those users "unmotivated" considering that they've put forth
substantial resources towards pgAudit in the form of the EU grant which
started it over a year ago and the further efforts being made to bring
it to PG.

I guess the over-arching question is whether we have to put this into
contrib so we can get feedback and change the API, or whether using from
PGXN or incrementally adding it to core is the right approach.

I'm surprised to hear this question of if we "have to" do X, Y, or Z.
pgAudit brings a fantastic capability to PostgreSQL which users have
been asking to have for many years and is a feature we should be itching
to have included. That we can then take it and incrementally add it to
core, to leverage things which are only available in core (as discussed
last summer, including grammar and relation metadata), looks to me like
a great direction to go in and one which we could use over and over to
bring new features and capabilities to PG.

Lack of auditing is one of the capabilities that users coming from other
large RDBMS's see as preventing their ability to migrate to PostgreSQL.
Other databases (open and closed source) have it and have had it for
years and it's a serious shortcoming of ours that makes users either
stick with their existing vendor or look to other closed-source or even
open-source solutions.

involved in this discussion will be also. Additionally, as discussed
last summer, we can provide a migration path (which does not need to be
automated or even feature compatible) from pgAudit to an in-core
solution and then sunset pgAudit.

Uh, that usually ends badly too.

I'm confused by this, as it was the result of our discussion and your
suggestion from last summer: 20140730192136.GM2791@momjian.us

I certainly hope that hasn't substantially changed as that entire
discussion is why we're even able to have this discussion about
including pgAudit now. I was very much on-board with trying to work on
an in-core solution until that thread convinced me that the upgrade
concerns which I was worried about wouldn't be an issue for inclusion of
an extension to provide the capability.

Building an in-core solution, in my estimation at least, is going to
require at least a couple of release cycles and having the feedback from
users of pgAudit will be very valuable to building a good solution, but
I don't believe we'll get that feedback without including it.

See above --- is it jump through the user hoops and only then they will
use it and give us feedback? How motivated can they be if they can't
use the PGXN version?

Why wouldn't we want to include this capability in PG? I also addressed
the "why not PGXN" above. It it not a lack of motivation but the entire
intent and design of the PGXN system which precludes most large
organizations from using it, particularly for sensitive requirements
such as auditing.

The bottom line is that for the _years_ we ship pg_audit in /contrib, we
will have some logging stuff in postgresql.conf and some in
contrib/pg_audit and that distinction is going to look quite odd. To
the extent you incrementally add to core, you will have duplicate
functionality in both places.

That's entirely correct, of course, but I'm not seeing it as an issue.
I'm certainly prepared to support shipping pgAudit in contrib, as are
others based on how this feature has been developed, for the years that
we'll have 9.5, 9.6 (or 10.0, etc) supported- and that's also another
reason why users will use it when they wouldn't use something on PGXN.

Further, I look forward to working incrementally to bring similar
capability into core, but I suspect those increments will largely be in
the infrastructure until we reach the point where we're able to provide
the user-facing bits, which is quite likely to go in all at once and
allow us a clear upgrade path from one to the other. Perhaps that's
optimistic, but we do tend to try and bring things in as whole
capabilities rather than bits and pieces and I don't expect us to need
to do it differently here.

Thanks!

Stephen

#60Peter Eisentraut
peter_e@gmx.net
In reply to: Stephen Frost (#56)
Re: Auditing extension for PostgreSQL (Take 2)

On 5/7/15 10:26 AM, Stephen Frost wrote:

Auditing is about "what happened" whereas
statement logging is "log whatever statement the user sent." pgAudit
bears this out by logging internal SQL statements and object
information, unlike what we do with statement logging today.

I don't think this is quite correct. For example,
log_min_duration_statement logs based on what happened. log_duration
records what happened. log_checkpoints records what happened.
log_statement also requires parsing before deciding whether to log.

Generally, "logging" is "what happened". The stuff in syslog is what
happened on the system.

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

#61Stephen Frost
sfrost@snowman.net
In reply to: Peter Eisentraut (#60)
Re: Auditing extension for PostgreSQL (Take 2)

Peter,

* Peter Eisentraut (peter_e@gmx.net) wrote:

On 5/7/15 10:26 AM, Stephen Frost wrote:

Auditing is about "what happened" whereas
statement logging is "log whatever statement the user sent." pgAudit
bears this out by logging internal SQL statements and object
information, unlike what we do with statement logging today.

I don't think this is quite correct. For example,
log_min_duration_statement logs based on what happened. log_duration
records what happened. log_checkpoints records what happened.
log_statement also requires parsing before deciding whether to log.

I'm not sure I agree, but it seems a relatively minor point (please
correct me if you feel differently). You're certainly correct that
log_min_duration_statement allows filtering of the statement logging
based on what happened, but it's still statement logging. The other log
options are more in-line with "what happened" kind of logging, but they
also aren't user activity, so perhaps rephrasing my statement along the
lines of "what happened based on user activity" would make more sense.
On the other hand, log_checkpoints isn't "statement" or "session"
logging, which is what we had been discussing, I thought.

I agree that log_duration is more in-line with "what happened".

Generally, "logging" is "what happened". The stuff in syslog is what
happened on the system.

Agreed, but I had thought we were primairly focusing on session /
statement logging, which is the potential overlap in capability being
discussed as related to pgAudit (I don't expect pgAudit to ever include
checkpoint logging, for example). My email to Bruce, I believe,
clarifies how I've been thinking about statement/session logging and the
more general category of "logging" (which auditing certainly falls under
also, as "audit logging").

Thanks!

Stephen

#62Bruce Momjian
bruce@momjian.us
In reply to: Stephen Frost (#59)
Re: Auditing extension for PostgreSQL (Take 2)

On Thu, May 7, 2015 at 03:41:13PM -0400, Stephen Frost wrote:

Bruce,

What is our history of doing things in contrib because we are not sure
what we want, then moving it into core? My general recollection is that
there is usually something in the contrib version we don't want to add
to core and people are locked into the contrib API, so we are left
supporting it, e.g. xml2, though you could argue that auditing doesn't
have application lock-in and xml2 was tied to an external library
feature.

That's exactly the argument that I'd make there. My recollection is
that we did move pieces of hstore and have moved pieces of other contrib
modules into core; perhaps we've not yet had a case where we've
completely pulled one in, but given the relatively low level of
dependency associated with pgAudit, I'm certainly hopeful that we'll be
able to here. Lack of history which could be pointed to that's exactly
what I'm suggesting here doesn't seem like a reason to not move forward
here though; the concept of having a capability initially in contrib and
then bringing it into core has certainly been discussed a number of
times on other threads and generally makes sense, at least to me,
especially when there's little API associated with the extension.

OK, I am just asking so we remember this can go badly, not that it will.

I guess the over-arching question is whether we have to put this into
contrib so we can get feedback and change the API, or whether using from
PGXN or incrementally adding it to core is the right approach.

I'm surprised to hear this question of if we "have to" do X, Y, or Z.
pgAudit brings a fantastic capability to PostgreSQL which users have
been asking to have for many years and is a feature we should be itching
to have included. That we can then take it and incrementally add it to
core, to leverage things which are only available in core (as discussed
last summer, including grammar and relation metadata), looks to me like
a great direction to go in and one which we could use over and over to
bring new features and capabilities to PG.

Lack of auditing is one of the capabilities that users coming from other
large RDBMS's see as preventing their ability to migrate to PostgreSQL.
Other databases (open and closed source) have it and have had it for
years and it's a serious shortcoming of ours that makes users either
stick with their existing vendor or look to other closed-source or even
open-source solutions.

Yes, more extensive auditing is definitely needed.

involved in this discussion will be also. Additionally, as discussed
last summer, we can provide a migration path (which does not need to be
automated or even feature compatible) from pgAudit to an in-core
solution and then sunset pgAudit.

Uh, that usually ends badly too.

I'm confused by this, as it was the result of our discussion and your
suggestion from last summer: 20140730192136.GM2791@momjian.us

I certainly hope that hasn't substantially changed as that entire
discussion is why we're even able to have this discussion about
including pgAudit now. I was very much on-board with trying to work on
an in-core solution until that thread convinced me that the upgrade
concerns which I was worried about wouldn't be an issue for inclusion of
an extension to provide the capability.

I had forgotten about that. Yes, pg_upgrade can easily do this.

Building an in-core solution, in my estimation at least, is going to
require at least a couple of release cycles and having the feedback from
users of pgAudit will be very valuable to building a good solution, but
I don't believe we'll get that feedback without including it.

See above --- is it jump through the user hoops and only then they will
use it and give us feedback? How motivated can they be if they can't
use the PGXN version?

Why wouldn't we want to include this capability in PG? I also addressed
the "why not PGXN" above. It it not a lack of motivation but the entire
intent and design of the PGXN system which precludes most large
organizations from using it, particularly for sensitive requirements
such as auditing.

So they trust the Postgres, but not the PGXN authors --- I guess legally
that makes sense.

The bottom line is that for the _years_ we ship pg_audit in /contrib, we
will have some logging stuff in postgresql.conf and some in
contrib/pg_audit and that distinction is going to look quite odd. To
the extent you incrementally add to core, you will have duplicate
functionality in both places.

That's entirely correct, of course, but I'm not seeing it as an issue.
I'm certainly prepared to support shipping pgAudit in contrib, as are
others based on how this feature has been developed, for the years that
we'll have 9.5, 9.6 (or 10.0, etc) supported- and that's also another
reason why users will use it when they wouldn't use something on PGXN.

Further, I look forward to working incrementally to bring similar
capability into core, but I suspect those increments will largely be in
the infrastructure until we reach the point where we're able to provide
the user-facing bits, which is quite likely to go in all at once and
allow us a clear upgrade path from one to the other. Perhaps that's
optimistic, but we do tend to try and bring things in as whole
capabilities rather than bits and pieces and I don't expect us to need
to do it differently here.

OK, I just felt I had to ask those questions so we know where the
pitfalls are --- over-optimism always sets of alarms for me.

--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com

+ Everyone has their own god. +

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

#63David Steele
david@pgmasters.net
In reply to: Sawada Masahiko (#49)
1 attachment(s)
Re: Auditing extension for PostgreSQL (Take 2)

On 5/1/15 5:58 AM, Sawada Masahiko wrote:

For now, since pg_audit patch has a pg_audit_ddl_command_end()
function which uses part of un-committed "deparsing utility commands"
patch API,
pg_audit patch is completely depend on that patch.
If API name, interface are changed, it would affect for pg_audit patch.
I'm not sure about progress of "deparsing utility command" patch but
you have any idea if that patch is not committed into 9.5 until May
15?

The attached v12 patch removes the code that became redundant with
Alvaro committing the event trigger/deparse work. I've updated the
regression tests to reflect the changes, which were fairly minor and add
additional information to the output. There are no longer any #ifdef'd
code blocks.

--
- David Steele
david@pgmasters.net

Attachments:

pg_audit-v12.patchtext/plain; charset=UTF-8; name=pg_audit-v12.patch; x-mac-creator=0; x-mac-type=0Download
diff --git a/contrib/Makefile b/contrib/Makefile
index f84e684..1f3d3f1 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -28,6 +28,7 @@ SUBDIRS = \
 		oid2name	\
 		pageinspect	\
 		passwordcheck	\
+		pg_audit	\
 		pg_buffercache	\
 		pg_freespacemap \
 		pg_prewarm	\
diff --git a/contrib/pg_audit/.gitignore b/contrib/pg_audit/.gitignore
new file mode 100644
index 0000000..a5267cf
--- /dev/null
+++ b/contrib/pg_audit/.gitignore
@@ -0,0 +1,5 @@
+log/
+results/
+tmp_check/
+regression.diffs
+regression.out
diff --git a/contrib/pg_audit/Makefile b/contrib/pg_audit/Makefile
new file mode 100644
index 0000000..7b36011
--- /dev/null
+++ b/contrib/pg_audit/Makefile
@@ -0,0 +1,21 @@
+# pg_audit/Makefile
+
+MODULE = pg_audit
+MODULE_big = pg_audit
+OBJS = pg_audit.o
+
+EXTENSION = pg_audit
+REGRESS = pg_audit
+REGRESS_OPTS = --temp-config=$(top_srcdir)/contrib/pg_audit/pg_audit.conf
+DATA = pg_audit--1.0.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/pg_audit
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/pg_audit/expected/pg_audit.out b/contrib/pg_audit/expected/pg_audit.out
new file mode 100644
index 0000000..80e3a79
--- /dev/null
+++ b/contrib/pg_audit/expected/pg_audit.out
@@ -0,0 +1,998 @@
+-- Load pg_audit module
+create extension pg_audit;
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+ALTER ROLE super SET pg_audit.log = 'Role';
+ALTER ROLE super SET pg_audit.log_notice = ON;
+\connect contrib_regression super;
+--
+-- Create auditor role
+CREATE ROLE auditor;
+NOTICE:  AUDIT: SESSION,1,1,ROLE,CREATE ROLE,,,CREATE ROLE auditor;
+--
+-- Create first test user
+CREATE USER user1;
+NOTICE:  AUDIT: SESSION,2,1,ROLE,CREATE ROLE,,,CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+NOTICE:  AUDIT: SESSION,3,1,ROLE,ALTER ROLE,,,"ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';"
+ALTER ROLE user1 SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,4,1,ROLE,ALTER ROLE,,,ALTER ROLE user1 SET pg_audit.log_notice = ON;
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.test,CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+ id 
+----
+(0 rows)
+
+DROP TABLE test;
+NOTICE:  AUDIT: SESSION,2,1,DDL,DROP TABLE,TABLE,public.test,DROP TABLE test;
+--
+-- Create second test user
+\connect contrib_regression super
+CREATE USER user2;
+NOTICE:  AUDIT: SESSION,1,1,ROLE,CREATE ROLE,,,CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+NOTICE:  AUDIT: SESSION,2,1,ROLE,ALTER ROLE,,,"ALTER ROLE user2 SET pg_audit.log = 'Read, writE';"
+ALTER ROLE user2 SET pg_audit.log_catalog = OFF;
+NOTICE:  AUDIT: SESSION,3,1,ROLE,ALTER ROLE,,,ALTER ROLE user2 SET pg_audit.log_catalog = OFF;
+ALTER ROLE user2 SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,4,1,ROLE,ALTER ROLE,,,ALTER ROLE user2 SET pg_audit.log_notice = ON;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+NOTICE:  AUDIT: SESSION,5,1,ROLE,ALTER ROLE,,,ALTER ROLE user2 SET pg_audit.role = auditor;
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+ count 
+-------
+     1
+(1 row)
+
+SELECT *
+  FROM test3, test2;
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,,,"SELECT *
+  FROM test3, test2;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.test2,"SELECT *
+  FROM vw_test3, test2;"
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,VIEW,public.vw_test3,"SELECT *
+  FROM vw_test3, test2;"
+ id | id 
+----+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,3,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.test2,"WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,4,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;"
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+NOTICE:  AUDIT: SESSION,5,1,WRITE,INSERT,,,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,INSERT,TABLE,public.test3,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.test2,"WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;"
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+NOTICE:  AUDIT: SESSION,6,1,WRITE,UPDATE,,,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+NOTICE:  AUDIT: OBJECT,6,1,WRITE,INSERT,TABLE,public.test2,"WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;"
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+NOTICE:  AUDIT: SESSION,1,1,ROLE,ALTER ROLE,,,alter role user2 set pg_audit.log = 'NONE';
+\connect contrib_regression user2
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+ id 
+----
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.test4,"SELECT name
+  FROM public.test4;"
+ name 
+------
+(0 rows)
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+				  VALUES (1);
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+				  VALUES ('test');
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,INSERT,TABLE,public.test4,"INSERT INTO public.test4 (name)
+				  VALUES ('test');"
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+NOTICE:  AUDIT: OBJECT,3,1,WRITE,UPDATE,TABLE,public.test4,"UPDATE public.test4
+   SET id = 1;"
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.test4,update public.test4 set name = 'foo' where name = 'bar';
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+NOTICE:  AUDIT: SESSION,1,1,ROLE,ALTER ROLE,,,"alter role user1 set pg_audit.log = 'DDL, READ';"
+\connect contrib_regression user1
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+NOTICE:  AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,"CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);"
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,,,"SELECT *
+  FROM account;"
+ id | name | password | description 
+----+------+----------+-------------
+(0 rows)
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+NOTICE:  AUDIT: SESSION,1,1,ROLE,ALTER ROLE,,,alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+NOTICE:  AUDIT: SESSION,2,1,ROLE,ALTER ROLE,,,alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+ id | name  
+----+-------
+  1 | user1
+(1 row)
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH1
+(1 row)
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+NOTICE:  AUDIT: SESSION,1,1,ROLE,ALTER ROLE,,,alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+NOTICE:  AUDIT: SESSION,2,1,ROLE,ALTER ROLE,,,"alter role user1 set pg_audit.log = 'read, WRITE';"
+\connect contrib_regression user1
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+NOTICE:  AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account_role_map,"SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;"
+ password | role_id 
+----------+---------
+(0 rows)
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+NOTICE:  AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+NOTICE:  AUDIT: SESSION,2,1,READ,SELECT,TABLE,public.account,"SELECT password
+  FROM account;"
+ password 
+----------
+ HASH2
+(1 row)
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+NOTICE:  AUDIT: SESSION,3,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada';"
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,4,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';"
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+NOTICE:  AUDIT: OBJECT,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+NOTICE:  AUDIT: SESSION,5,1,WRITE,UPDATE,TABLE,public.account,"UPDATE account
+   SET password = 'HASH2';"
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+NOTICE:  AUDIT: SESSION,1,1,MISC,SET,,,SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+NOTICE:  AUDIT: SESSION,2,1,MISC,SET,,,SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+NOTICE:  AUDIT: SESSION,3,1,MISC,SET,,,SET pg_audit.log_relation = ON;
+SET pg_audit.log_parameter = ON;
+NOTICE:  AUDIT: SESSION,4,1,MISC,SET,,,SET pg_audit.log_parameter = ON;
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+NOTICE:  AUDIT: SESSION,5,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	raise notice 'test';
+END $$;"
+NOTICE:  test
+--
+-- Create test schema
+CREATE SCHEMA test;
+NOTICE:  AUDIT: SESSION,6,1,DDL,CREATE SCHEMA,SCHEMA,test,CREATE SCHEMA test;
+--
+-- Copy account to stdout
+COPY account TO stdout;
+NOTICE:  AUDIT: SESSION,7,1,READ,SELECT,TABLE,public.account,COPY account TO stdout;
+1	user1	HASH2	yada, yada
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+NOTICE:  AUDIT: SESSION,8,1,READ,SELECT,TABLE,public.account,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,8,1,WRITE,INSERT,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+NOTICE:  AUDIT: SESSION,8,2,DDL,CREATE TABLE AS,TABLE,test.account_copy,"CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;"
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+NOTICE:  AUDIT: SESSION,9,1,WRITE,INSERT,TABLE,test.account_copy,COPY test.account_copy from stdin;
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+NOTICE:  AUDIT: SESSION,10,1,READ,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,11,1,READ,SELECT,TABLE,public.account,"PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;",1
+NOTICE:  AUDIT: SESSION,11,2,READ,EXECUTE,,,EXECUTE pgclassstmt (1);
+ id | name  | password | description 
+----+-------+----------+-------------
+  1 | user1 | HASH2    | yada, yada
+(1 row)
+
+DEALLOCATE pgclassstmt;
+NOTICE:  AUDIT: SESSION,12,1,MISC,DEALLOCATE,,,DEALLOCATE pgclassstmt;
+--
+-- Test cursor
+BEGIN;
+NOTICE:  AUDIT: SESSION,13,1,MISC,BEGIN,,,BEGIN;
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+NOTICE:  AUDIT: SESSION,14,1,READ,SELECT,TABLE,pg_catalog.pg_class,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+NOTICE:  AUDIT: SESSION,14,2,READ,DECLARE CURSOR,,,"DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;"
+FETCH NEXT FROM ctest;
+NOTICE:  AUDIT: SESSION,15,1,MISC,FETCH,,,FETCH NEXT FROM ctest;
+ count 
+-------
+     1
+(1 row)
+
+CLOSE ctest;
+NOTICE:  AUDIT: SESSION,16,1,MISC,CLOSE CURSOR,,,CLOSE ctest;
+COMMIT;
+NOTICE:  AUDIT: SESSION,17,1,MISC,COMMIT,,,COMMIT;
+--
+-- Turn off log_catalog and pg_class will not be logged
+SET pg_audit.log_catalog = OFF;
+NOTICE:  AUDIT: SESSION,18,1,MISC,SET,,,SET pg_audit.log_catalog = OFF;
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+ count 
+-------
+     1
+(1 row)
+
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+NOTICE:  AUDIT: SESSION,19,1,DDL,CREATE TABLE,TABLE,test.test_insert,"CREATE TABLE test.test_insert
+(
+	id INT
+);"
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);
+NOTICE:  AUDIT: SESSION,20,1,WRITE,PREPARE,,,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);"
+EXECUTE pgclassstmt (1);
+NOTICE:  AUDIT: SESSION,21,1,WRITE,INSERT,TABLE,test.test_insert,"PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);",1
+NOTICE:  AUDIT: SESSION,21,2,WRITE,EXECUTE,,,EXECUTE pgclassstmt (1);
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+NOTICE:  AUDIT: SESSION,22,1,DDL,CREATE TABLE,TABLE,public.test,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+NOTICE:  AUDIT: SESSION,22,1,DDL,CREATE TABLE,INDEX,public.test_pkey,"CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);"
+--
+-- Check that analyze is logged
+ANALYZE test;
+NOTICE:  AUDIT: SESSION,23,1,MISC,ANALYZE,,,ANALYZE test;
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+NOTICE:  AUDIT: SESSION,24,1,ROLE,GRANT,TABLE,,"GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;"
+SELECT *
+  FROM test;
+NOTICE:  AUDIT: SESSION,25,1,READ,SELECT,TABLE,public.test,"SELECT *
+  FROM test;"
+ id | name | description 
+----+------+-------------
+(0 rows)
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+NOTICE:  AUDIT: SESSION,26,1,READ,SELECT,TABLE,public.test,"SELECT
+  FROM test;"
+--
+(0 rows)
+
+SELECT 1,
+	   current_user;
+NOTICE:  AUDIT: SESSION,27,1,READ,SELECT,,,"SELECT 1,
+	   current_user;"
+ ?column? | current_user 
+----------+--------------
+        1 | super
+(1 row)
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+NOTICE:  AUDIT: SESSION,28,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;"
+NOTICE:  AUDIT: SESSION,28,2,READ,SELECT,,,SELECT 1
+CONTEXT:  SQL statement "SELECT 1"
+PL/pgSQL function inline_code_block line 5 at SQL statement
+explain select 1;
+NOTICE:  AUDIT: SESSION,29,1,READ,SELECT,,,explain select 1;
+NOTICE:  AUDIT: SESSION,29,2,MISC,EXPLAIN,,,explain select 1;
+                QUERY PLAN                
+------------------------------------------
+ Result  (cost=0.00..0.01 rows=1 width=0)
+(1 row)
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+NOTICE:  AUDIT: SESSION,30,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (1);"
+INSERT INTO TEST (id)
+		  VALUES (2);
+NOTICE:  AUDIT: SESSION,31,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (2);"
+INSERT INTO TEST (id)
+		  VALUES (3);
+NOTICE:  AUDIT: SESSION,32,1,WRITE,INSERT,TABLE,public.test,"INSERT INTO TEST (id)
+		  VALUES (3);"
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;
+NOTICE:  AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;"
+NOTICE:  AUDIT: SESSION,33,2,READ,SELECT,TABLE,public.test,"SELECT id
+		  FROM test"
+CONTEXT:  PL/pgSQL function inline_code_block line 5 at FOR over SELECT rows
+NOTICE:  AUDIT: SESSION,33,3,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,33,4,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+NOTICE:  AUDIT: SESSION,33,5,WRITE,INSERT,TABLE,public.test,"INSERT INTO test (id)
+			 VALUES (result.id + 100)",,,
+CONTEXT:  SQL statement "INSERT INTO test (id)
+			 VALUES (result.id + 100)"
+PL/pgSQL function inline_code_block line 9 at SQL statement
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+NOTICE:  AUDIT: SESSION,34,1,FUNCTION,DO,,,"DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' (""weird name"" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;"
+NOTICE:  AUDIT: SESSION,34,2,DDL,CREATE TABLE,TABLE,public.do_table,"CREATE TABLE do_table (""weird name"" INT)"
+CONTEXT:  SQL statement "CREATE TABLE do_table ("weird name" INT)"
+PL/pgSQL function inline_code_block line 5 at EXECUTE statement
+NOTICE:  AUDIT: SESSION,34,3,DDL,DROP TABLE,TABLE,public.do_table,DROP table do_table
+CONTEXT:  SQL statement "DROP table do_table"
+PL/pgSQL function inline_code_block line 6 at EXECUTE statement
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+NOTICE:  AUDIT: SESSION,35,1,FUNCTION,DO,,,"DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;"
+ERROR:  schema "bogus" does not exist
+LINE 1: CREATE TABLE bogus.test_block
+                     ^
+QUERY:  CREATE TABLE bogus.test_block
+	(
+		id INT
+	)
+CONTEXT:  PL/pgSQL function inline_code_block line 3 at SQL statement
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE COLUMN,public.test.description,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+NOTICE:  AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test,"ALTER TABLE public.test
+	DROP COLUMN description ;"
+ALTER TABLE public.test
+	RENAME TO test2;
+NOTICE:  AUDIT: SESSION,37,1,DDL,ALTER TABLE,TABLE,public.test2,"ALTER TABLE public.test
+	RENAME TO test2;"
+ALTER TABLE public.test2
+	SET SCHEMA test;
+NOTICE:  AUDIT: SESSION,38,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE public.test2
+	SET SCHEMA test;"
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+NOTICE:  AUDIT: SESSION,39,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	ADD COLUMN description TEXT;"
+ALTER TABLE test.test2
+	DROP COLUMN description;
+NOTICE:  AUDIT: SESSION,40,1,DDL,ALTER TABLE,TABLE COLUMN,test.test2.description,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+NOTICE:  AUDIT: SESSION,40,1,DDL,ALTER TABLE,TABLE,test.test2,"ALTER TABLE test.test2
+	DROP COLUMN description;"
+DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,41,1,DDL,DROP TABLE,TABLE,test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,41,1,DDL,DROP TABLE,TABLE CONSTRAINT,test_pkey on test.test2,DROP TABLE test.test2;
+NOTICE:  AUDIT: SESSION,41,1,DDL,DROP TABLE,INDEX,test.test_pkey,DROP TABLE test.test2;
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+NOTICE:  AUDIT: SESSION,42,1,DDL,CREATE SCHEMA,SCHEMA,foo,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,42,1,DDL,CREATE SCHEMA,TABLE,foo.bar,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+NOTICE:  AUDIT: SESSION,42,1,DDL,CREATE SCHEMA,TABLE,foo.baz,"CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);"
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+NOTICE:  AUDIT: SESSION,43,1,DDL,CREATE FUNCTION,FUNCTION,"public.int_add(integer,integer)","CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;"
+SELECT int_add(1, 1);
+NOTICE:  AUDIT: SESSION,44,1,READ,SELECT,,,"SELECT int_add(1, 1);"
+NOTICE:  AUDIT: SESSION,44,2,FUNCTION,EXECUTE,FUNCTION,public.int_add,"SELECT int_add(1, 1);"
+ int_add 
+---------
+       2
+(1 row)
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+NOTICE:  AUDIT: SESSION,45,1,DDL,CREATE AGGREGATE,AGGREGATE,public.sum_test(integer),"CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');"
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+NOTICE:  AUDIT: SESSION,46,1,DDL,ALTER AGGREGATE,AGGREGATE,public.sum_test2(integer),ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+NOTICE:  AUDIT: SESSION,47,1,DDL,CREATE CONVERSION,CONVERSION,public.conversion_test,CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+NOTICE:  AUDIT: SESSION,48,1,DDL,ALTER CONVERSION,CONVERSION,public.conversion_test2,ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+NOTICE:  AUDIT: SESSION,49,1,DDL,CREATE DATABASE,,,CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,50,1,DDL,ALTER DATABASE,,,ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+NOTICE:  AUDIT: SESSION,51,1,DDL,DROP DATABASE,,,DROP DATABASE contrib_regression_pgaudit2;
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+NOTICE:  AUDIT: SESSION,52,1,DDL,CREATE TABLE,TABLE,public.hoge,"CREATE TABLE hoge
+(
+	id int
+);"
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+NOTICE:  AUDIT: SESSION,53,1,DDL,CREATE FUNCTION,FUNCTION,public.test(),"CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;"
+SELECT test();
+NOTICE:  AUDIT: SESSION,54,1,READ,SELECT,,,SELECT test();
+NOTICE:  AUDIT: SESSION,54,2,FUNCTION,EXECUTE,FUNCTION,public.test,SELECT test();
+NOTICE:  AUDIT: SESSION,54,3,READ,SELECT,TABLE,public.hoge,select * from hoge
+CONTEXT:  PL/pgSQL function test() line 6 at OPEN
+ test 
+------
+     
+(1 row)
+
+--
+-- Delete all rows then delete 1 row
+SET pg_audit.log = 'write';
+SET pg_audit.role = 'auditor';
+create table bar
+(
+	col int
+);
+grant delete
+   on bar
+   to auditor;
+insert into bar (col)
+		 values (1);
+NOTICE:  AUDIT: SESSION,55,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col)
+		 values (1);"
+delete from bar;
+NOTICE:  AUDIT: OBJECT,56,1,WRITE,DELETE,TABLE,public.bar,delete from bar;
+NOTICE:  AUDIT: SESSION,56,1,WRITE,DELETE,TABLE,public.bar,delete from bar;
+insert into bar (col)
+		 values (1);
+NOTICE:  AUDIT: SESSION,57,1,WRITE,INSERT,TABLE,public.bar,"insert into bar (col)
+		 values (1);"
+delete from bar
+ where col = 1;
+NOTICE:  AUDIT: OBJECT,58,1,WRITE,DELETE,TABLE,public.bar,"delete from bar
+ where col = 1;"
+NOTICE:  AUDIT: SESSION,58,1,WRITE,DELETE,TABLE,public.bar,"delete from bar
+ where col = 1;"
+drop table bar;
+--
+-- Grant roles to each other
+SET pg_audit.log = 'role';
+GRANT user1 TO user2;
+NOTICE:  AUDIT: SESSION,59,1,ROLE,GRANT ROLE,,,GRANT user1 TO user2;
+REVOKE user1 FROM user2;
+NOTICE:  AUDIT: SESSION,60,1,ROLE,REVOKE ROLE,,,REVOKE user1 FROM user2;
diff --git a/contrib/pg_audit/pg_audit--1.0.0.sql b/contrib/pg_audit/pg_audit--1.0.0.sql
new file mode 100644
index 0000000..9d9ee83
--- /dev/null
+++ b/contrib/pg_audit/pg_audit--1.0.0.sql
@@ -0,0 +1,22 @@
+/* pg_audit/pg_audit--1.0.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_audit" to load this file.\quit
+
+CREATE FUNCTION pg_audit_ddl_command_end()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_ddl_command_end';
+
+CREATE EVENT TRIGGER pg_audit_ddl_command_end
+	ON ddl_command_end
+	EXECUTE PROCEDURE pg_audit_ddl_command_end();
+
+CREATE FUNCTION pg_audit_sql_drop()
+	RETURNS event_trigger
+	LANGUAGE C
+	AS 'MODULE_PATHNAME', 'pg_audit_sql_drop';
+
+CREATE EVENT TRIGGER pg_audit_sql_drop
+	ON sql_drop
+	EXECUTE PROCEDURE pg_audit_sql_drop();
diff --git a/contrib/pg_audit/pg_audit.c b/contrib/pg_audit/pg_audit.c
new file mode 100644
index 0000000..11ddcd0
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.c
@@ -0,0 +1,1707 @@
+/*------------------------------------------------------------------------------
+ * pg_audit.c
+ *
+ * An auditing extension for PostgreSQL. Improves on standard statement logging
+ * by adding more logging classes, object level logging, and providing
+ * fully-qualified object names for all DML and many DDL statements (See
+ * pg_audit.sgml for details).
+ *
+ * Copyright (c) 2014-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  contrib/pg_audit/pg_audit.c
+ *------------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_class.h"
+#include "catalog/namespace.h"
+#include "commands/dbcommands.h"
+#include "catalog/pg_proc.h"
+#include "commands/event_trigger.h"
+#include "executor/executor.h"
+#include "executor/spi.h"
+#include "miscadmin.h"
+#include "libpq/auth.h"
+#include "nodes/nodes.h"
+#include "tcop/utility.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+PG_MODULE_MAGIC;
+
+void _PG_init(void);
+
+/*
+ * Event trigger prototypes
+ */
+Datum pg_audit_ddl_command_end(PG_FUNCTION_ARGS);
+Datum pg_audit_sql_drop(PG_FUNCTION_ARGS);
+
+PG_FUNCTION_INFO_V1(pg_audit_ddl_command_end);
+PG_FUNCTION_INFO_V1(pg_audit_sql_drop);
+
+/*
+ * auditRole is the string value of the pg_audit.role GUC, which contains the
+ * role for grant-based auditing.
+ */
+char *auditRole = NULL;
+
+/*
+ * auditLog is the string value of the pg_audit.log GUC, e.g. "read, write, ddl"
+ * (it's not used by the module but is required by DefineCustomStringVariable).
+ * Each token corresponds to a flag in enum LogClass below. We convert the list
+ * of tokens into a bitmap in auditLogBitmap for internal use.
+ */
+char *auditLog = NULL;
+static uint64 auditLogBitmap = 0;
+
+/*
+ * auditLogParameter controls whether parameters passed with the statement are
+ * included in the audit log.
+ */
+bool auditLogParameter = false;
+
+/*
+ * auditLogRelation controls whether all relations are logged for READ and
+ * WRITE classes during session logging.
+ */
+bool auditLogRelation = false;
+
+/*
+ * auditLogCatalog allows logging of relations in pg_catalog when a query
+ * contains *all* pg_catalog relations.
+ */
+bool auditLogCatalog = false;
+
+/*
+ * auditLogNotice raises a notice as well as logging via the standard facility.
+ * This is primarily for the benefit of testing.
+ */
+bool auditLogNotice = false;
+
+/*
+ * String constants for audit types - used when logging to distinguish session
+ * vs. object auditing.
+ */
+#define AUDIT_TYPE_OBJECT	"OBJECT"
+#define AUDIT_TYPE_SESSION	"SESSION"
+
+/*
+ * String constants for log classes - used when processing tokens in the
+ * pg_audit.log GUC.
+ */
+#define CLASS_DDL			"DDL"
+#define CLASS_FUNCTION		"FUNCTION"
+#define CLASS_MISC			"MISC"
+#define CLASS_READ			"READ"
+#define CLASS_ROLE			"ROLE"
+#define CLASS_WRITE			"WRITE"
+
+#define CLASS_ALL			"ALL"
+#define CLASS_NONE			"NONE"
+
+/* Log class enum used to represent bits in auditLogBitmap */
+enum LogClass
+{
+	LOG_NONE = 0,
+
+	/* DDL: CREATE/DROP/ALTER */
+	LOG_DDL = (1 << 1),
+
+	/* Function execution */
+	LOG_FUNCTION = (1 << 2),
+
+	/* Statements not covered by another class */
+	LOG_MISC = (1 << 3),
+
+	/* SELECT */
+	LOG_READ = (1 << 4),
+
+	/* GRANT, REVOKE, CREATE/ALTER/DROP ROLE */
+	LOG_ROLE = (1 << 5),
+
+	/* INSERT, UPDATE, DELETE, TRUNCATE */
+	LOG_WRITE = (1 << 6),
+
+	/* Absolutely everything */
+	LOG_ALL = ~(uint64)0
+};
+
+/* String constants for logging commands */
+#define COMMAND_DELETE		"DELETE"
+#define COMMAND_EXECUTE		"EXECUTE"
+#define COMMAND_INSERT		"INSERT"
+#define COMMAND_UPDATE		"UPDATE"
+#define COMMAND_SELECT		"SELECT"
+
+#define COMMAND_ALTER_ROLE		"ALTER ROLE"
+#define COMMAND_DROP_ROLE		"CREATE ROLE"
+#define COMMAND_CREATE_DATABASE	"CREATE DATABASE"
+#define COMMAND_ALTER_DATABASE	"ALTER DATABASE"
+#define COMMAND_DROP_DATABASE	"DROP DATABASE"
+
+#define COMMAND_UNKNOWN		"UNKNOWN"
+
+/* String constants for logging object types */
+#define OBJECT_TYPE_COMPOSITE_TYPE	"COMPOSITE TYPE"
+#define OBJECT_TYPE_FOREIGN_TABLE	"FOREIGN TABLE"
+#define OBJECT_TYPE_FUNCTION		"FUNCTION"
+#define OBJECT_TYPE_INDEX			"INDEX"
+#define OBJECT_TYPE_TABLE			"TABLE"
+#define OBJECT_TYPE_TOASTVALUE		"TOASTVALUE"
+#define OBJECT_TYPE_MATVIEW			"MATERIALIZED VIEW"
+#define OBJECT_TYPE_SEQUENCE		"SEQUENCE"
+#define OBJECT_TYPE_VIEW			"VIEW"
+
+#define OBJECT_TYPE_UNKNOWN			"UNKNOWN"
+
+/*
+ * An AuditEvent represents an operation that potentially affects a single
+ * object. If a statement affects multiple objects multiple AuditEvents must be
+ * created to represent it.
+ */
+typedef struct
+{
+	int64 statementId;
+	int64 substatementId;
+
+	LogStmtLevel logStmtLevel;
+	NodeTag commandTag;
+	const char *command;
+	const char *objectType;
+	char *objectName;
+	const char *commandText;
+	ParamListInfo paramList;
+
+	bool granted;
+	bool logged;
+} AuditEvent;
+
+/*
+ * A simple FIFO queue to keep track of the current stack of audit events.
+ */
+typedef struct AuditEventStackItem
+{
+	struct AuditEventStackItem *next;
+
+	AuditEvent auditEvent;
+
+	int64 stackId;
+
+	MemoryContext contextAudit;
+	MemoryContextCallback contextCallback;
+} AuditEventStackItem;
+
+AuditEventStackItem *auditEventStack = NULL;
+
+/*
+ * Track when an internal statement is running so it is not logged
+ */
+static bool internalStatement = false;
+
+/*
+ * Track running total for statements and substatements and whether or not
+ * anything has been logged since this statement began.
+ */
+static int64 statementTotal = 0;
+static int64 substatementTotal = 0;
+static int64 stackTotal = 0;
+
+static bool statementLogged = false;
+
+/*
+ * Stack functions
+ *
+ * Audit events can go down to multiple levels so a stack is maintained to keep
+ * track of them.
+ */
+
+/*
+ * Respond to callbacks registered with MemoryContextRegisterResetCallback().
+ * Removes the event(s) off the stack that have become obsolete once the
+ * MemoryContext has been freed.  The callback should always be freeing the top
+ * of the stack, but the code is tolerant of out-of-order callbacks.
+ */
+static void
+stack_free(void *stackFree)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item to be freed */
+		if (nextItem == (AuditEventStackItem *)stackFree)
+		{
+			/* Move top of stack to the item after the freed item */
+			auditEventStack = nextItem->next;
+
+			/* If the stack is not empty */
+			if (auditEventStack == NULL)
+			{
+				/* Reset internal statement in case of error */
+				internalStatement = false;
+
+				/* Reset sub statement total */
+				substatementTotal = 0;
+
+				/* Reset statement logged flag total */
+				statementLogged = false;
+			}
+
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+}
+
+/*
+ * Push a new audit event onto the stack and create a new memory context to
+ * store it.
+ */
+static AuditEventStackItem *
+stack_push()
+{
+	MemoryContext contextAudit;
+	MemoryContext contextOld;
+	AuditEventStackItem *stackItem;
+
+	/* Create a new memory context */
+	contextAudit = AllocSetContextCreate(CurrentMemoryContext,
+										 "pg_audit stack context",
+										 ALLOCSET_DEFAULT_MINSIZE,
+										 ALLOCSET_DEFAULT_INITSIZE,
+										 ALLOCSET_DEFAULT_MAXSIZE);
+	contextOld = MemoryContextSwitchTo(contextAudit);
+
+	/* Allocate the stack item */
+	stackItem = palloc0(sizeof(AuditEventStackItem));
+
+	/* Store memory contexts */
+	stackItem->contextAudit = contextAudit;
+
+	/* If item already on stack then push it down */
+	if (auditEventStack != NULL)
+		stackItem->next = auditEventStack;
+	else
+		stackItem->next = NULL;
+
+	/*
+	 * Create the unique stackId - used to keep the stack sane when memory
+	 * contexts are freed unexpectedly.
+	 */
+	stackItem->stackId = ++stackTotal;
+
+	/*
+	 * Setup a callback in case an error happens.  stack_free() will truncate
+	 * the stack at this item.
+	 */
+	stackItem->contextCallback.func = stack_free;
+	stackItem->contextCallback.arg = (void *)stackItem;
+	MemoryContextRegisterResetCallback(contextAudit,
+									   &stackItem->contextCallback);
+
+	/* Push item on the stack */
+	auditEventStack = stackItem;
+
+	/* Return to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+
+	/* Return the stack item */
+	return stackItem;
+}
+
+/*
+ * Pop an audit event from the stack by deleting the memory context that
+ * contains it.  The callback to stack_free() does the actual pop.
+ */
+static void
+stack_pop(int64 stackId)
+{
+	/* Make sure what we want to delete is at the top of the stack */
+	if (auditEventStack != NULL && auditEventStack->stackId == stackId)
+	{
+		MemoryContextDelete(auditEventStack->contextAudit);
+	}
+	else
+	{
+		elog(ERROR, "pg_audit stack item %ld not found on top - cannot pop",
+					stackId);
+	}
+}
+
+/*
+ * Check that an item is on the stack.  If not an error will be raised since
+ * this is a bad state to be in, and it might mean audit records are being
+ * lost.
+ */
+static void
+stack_valid(int64 stackId)
+{
+	AuditEventStackItem *nextItem = auditEventStack;
+
+	if (nextItem == NULL)
+	{
+		elog(ERROR, "pg_audit stack item %ld not found - stack is empty",
+					stackId);
+	}
+
+	/* Only process if the stack contains items */
+	while (nextItem != NULL)
+	{
+		/* Check if this item matches the item searched for */
+		if (nextItem->stackId == stackId)
+		{
+			return;
+		}
+
+		/* Still looking, test the next item */
+		nextItem = nextItem->next;
+	}
+
+	elog(ERROR, "pg_audit stack item %ld not found - top of stack is %ld",
+				stackId, auditEventStack->stackId);
+}
+
+/*
+ * Appends a properly quoted CSV field to StringInfo.
+ */
+static void
+append_valid_csv(StringInfoData *buffer, const char *appendStr)
+{
+	const char *pChar;
+
+	/*
+	 * If the append string is null then return.  NULL fields are not quoted
+	 * in CSV
+	 */
+	if (appendStr == NULL)
+		return;
+
+	/* Only format for CSV if appendStr contains: ", comma, \n, \r */
+	if (strstr(appendStr, ",") || strstr(appendStr, "\"") ||
+		strstr(appendStr, "\n") || strstr(appendStr, "\r"))
+	{
+		appendStringInfoCharMacro(buffer, '"');
+
+		for (pChar = appendStr; *pChar; pChar++)
+		{
+			if (*pChar == '"') /* double single quotes */
+				appendStringInfoCharMacro(buffer, *pChar);
+
+			appendStringInfoCharMacro(buffer, *pChar);
+		}
+
+		appendStringInfoCharMacro(buffer, '"');
+	}
+	/* Else just append */
+	else
+	{
+		appendStringInfoString(buffer, appendStr);
+	}
+}
+
+/*
+ * Takes an AuditEvent, classifies it, then logs it if permissions were granted
+ * via roles or if the statement belongs in a class that is being logged.
+ */
+static void
+log_audit_event(AuditEventStackItem *stackItem)
+{
+	MemoryContext contextOld;
+	StringInfoData auditStr;
+
+	/* By default put everything in the MISC class. */
+	enum LogClass class = LOG_MISC;
+	const char *className = CLASS_MISC;
+
+	/* Classify the statement using log stmt level and the command tag */
+	switch (stackItem->auditEvent.logStmtLevel)
+	{
+		/* All mods go in WRITE class */
+		case LOGSTMT_MOD:
+			className = CLASS_WRITE;
+			class = LOG_WRITE;
+			break;
+
+		/* Separate ROLE from other DDL statements */
+		case LOGSTMT_DDL:
+			/* Identify role statements */
+			if (stackItem->auditEvent.commandTag == T_GrantStmt ||
+				stackItem->auditEvent.commandTag == T_GrantRoleStmt ||
+				stackItem->auditEvent.commandTag == T_CreateRoleStmt ||
+				(stackItem->auditEvent.commandTag == T_RenameStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_ALTER_ROLE) == 0) ||
+				(stackItem->auditEvent.commandTag == T_DropStmt &&
+				 pg_strcasecmp(stackItem->auditEvent.command,
+							   COMMAND_DROP_ROLE) == 0) ||
+				stackItem->auditEvent.commandTag == T_DropRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleStmt ||
+				stackItem->auditEvent.commandTag == T_AlterRoleSetStmt)
+			{
+				className = CLASS_ROLE;
+				class = LOG_ROLE;
+			}
+			/* Else log as DDL */
+			else
+			{
+				className = CLASS_DDL;
+				class = LOG_DDL;
+			}
+
+		/* Figure out the rest */
+		case LOGSTMT_ALL:
+			switch (stackItem->auditEvent.commandTag)
+			{
+				/* READ statements */
+				case T_CopyStmt:
+				case T_SelectStmt:
+				case T_PrepareStmt:
+				case T_PlannedStmt:
+				case T_ExecuteStmt:
+					className = CLASS_READ;
+					class = LOG_READ;
+					break;
+
+				/* Reindex is DDL (because cluster is DDL) */
+				case T_ReindexStmt:
+					className = CLASS_DDL;
+					class = LOG_DDL;
+					break;
+
+				/* FUNCTION statements */
+				case T_DoStmt:
+					className = CLASS_FUNCTION;
+					class = LOG_FUNCTION;
+					break;
+
+				default:
+					break;
+			}
+			break;
+
+		case LOGSTMT_NONE:
+			break;
+	}
+
+	/*
+	 * Only log the statement if:
+	 *
+	 * 1. If permissions were granted via roles
+	 * 2. The statement belongs to a class that is being logged
+	 */
+	if (!stackItem->auditEvent.granted && !(auditLogBitmap & class))
+		return;
+
+	/* Use audit memory context in case something is not freed */
+	contextOld = MemoryContextSwitchTo(stackItem->contextAudit);
+
+	/* Set statement and substatement Ids */
+	if (stackItem->auditEvent.statementId == 0)
+	{
+		/* If nothing has been logged yet then create a new statement Id */
+		if (!statementLogged)
+		{
+			statementTotal++;
+			statementLogged = true;
+		}
+
+		stackItem->auditEvent.statementId = statementTotal;
+		stackItem->auditEvent.substatementId = ++substatementTotal;
+	}
+
+	/* Create the audit string */
+	initStringInfo(&auditStr);
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.command);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectType);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.objectName);
+	appendStringInfoCharMacro(&auditStr, ',');
+
+	append_valid_csv(&auditStr, stackItem->auditEvent.commandText);
+
+	/* If parameter logging is turned on and there are parameters to log */
+	if (auditLogParameter &&
+		stackItem->auditEvent.paramList != NULL &&
+		stackItem->auditEvent.paramList->numParams > 0 &&
+		!IsAbortedTransactionBlockState())
+	{
+		ParamListInfo paramList = stackItem->auditEvent.paramList;
+		int paramIdx;
+
+		/* Iterate through all params */
+		for (paramIdx = 0; paramIdx < paramList->numParams; paramIdx++)
+		{
+			ParamExternData *prm = &paramList->params[paramIdx];
+			Oid 			 typeOutput;
+			bool 			 typeIsVarLena;
+			char 			*paramStr;
+
+			/* Add a comma for each param */
+			appendStringInfoCharMacro(&auditStr, ',');
+
+			/* Skip this param if null or if oid is invalid */
+			if (prm->isnull || !OidIsValid(prm->ptype))
+			{
+				continue;
+			}
+
+			/* Output the string */
+			getTypeOutputInfo(prm->ptype, &typeOutput, &typeIsVarLena);
+			paramStr = OidOutputFunctionCall(typeOutput, prm->value);
+
+			append_valid_csv(&auditStr, paramStr);
+			pfree(paramStr);
+		}
+	}
+
+	/* Log the audit string */
+	elog(auditLogNotice ? NOTICE : LOG,
+		 "AUDIT: %s,%ld,%ld,%s,%s",
+			stackItem->auditEvent.granted ?
+				AUDIT_TYPE_OBJECT : AUDIT_TYPE_SESSION,
+			stackItem->auditEvent.statementId,
+			stackItem->auditEvent.substatementId,
+			className, auditStr.data);
+
+	/* Mark the audit event as logged */
+	stackItem->auditEvent.logged = true;
+
+	/* Switch back to the old memory context */
+	MemoryContextSwitchTo(contextOld);
+}
+
+/*
+ * Check if the role or any inherited role has any permission in the mask.  The
+ * public role is excluded from this check and superuser permissions are not
+ * considered.
+ */
+static bool
+audit_on_acl(Datum aclDatum,
+			 Oid auditOid,
+			 AclMode mask)
+{
+	bool		result = false;
+	Acl		   *acl;
+	AclItem	   *aclItemData;
+	int			aclIndex;
+	int			aclTotal;
+
+	/* Detoast column's ACL if necessary */
+	acl = DatumGetAclP(aclDatum);
+
+	/* Get the acl list and total */
+	aclTotal = ACL_NUM(acl);
+	aclItemData = ACL_DAT(acl);
+
+	/* Check privileges granted directly to auditOid */
+	for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+	{
+		AclItem *aclItem = &aclItemData[aclIndex];
+
+		if (aclItem->ai_grantee == auditOid &&
+			aclItem->ai_privs & mask)
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/*
+	 * Check privileges granted indirectly via role memberships. We do this in
+	 * a separate pass to minimize expensive indirect membership tests.  In
+	 * particular, it's worth testing whether a given ACL entry grants any
+	 * privileges still of interest before we perform the has_privs_of_role
+	 * test.
+	 */
+	if (!result)
+	{
+		for (aclIndex = 0; aclIndex < aclTotal; aclIndex++)
+		{
+			AclItem *aclItem = &aclItemData[aclIndex];
+
+			/* Don't test public or auditOid (it has been tested already) */
+			if (aclItem->ai_grantee == ACL_ID_PUBLIC ||
+				aclItem->ai_grantee == auditOid)
+				continue;
+
+			/*
+			 * Check that the role has the required privileges and that it is
+			 * inherited by auditOid.
+			 */
+			if (aclItem->ai_privs & mask &&
+				has_privs_of_role(auditOid, aclItem->ai_grantee))
+			{
+				result = true;
+				break;
+			}
+		}
+	}
+
+	/* if we have a detoasted copy, free it */
+	if (acl && (Pointer) acl != DatumGetPointer(aclDatum))
+		pfree(acl);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on a relation.
+ */
+static bool
+audit_on_relation(Oid relOid,
+				  Oid auditOid,
+				  AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	tuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get relation tuple from pg_class */
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+	/* Return false if tuple is not valid */
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	/* Get the relation's ACL */
+	aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl,
+							   &isNull);
+
+	/* If not null then test */
+	if (!isNull)
+		result = audit_on_acl(aclDatum, auditOid, mask);
+
+	/* Free the relation tuple */
+	ReleaseSysCache(tuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute.
+ */
+static bool
+audit_on_attribute(Oid relOid,
+				   AttrNumber attNum,
+				   Oid auditOid,
+				   AclMode mask)
+{
+	bool		result = false;
+	HeapTuple	attTuple;
+	Datum		aclDatum;
+	bool		isNull;
+
+	/* Get the attribute's ACL */
+	attTuple = SearchSysCache2(ATTNUM,
+							   ObjectIdGetDatum(relOid),
+							   Int16GetDatum(attNum));
+
+	/* Return false if attribute is invalid */
+	if (!HeapTupleIsValid(attTuple))
+		return false;
+
+	/* Only process attribute that have not been dropped */
+	if (!((Form_pg_attribute) GETSTRUCT(attTuple))->attisdropped)
+	{
+		aclDatum = SysCacheGetAttr(ATTNUM, attTuple, Anum_pg_attribute_attacl,
+								   &isNull);
+
+		if (!isNull)
+			result = audit_on_acl(aclDatum, auditOid, mask);
+	}
+
+	/* Free attribute */
+	ReleaseSysCache(attTuple);
+
+	return result;
+}
+
+/*
+ * Check if a role has any of the permissions in the mask on an attribute in
+ * the provided set.  If the set is empty, then all valid attributes in the
+ * relation will be tested.
+ */
+static bool
+audit_on_any_attribute(Oid relOid,
+					   Oid auditOid,
+					   Bitmapset *attributeSet,
+					   AclMode mode)
+{
+	bool result = false;
+	AttrNumber col;
+	Bitmapset *tmpSet;
+
+	/* If bms is empty then check for any column match */
+	if (bms_is_empty(attributeSet))
+	{
+		HeapTuple	classTuple;
+		AttrNumber	nattrs;
+		AttrNumber	curr_att;
+
+		/* Get relation to determine total attribute */
+		classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid));
+
+		if (!HeapTupleIsValid(classTuple))
+			return false;
+
+		nattrs = ((Form_pg_class) GETSTRUCT(classTuple))->relnatts;
+		ReleaseSysCache(classTuple);
+
+		/* Check each column */
+		for (curr_att = 1; curr_att <= nattrs; curr_att++)
+		{
+			if (audit_on_attribute(relOid, curr_att, auditOid, mode))
+				return true;
+		}
+	}
+
+	/* bms_first_member is destructive, so make a copy before using it. */
+	tmpSet = bms_copy(attributeSet);
+
+	/* Check each column */
+	while ((col = bms_first_member(tmpSet)) >= 0)
+	{
+		col += FirstLowInvalidHeapAttributeNumber;
+
+		if (col != InvalidAttrNumber &&
+			audit_on_attribute(relOid, col, auditOid, mode))
+		{
+			result = true;
+			break;
+		}
+	}
+
+	/* Free the column set */
+	bms_free(tmpSet);
+
+	return result;
+}
+
+/*
+ * Create AuditEvents for SELECT/DML operations via executor permissions checks.
+ */
+static void
+log_select_dml(Oid auditOid, List *rangeTabls)
+{
+	ListCell *lr;
+	bool first = true;
+	bool found = false;
+
+	/* Do not log if this is an internal statement */
+	if (internalStatement)
+		return;
+
+	foreach(lr, rangeTabls)
+	{
+		Oid relOid;
+		Relation rel;
+		RangeTblEntry *rte = lfirst(lr);
+
+		/* We only care about tables, and can ignore subqueries etc. */
+		if (rte->rtekind != RTE_RELATION)
+			continue;
+
+		found = true;
+
+		/*
+		 * Filter out any system relations
+		 */
+		relOid = rte->relid;
+		rel = relation_open(relOid, NoLock);
+
+		if (!auditLogCatalog && IsSystemNamespace(RelationGetNamespace(rel)))
+		{
+			relation_close(rel, NoLock);
+			continue;
+		}
+
+		/*
+		 * Fill values in the event struct that are required for session
+		 * logging.
+		 */
+		auditEventStack->auditEvent.granted = false;
+
+		/*
+		 * If this is the first rte then session log unless auditLogRelation
+		 * is set.
+		 */
+		if (first && !auditLogRelation)
+		{
+			log_audit_event(auditEventStack);
+
+			first = false;
+		}
+
+		/*
+		 * We don't have access to the parsetree here, so we have to generate
+		 * the node type, object type, and command tag by decoding
+		 * rte->requiredPerms and rte->relkind.
+		 */
+		if (rte->requiredPerms & ACL_INSERT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_InsertStmt;
+			auditEventStack->auditEvent.command = COMMAND_INSERT;
+		}
+		else if (rte->requiredPerms & ACL_UPDATE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_UpdateStmt;
+			auditEventStack->auditEvent.command = COMMAND_UPDATE;
+		}
+		else if (rte->requiredPerms & ACL_DELETE)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_MOD;
+			auditEventStack->auditEvent.commandTag = T_DeleteStmt;
+			auditEventStack->auditEvent.command = COMMAND_DELETE;
+		}
+		else if (rte->requiredPerms & ACL_SELECT)
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_SelectStmt;
+			auditEventStack->auditEvent.command = COMMAND_SELECT;
+		}
+		else
+		{
+			auditEventStack->auditEvent.logStmtLevel = LOGSTMT_ALL;
+			auditEventStack->auditEvent.commandTag = T_Invalid;
+			auditEventStack->auditEvent.command = COMMAND_UNKNOWN;
+		}
+
+		/* Get the relation type */
+		switch (rte->relkind)
+		{
+			case RELKIND_RELATION:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TABLE;
+				break;
+
+			case RELKIND_INDEX:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_INDEX;
+				break;
+
+			case RELKIND_SEQUENCE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_SEQUENCE;
+				break;
+
+			case RELKIND_TOASTVALUE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_TOASTVALUE;
+				break;
+
+			case RELKIND_VIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_VIEW;
+				break;
+
+			case RELKIND_COMPOSITE_TYPE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_COMPOSITE_TYPE;
+				break;
+
+			case RELKIND_FOREIGN_TABLE:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_FOREIGN_TABLE;
+				break;
+
+			case RELKIND_MATVIEW:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_MATVIEW;
+				break;
+
+			default:
+				auditEventStack->auditEvent.objectType =
+					OBJECT_TYPE_UNKNOWN;
+				break;
+		}
+
+		/* Get the relation name */
+		auditEventStack->auditEvent.objectName =
+			quote_qualified_identifier(get_namespace_name(
+									   RelationGetNamespace(rel)),
+									   RelationGetRelationName(rel));
+		relation_close(rel, NoLock);
+
+		/* Perform object auditing only if the audit role is valid */
+		if (auditOid != InvalidOid)
+		{
+			AclMode auditPerms =
+				(ACL_SELECT | ACL_UPDATE | ACL_INSERT | ACL_DELETE) &
+				rte->requiredPerms;
+
+			/*
+			 * If any of the required permissions for the relation are granted
+			 * to the audit role then audit the relation
+			 */
+			if (audit_on_relation(relOid, auditOid, auditPerms))
+			{
+				auditEventStack->auditEvent.granted = true;
+			}
+
+			/*
+			 * Else check if the audit role has column-level permissions for
+			 * select, insert, or update.
+			 */
+			else if (auditPerms != 0)
+			{
+				/*
+				 * Check the select columns
+				 */
+				if (auditPerms & ACL_SELECT)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->selectedCols,
+											   ACL_SELECT);
+				}
+
+				/*
+				 * Check the inserted columns
+				 */
+				if (!auditEventStack->auditEvent.granted &&
+					auditPerms & ACL_INSERT)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->insertedCols,
+											   auditPerms);
+				}
+
+				/*
+				 * Check the updated columns
+				 */
+				if (!auditEventStack->auditEvent.granted &&
+					auditPerms & ACL_UPDATE)
+				{
+					auditEventStack->auditEvent.granted =
+						audit_on_any_attribute(relOid, auditOid,
+											   rte->updatedCols,
+											   auditPerms);
+				}
+			}
+		}
+
+		/* Do relation level logging if a grant was found */
+		if (auditEventStack->auditEvent.granted)
+		{
+			auditEventStack->auditEvent.logged = false;
+			log_audit_event(auditEventStack);
+		}
+
+		/* Do relation level logging if auditLogRelation is set */
+		if (auditLogRelation)
+		{
+			auditEventStack->auditEvent.logged = false;
+			auditEventStack->auditEvent.granted = false;
+			log_audit_event(auditEventStack);
+		}
+
+		pfree(auditEventStack->auditEvent.objectName);
+	}
+
+	/*
+	 * If no tables were found that means that RangeTbls was empty or all
+	 * relations were in the system schema.  In that case still log a
+	 * session record.
+	 */
+	if (!found)
+	{
+		auditEventStack->auditEvent.granted = false;
+		auditEventStack->auditEvent.logged = false;
+
+		log_audit_event(auditEventStack);
+	}
+}
+
+/*
+ * Create AuditEvents for non-catalog function execution, as detected by
+ * log_object_access() below.
+ */
+static void
+log_function_execute(Oid objectId)
+{
+	HeapTuple proctup;
+	Form_pg_proc proc;
+	AuditEventStackItem *stackItem;
+
+	/* Get info about the function. */
+	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(objectId));
+
+	if (!proctup)
+		elog(ERROR, "cache lookup failed for function %u", objectId);
+	proc = (Form_pg_proc) GETSTRUCT(proctup);
+
+	/*
+	 * Logging execution of all pg_catalog functions would make the log
+	 * unusably noisy.
+	 */
+	if (IsSystemNamespace(proc->pronamespace))
+	{
+		ReleaseSysCache(proctup);
+		return;
+	}
+
+	/* Push audit event onto the stack */
+	stackItem = stack_push();
+
+	/* Generate the fully-qualified function name. */
+	stackItem->auditEvent.objectName =
+		quote_qualified_identifier(get_namespace_name(proc->pronamespace),
+								   NameStr(proc->proname));
+	ReleaseSysCache(proctup);
+
+	/* Log the function call */
+	stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+	stackItem->auditEvent.commandTag = T_DoStmt;
+	stackItem->auditEvent.command = COMMAND_EXECUTE;
+	stackItem->auditEvent.objectType = OBJECT_TYPE_FUNCTION;
+	stackItem->auditEvent.commandText = stackItem->next->auditEvent.commandText;
+
+	log_audit_event(stackItem);
+
+	/* Pop audit event from the stack */
+	stack_pop(stackItem->stackId);
+}
+
+/*
+ * Hook functions
+ */
+static ExecutorCheckPerms_hook_type next_ExecutorCheckPerms_hook = NULL;
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+static object_access_hook_type next_object_access_hook = NULL;
+static ExecutorStart_hook_type next_ExecutorStart_hook = NULL;
+
+/*
+ * Hook ExecutorStart to get the query text and basic command type for queries
+ * that do not contain a table so can't be idenitified accurately in
+ * ExecutorCheckPerms.
+ */
+static void
+pg_audit_ExecutorStart_hook(QueryDesc *queryDesc, int eflags)
+{
+	AuditEventStackItem *stackItem = NULL;
+
+	if (!internalStatement)
+	{
+		/* Allocate the audit event */
+		stackItem = stack_push();
+
+		/* Initialize command */
+		switch (queryDesc->operation)
+		{
+			case CMD_SELECT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_SelectStmt;
+				stackItem->auditEvent.command = COMMAND_SELECT;
+				break;
+
+			case CMD_INSERT:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_InsertStmt;
+				stackItem->auditEvent.command = COMMAND_INSERT;
+				break;
+
+			case CMD_UPDATE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_UpdateStmt;
+				stackItem->auditEvent.command = COMMAND_UPDATE;
+				break;
+
+			case CMD_DELETE:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_MOD;
+				stackItem->auditEvent.commandTag = T_DeleteStmt;
+				stackItem->auditEvent.command = COMMAND_DELETE;
+				break;
+
+			default:
+				stackItem->auditEvent.logStmtLevel = LOGSTMT_ALL;
+				stackItem->auditEvent.commandTag = T_Invalid;
+				stackItem->auditEvent.command = COMMAND_UNKNOWN;
+				break;
+		}
+
+		/* Initialize the audit event */
+		stackItem->auditEvent.commandText = queryDesc->sourceText;
+		stackItem->auditEvent.paramList = queryDesc->params;
+	}
+
+	/* Call the previous hook or standard function */
+	if (next_ExecutorStart_hook)
+		next_ExecutorStart_hook(queryDesc, eflags);
+	else
+		standard_ExecutorStart(queryDesc, eflags);
+
+	/*
+	 * Move the stack memory context to the query memory context.  This needs to
+	 * be done here because the query context does not exist before the call
+	 * to standard_ExecutorStart() but the stack item is required by
+	 * pg_audit_ExecutorCheckPerms_hook() which is called during
+	 * standard_ExecutorStart().
+	 */
+	if (stackItem)
+	{
+		MemoryContextSetParent(stackItem->contextAudit,
+							   queryDesc->estate->es_query_cxt);
+	}
+}
+
+/*
+ * Hook ExecutorCheckPerms to do session and object auditing for DML.
+ */
+static bool
+pg_audit_ExecutorCheckPerms_hook(List *rangeTabls, bool abort)
+{
+	Oid auditOid;
+
+	/* Get the audit oid if the role exists. */
+	auditOid = get_role_oid(auditRole, true);
+
+	/* Log DML if the audit role is valid or session logging is enabled. */
+	if ((auditOid != InvalidOid || auditLogBitmap != 0) &&
+		!IsAbortedTransactionBlockState())
+		log_select_dml(auditOid, rangeTabls);
+
+	/* Call the next hook function. */
+	if (next_ExecutorCheckPerms_hook &&
+		!(*next_ExecutorCheckPerms_hook) (rangeTabls, abort))
+		return false;
+
+	return true;
+}
+
+/*
+ * Hook ProcessUtility to do session auditing for DDL and utility commands.
+ */
+static void
+pg_audit_ProcessUtility_hook(Node *parsetree,
+							 const char *queryString,
+							 ProcessUtilityContext context,
+							 ParamListInfo params,
+							 DestReceiver *dest,
+							 char *completionTag)
+{
+	AuditEventStackItem *stackItem = NULL;
+	int64 stackId;
+
+	/* Allocate the audit event */
+	if (context <= PROCESS_UTILITY_QUERY && !IsAbortedTransactionBlockState())
+	{
+		/* Process top level utility statement */
+		if (context == PROCESS_UTILITY_TOPLEVEL)
+		{
+			if (auditEventStack != NULL)
+				elog(ERROR, "pg_audit stack is not empty");
+
+			/* Set params */
+			stackItem = stack_push();
+			stackItem->auditEvent.paramList = params;
+		}
+		else
+			stackItem = stack_push();
+
+		stackId = stackItem->stackId;
+		stackItem->auditEvent.logStmtLevel = GetCommandLogLevel(parsetree);
+		stackItem->auditEvent.commandTag = nodeTag(parsetree);
+		stackItem->auditEvent.command = CreateCommandTag(parsetree);
+		stackItem->auditEvent.commandText = queryString;
+
+		/*
+		 * If this is a DO block log it before calling the next ProcessUtility
+		 * hook.
+		 */
+		if (auditLogBitmap & LOG_FUNCTION &&
+			stackItem->auditEvent.commandTag == T_DoStmt &&
+			!IsAbortedTransactionBlockState())
+		{
+			log_audit_event(stackItem);
+		}
+	}
+
+	/* Call the standard process utility chain. */
+	if (next_ProcessUtility_hook)
+		(*next_ProcessUtility_hook) (parsetree, queryString, context,
+									 params, dest, completionTag);
+	else
+		standard_ProcessUtility(parsetree, queryString, context,
+								params, dest, completionTag);
+
+	/*
+	 * Process the audit event if there is one.  Also check that this event was
+	 * not popped off the stack by a memory context being freed elsewhere.
+	 */
+	if (stackItem && !IsAbortedTransactionBlockState())
+	{
+		/*
+		 * Make sure the item we want to log is still on the stack - if not then
+		 * something has gone wrong and an error will be raised.
+		 */
+		stack_valid(stackId);
+
+		/* Log the utility command if logging is on, the command has not already
+		 * been logged by another hook, and the transaction is not aborted. */
+		if (auditLogBitmap != 0 && !stackItem->auditEvent.logged)
+			log_audit_event(stackItem);
+	}
+}
+
+/*
+ * Hook object_access_hook to provide fully-qualified object names for function
+ * calls.
+ */
+static void
+pg_audit_object_access_hook(ObjectAccessType access,
+							Oid classId,
+							Oid objectId,
+							int subId,
+							void *arg)
+{
+	if (auditLogBitmap & LOG_FUNCTION && access == OAT_FUNCTION_EXECUTE &&
+		auditEventStack && !IsAbortedTransactionBlockState())
+		log_function_execute(objectId);
+
+	if (next_object_access_hook)
+		(*next_object_access_hook) (access, classId, objectId, subId, arg);
+}
+
+/*
+ * Event trigger functions
+ */
+
+/*
+ * Supply additional data for (non drop) statements that have event trigger
+ * support and can be deparsed.
+ */
+Datum
+pg_audit_ddl_command_end(PG_FUNCTION_ARGS)
+{
+	/* Continue only if session DDL logging is enabled */
+	if (auditLogBitmap & LOG_DDL)
+	{
+		EventTriggerData *eventData;
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to "
+						"pg_audit_ddl_command_end()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Get information about triggered events */
+		eventData = (EventTriggerData *) fcinfo->context;
+
+		auditEventStack->auditEvent.logStmtLevel =
+			GetCommandLogLevel(eventData->parsetree);
+		auditEventStack->auditEvent.commandTag =
+			nodeTag(eventData->parsetree);
+		auditEventStack->auditEvent.command =
+			CreateCommandTag(eventData->parsetree);
+
+		/* Return objects affected by the (non drop) DDL statement */
+		query = "SELECT UPPER(object_type), object_identity\n"
+				"  FROM pg_event_trigger_ddl_commands()";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_command_end: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			/* Supply object name and type for audit event */
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 1);
+			auditEventStack->auditEvent.objectName =
+				SPI_getvalue(spiTuple, spiTupDesc, 2);
+
+			/* Log the audit event */
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Supply additional data for drop statements that have event trigger support.
+ */
+Datum
+pg_audit_sql_drop(PG_FUNCTION_ARGS)
+{
+	if (auditLogBitmap & LOG_DDL)
+	{
+		int				  result, row;
+		TupleDesc		  spiTupDesc;
+		const char		 *query;
+		MemoryContext 	  contextQuery;
+		MemoryContext 	  contextOld;
+
+		/* Be sure the module was loaded */
+		if (!auditEventStack)
+		{
+			elog(ERROR, "pg_audit not loaded before call to "
+						"pg_audit_sql_drop()");
+		}
+
+		/* This is an internal statement - do not log it */
+		internalStatement = true;
+
+		/* Make sure the fuction was fired as a trigger */
+		if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+			elog(ERROR, "not fired by event trigger manager");
+
+		/* Switch memory context */
+		contextQuery = AllocSetContextCreate(
+						CurrentMemoryContext,
+						"pg_audit_func_ddl_command_end temporary context",
+						ALLOCSET_DEFAULT_MINSIZE,
+						ALLOCSET_DEFAULT_INITSIZE,
+						ALLOCSET_DEFAULT_MAXSIZE);
+		contextOld = MemoryContextSwitchTo(contextQuery);
+
+		/* Return objects affected by the drop statement */
+		query = "SELECT UPPER(object_type),\n"
+				"       object_identity\n"
+				"  FROM pg_event_trigger_dropped_objects()\n"
+				" WHERE lower(object_type) <> 'type'\n"
+				"   AND schema_name <> 'pg_toast'";
+
+		/* Attempt to connect */
+		result = SPI_connect();
+
+		if (result < 0)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_connect returned %d",
+						result);
+
+		/* Execute the query */
+		result = SPI_execute(query, true, 0);
+
+		if (result != SPI_OK_SELECT)
+			elog(ERROR, "pg_audit_ddl_drop: SPI_execute returned %d",
+						result);
+
+		/* Iterate returned rows */
+		spiTupDesc = SPI_tuptable->tupdesc;
+
+		for (row = 0; row < SPI_processed; row++)
+		{
+			HeapTuple  spiTuple;
+
+			spiTuple = SPI_tuptable->vals[row];
+
+			auditEventStack->auditEvent.objectType =
+				SPI_getvalue(spiTuple, spiTupDesc, 1);
+			auditEventStack->auditEvent.objectName =
+					SPI_getvalue(spiTuple, spiTupDesc, 2);
+
+			log_audit_event(auditEventStack);
+		}
+
+		/* Complete the query */
+		SPI_finish();
+
+		/* Switch to the old memory context */
+		MemoryContextSwitchTo(contextOld);
+		MemoryContextDelete(contextQuery);
+
+		/* No longer in an internal statement */
+		internalStatement = false;
+	}
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * GUC check and assign functions
+ */
+
+/*
+ * Take a pg_audit.log value such as "read, write, dml", verify that each of the
+ * comma-separated tokens corresponds to a LogClass value, and convert them into
+ * a bitmap that log_audit_event can check.
+ */
+static bool
+check_pg_audit_log(char **newval, void **extra, GucSource source)
+{
+	List *flags;
+	char *rawval;
+	ListCell *lt;
+	uint64 *f;
+
+	/* Make sure newval is a comma-separated list of tokens. */
+	rawval = pstrdup(*newval);
+	if (!SplitIdentifierString(rawval, ',', &flags))
+	{
+		GUC_check_errdetail("List syntax is invalid");
+		list_free(flags);
+		pfree(rawval);
+		return false;
+	}
+
+	/*
+	 * Check that we recognise each token, and add it to the bitmap we're
+	 * building up in a newly-allocated uint64 *f.
+	 */
+	f = (uint64 *) malloc(sizeof(uint64));
+	if (!f)
+		return false;
+	*f = 0;
+
+	foreach(lt, flags)
+	{
+		bool subtract = false;
+		uint64 class;
+
+		/* Retrieve a token */
+		char *token = (char *)lfirst(lt);
+
+		/* If token is preceded by -, then the token is subtractive. */
+		if (strstr(token, "-") == token)
+		{
+			token = token + 1;
+			subtract = true;
+		}
+
+		/* Test each token. */
+		if (pg_strcasecmp(token, CLASS_NONE) == 0)
+			class = LOG_NONE;
+		else if (pg_strcasecmp(token, CLASS_ALL) == 0)
+			class = LOG_ALL;
+		else if (pg_strcasecmp(token, CLASS_DDL) == 0)
+			class = LOG_DDL;
+		else if (pg_strcasecmp(token, CLASS_FUNCTION) == 0)
+			class = LOG_FUNCTION;
+		else if (pg_strcasecmp(token, CLASS_MISC) == 0)
+			class = LOG_MISC;
+		else if (pg_strcasecmp(token, CLASS_READ) == 0)
+			class = LOG_READ;
+		else if (pg_strcasecmp(token, CLASS_ROLE) == 0)
+			class = LOG_ROLE;
+		else if (pg_strcasecmp(token, CLASS_WRITE) == 0)
+			class = LOG_WRITE;
+		else
+		{
+			free(f);
+			pfree(rawval);
+			list_free(flags);
+			return false;
+		}
+
+		/* Add or subtract class bits from the log bitmap. */
+		if (subtract)
+			*f &= ~class;
+		else
+			*f |= class;
+	}
+
+	pfree(rawval);
+	list_free(flags);
+
+	/*
+	 * Store the bitmap for assign_pg_audit_log.
+	 */
+	*extra = f;
+
+	return true;
+}
+
+/*
+ * Set pg_audit_log from extra (ignoring newval, which has already been
+ * converted to a bitmap above). Note that extra may not be set if the
+ * assignment is to be suppressed.
+ */
+static void
+assign_pg_audit_log(const char *newval, void *extra)
+{
+	if (extra)
+		auditLogBitmap = *(uint64 *)extra;
+}
+
+/*
+ * Define GUC variables and install hooks upon module load.
+ */
+void
+_PG_init(void)
+{
+	if (IsUnderPostmaster)
+		ereport(ERROR,
+			(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+			errmsg("pg_audit must be loaded via shared_preload_libraries")));
+
+	/*
+	 * pg_audit.role = "audit"
+	 *
+	 * Defines a role to be used for auditing.
+	 */
+	DefineCustomStringVariable("pg_audit.role",
+							   "Enable object auditing for role",
+							   NULL,
+							   &auditRole,
+							   "",
+							   PGC_SUSET,
+							   GUC_NOT_IN_SAMPLE,
+							   NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log = "read, write, ddl"
+	 *
+	 * Controls what classes of commands are logged.
+	 */
+	DefineCustomStringVariable("pg_audit.log",
+							   "Enable session auditing for classes of "
+							   "commands",
+							   NULL,
+							   &auditLog,
+							   "none",
+							   PGC_SUSET,
+							   GUC_LIST_INPUT | GUC_NOT_IN_SAMPLE,
+							   check_pg_audit_log,
+							   assign_pg_audit_log,
+							   NULL);
+
+	/*
+	 * pg_audit.log_parameter = on
+	 *
+	 * Controls whether parameters passed with a statement are logged.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_parameter",
+							 "Enable statement parameter logging",
+							 NULL,
+							 &auditLogParameter,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log_relation = on
+	 *
+	 * Controls whether relations get separate log entries during session
+	 * logging of READ and WRITE classes.  This works as if all relations in the
+	 * database had been added to the audit role and provides a shortcut when
+	 * really detailed logging of absolutely every relation is required.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_relation",
+							 "Enable session relation logging",
+							 NULL,
+							 &auditLogRelation,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log_catalog = on
+	 *
+	 * Controls whether relations in pg_catalog are session logged in queries
+	 * that contain *only* pg_catalog relations.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_catalog",
+							 "Enable logging of relations in pg_catalog",
+							 NULL,
+							 &auditLogCatalog,
+							 true,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * pg_audit.log_notice = on
+	 *
+	 * Audit logging is raised as notices that can be seen on the client.  This
+	 * is intended for testing purposes.
+	 */
+	DefineCustomBoolVariable("pg_audit.log_notice",
+							 "Raise a notice when logging",
+							 NULL,
+							 &auditLogNotice,
+							 false,
+							 PGC_SUSET,
+							 GUC_NOT_IN_SAMPLE,
+							 NULL, NULL, NULL);
+
+	/*
+	 * Install our hook functions after saving the existing pointers to preserve
+	 * the chain.
+	 */
+	next_ExecutorStart_hook = ExecutorStart_hook;
+	ExecutorStart_hook = pg_audit_ExecutorStart_hook;
+
+	next_ExecutorCheckPerms_hook = ExecutorCheckPerms_hook;
+	ExecutorCheckPerms_hook = pg_audit_ExecutorCheckPerms_hook;
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = pg_audit_ProcessUtility_hook;
+
+	next_object_access_hook = object_access_hook;
+	object_access_hook = pg_audit_object_access_hook;
+}
diff --git a/contrib/pg_audit/pg_audit.conf b/contrib/pg_audit/pg_audit.conf
new file mode 100644
index 0000000..e9f4a22
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.conf
@@ -0,0 +1 @@
+shared_preload_libraries = pg_audit
diff --git a/contrib/pg_audit/pg_audit.control b/contrib/pg_audit/pg_audit.control
new file mode 100644
index 0000000..6730c68
--- /dev/null
+++ b/contrib/pg_audit/pg_audit.control
@@ -0,0 +1,5 @@
+# pg_audit extension
+comment = 'provides auditing functionality'
+default_version = '1.0.0'
+module_pathname = '$libdir/pg_audit'
+relocatable = true
diff --git a/contrib/pg_audit/sql/pg_audit.sql b/contrib/pg_audit/sql/pg_audit.sql
new file mode 100644
index 0000000..04fc612
--- /dev/null
+++ b/contrib/pg_audit/sql/pg_audit.sql
@@ -0,0 +1,603 @@
+-- Load pg_audit module
+create extension pg_audit;
+
+--
+-- Create a superuser role that we know the name of for testing
+CREATE USER super SUPERUSER;
+ALTER ROLE super SET pg_audit.log = 'Role';
+ALTER ROLE super SET pg_audit.log_notice = ON;
+\connect contrib_regression super;
+
+--
+-- Create auditor role
+CREATE ROLE auditor;
+
+--
+-- Create first test user
+CREATE USER user1;
+ALTER ROLE user1 SET pg_audit.log = 'ddl, ROLE';
+ALTER ROLE user1 SET pg_audit.log_notice = ON;
+
+--
+-- Create, select, drop (select will not be audited)
+\connect contrib_regression user1
+CREATE TABLE public.test (id INT);
+SELECT * FROM test;
+DROP TABLE test;
+
+--
+-- Create second test user
+\connect contrib_regression super
+
+CREATE USER user2;
+ALTER ROLE user2 SET pg_audit.log = 'Read, writE';
+ALTER ROLE user2 SET pg_audit.log_catalog = OFF;
+ALTER ROLE user2 SET pg_audit.log_notice = ON;
+ALTER ROLE user2 SET pg_audit.role = auditor;
+
+\connect contrib_regression user2
+CREATE TABLE test2 (id INT);
+GRANT SELECT ON TABLE public.test2 TO auditor;
+
+--
+-- Role-based tests
+CREATE TABLE test3
+(
+	id INT
+);
+
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	  LIMIT 1
+) SUBQUERY;
+
+SELECT *
+  FROM test3, test2;
+
+GRANT INSERT
+   ON TABLE public.test3
+   TO auditor;
+
+--
+-- Create a view to test logging
+CREATE VIEW vw_test3 AS
+SELECT *
+  FROM test3;
+
+GRANT SELECT
+   ON vw_test3
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select on vw_test3
+-- select on test2
+SELECT *
+  FROM vw_test3, test2;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- select on test2
+WITH CTE AS
+(
+	SELECT id
+	  FROM test2
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test3
+WITH CTE AS
+(
+	INSERT INTO test3 VALUES (1)
+				   RETURNING id
+)
+INSERT INTO test2
+SELECT id
+  FROM cte;
+
+GRANT UPDATE ON TABLE public.test2 TO auditor;
+
+--
+-- Object logged because of:
+-- insert on test3
+-- update on test2
+WITH CTE AS
+(
+	UPDATE test2
+	   SET id = 1
+	RETURNING id
+)
+INSERT INTO test3
+SELECT id
+  FROM cte;
+
+--
+-- Object logged because of:
+-- insert on test2
+WITH CTE AS
+(
+	INSERT INTO test2 VALUES (1)
+				   RETURNING id
+)
+UPDATE test3
+   SET id = cte.id
+  FROM cte
+ WHERE test3.id <> cte.id;
+
+--
+-- Change permissions of user 2 so that only object logging will be done
+\connect contrib_regression super
+alter role user2 set pg_audit.log = 'NONE';
+
+\connect contrib_regression user2
+
+--
+-- Create test4 and add permissions
+CREATE TABLE test4
+(
+	id int,
+	name text
+);
+
+GRANT SELECT (name)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT UPDATE (id)
+   ON TABLE public.test4
+   TO auditor;
+
+GRANT insert (name)
+   ON TABLE public.test4
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id
+  FROM public.test4;
+
+--
+-- Object logged because of:
+-- select (name) on test4
+SELECT name
+  FROM public.test4;
+
+--
+-- Not object logged
+INSERT INTO public.test4 (id)
+				  VALUES (1);
+
+--
+-- Object logged because of:
+-- insert (name) on test4
+INSERT INTO public.test4 (name)
+				  VALUES ('test');
+
+--
+-- Not object logged
+UPDATE public.test4
+   SET name = 'foo';
+
+--
+-- Object logged because of:
+-- update (id) on test4
+UPDATE public.test4
+   SET id = 1;
+
+--
+-- Object logged because of:
+-- update (name) on test4
+-- update (name) takes precedence over select (name) due to ordering
+update public.test4 set name = 'foo' where name = 'bar';
+
+--
+-- Drop test tables
+DROP TABLE test2;
+DROP VIEW vw_test3;
+DROP TABLE test3;
+DROP TABLE test4;
+
+--
+-- Change permissions of user 1 so that session logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'DDL, READ';
+\connect contrib_regression user1
+
+--
+-- Create table is session logged
+CREATE TABLE public.account
+(
+	id INT,
+	name TEXT,
+	password TEXT,
+	description TEXT
+);
+
+--
+-- Select is session logged
+SELECT *
+  FROM account;
+
+--
+-- Insert is not logged
+INSERT INTO account (id, name, password, description)
+			 VALUES (1, 'user1', 'HASH1', 'blah, blah');
+
+--
+-- Change permissions of user 1 so that only object logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log = 'none';
+alter role user1 set pg_audit.role = 'auditor';
+\connect contrib_regression user1
+
+--
+-- Auditor grants not logged
+GRANT SELECT (password),
+	  UPDATE (name, password)
+   ON TABLE public.account
+   TO auditor;
+
+--
+-- Not object logged
+SELECT id,
+	   name
+  FROM account;
+
+--
+-- Object logged because of:
+-- select (password) on account
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- update (password) on account
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change permissions of user 1 so that session relation logging will be done
+\connect contrib_regression super
+alter role user1 set pg_audit.log_relation = on;
+alter role user1 set pg_audit.log = 'read, WRITE';
+\connect contrib_regression user1
+
+--
+-- Not logged
+create table ACCOUNT_ROLE_MAP
+(
+	account_id INT,
+	role_id INT
+);
+
+--
+-- Auditor grants not logged
+GRANT SELECT
+   ON TABLE public.account_role_map
+   TO auditor;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- select on account_role_map
+-- Session logged on all tables because log = read and log_relation = on
+SELECT account.password,
+	   account_role_map.role_id
+  FROM account
+	   INNER JOIN account_role_map
+			on account.id = account_role_map.account_id;
+
+--
+-- Object logged because of:
+-- select (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+SELECT password
+  FROM account;
+
+--
+-- Not object logged
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada';
+
+--
+-- Object logged because of:
+-- select (password) on account (in the where clause)
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET description = 'yada, yada'
+ where password = 'HASH2';
+
+--
+-- Object logged because of:
+-- update (password) on account
+-- Session logged on all tables because log = read and log_relation = on
+UPDATE account
+   SET password = 'HASH2';
+
+--
+-- Change back to superuser to do exhaustive tests
+\connect contrib_regression super
+SET pg_audit.log = 'ALL';
+SET pg_audit.log_notice = ON;
+SET pg_audit.log_relation = ON;
+SET pg_audit.log_parameter = ON;
+
+--
+-- Simple DO block
+DO $$
+BEGIN
+	raise notice 'test';
+END $$;
+
+--
+-- Create test schema
+CREATE SCHEMA test;
+
+--
+-- Copy account to stdout
+COPY account TO stdout;
+
+--
+-- Create a table from a query
+CREATE TABLE test.account_copy AS
+SELECT *
+  FROM account;
+
+--
+-- Copy from stdin to account copy
+COPY test.account_copy from stdin;
+1	user1	HASH2	yada, yada
+\.
+
+--
+-- Test prepared statement
+PREPARE pgclassstmt (oid) AS
+SELECT *
+  FROM account
+ WHERE id = $1;
+
+EXECUTE pgclassstmt (1);
+DEALLOCATE pgclassstmt;
+
+--
+-- Test cursor
+BEGIN;
+
+DECLARE ctest SCROLL CURSOR FOR
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+
+FETCH NEXT FROM ctest;
+CLOSE ctest;
+COMMIT;
+
+--
+-- Turn off log_catalog and pg_class will not be logged
+SET pg_audit.log_catalog = OFF;
+
+SELECT count(*)
+  FROM
+(
+	SELECT relname
+	  FROM pg_class
+	 LIMIT 1
+ ) subquery;
+
+--
+-- Test prepared insert
+CREATE TABLE test.test_insert
+(
+	id INT
+);
+
+PREPARE pgclassstmt (oid) AS
+INSERT INTO test.test_insert (id)
+					  VALUES ($1);
+EXECUTE pgclassstmt (1);
+
+--
+-- Check that primary key creation is logged
+CREATE TABLE public.test
+(
+	id INT,
+	name TEXT,
+	description TEXT,
+	CONSTRAINT test_pkey PRIMARY KEY (id)
+);
+
+--
+-- Check that analyze is logged
+ANALYZE test;
+
+--
+-- Grants to public should not cause object logging (session logging will
+-- still happen)
+GRANT SELECT
+  ON TABLE public.test
+  TO PUBLIC;
+
+SELECT *
+  FROM test;
+
+-- Check that statements without columns log
+SELECT
+  FROM test;
+
+SELECT 1,
+	   current_user;
+
+DO $$
+DECLARE
+	test INT;
+BEGIN
+	SELECT 1
+	  INTO test;
+END $$;
+
+explain select 1;
+
+--
+-- Test that looks inside of do blocks log
+INSERT INTO TEST (id)
+		  VALUES (1);
+INSERT INTO TEST (id)
+		  VALUES (2);
+INSERT INTO TEST (id)
+		  VALUES (3);
+
+DO $$
+DECLARE
+	result RECORD;
+BEGIN
+	FOR result IN
+		SELECT id
+		  FROM test
+	LOOP
+		INSERT INTO test (id)
+			 VALUES (result.id + 100);
+	END LOOP;
+END $$;
+
+--
+-- Test obfuscated dynamic sql for clean logging
+DO $$
+DECLARE
+	table_name TEXT = 'do_table';
+BEGIN
+	EXECUTE 'CREATE TABLE ' || table_name || ' ("weird name" INT)';
+	EXECUTE 'DROP table ' || table_name;
+END $$;
+
+--
+-- Generate an error and make sure the stack gets cleared
+DO $$
+BEGIN
+	CREATE TABLE bogus.test_block
+	(
+		id INT
+	);
+END $$;
+
+--
+-- Test alter table statements
+ALTER TABLE public.test
+	DROP COLUMN description ;
+
+ALTER TABLE public.test
+	RENAME TO test2;
+
+ALTER TABLE public.test2
+	SET SCHEMA test;
+
+ALTER TABLE test.test2
+	ADD COLUMN description TEXT;
+
+ALTER TABLE test.test2
+	DROP COLUMN description;
+
+DROP TABLE test.test2;
+
+--
+-- Test multiple statements with one semi-colon
+CREATE SCHEMA foo
+	CREATE TABLE foo.bar (id int)
+	CREATE TABLE foo.baz (id int);
+
+--
+-- Test aggregate
+CREATE FUNCTION public.int_add
+(
+	a INT,
+	b INT
+)
+	RETURNS INT LANGUAGE plpgsql AS $$
+BEGIN
+	return a + b;
+END $$;
+
+SELECT int_add(1, 1);
+
+CREATE AGGREGATE public.sum_test(INT) (SFUNC=public.int_add, STYPE=INT, INITCOND='0');
+ALTER AGGREGATE public.sum_test(integer) RENAME TO sum_test2;
+
+--
+-- Test conversion
+CREATE CONVERSION public.conversion_test FOR 'SQL_ASCII' TO 'MULE_INTERNAL' FROM pg_catalog.ascii_to_mic;
+ALTER CONVERSION public.conversion_test RENAME TO conversion_test2;
+
+--
+-- Test create/alter/drop database
+CREATE DATABASE contrib_regression_pgaudit;
+ALTER DATABASE contrib_regression_pgaudit RENAME TO contrib_regression_pgaudit2;
+DROP DATABASE contrib_regression_pgaudit2;
+
+--
+-- Test that frees a memory context earlier than expected
+CREATE TABLE hoge
+(
+	id int
+);
+
+CREATE FUNCTION test()
+	RETURNS INT AS $$
+DECLARE
+	cur1 cursor for select * from hoge;
+	tmp int;
+BEGIN
+	OPEN cur1;
+	FETCH cur1 into tmp;
+	RETURN tmp;
+END $$
+LANGUAGE plpgsql ;
+
+SELECT test();
+
+--
+-- Delete all rows then delete 1 row
+SET pg_audit.log = 'write';
+SET pg_audit.role = 'auditor';
+
+create table bar
+(
+	col int
+);
+
+grant delete
+   on bar
+   to auditor;
+
+insert into bar (col)
+		 values (1);
+delete from bar;
+
+insert into bar (col)
+		 values (1);
+delete from bar
+ where col = 1;
+
+drop table bar;
+
+--
+-- Grant roles to each other
+SET pg_audit.log = 'role';
+GRANT user1 TO user2;
+REVOKE user1 FROM user2;
diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml
index 49a6ce8..0a2bae8 100644
--- a/doc/src/sgml/contrib.sgml
+++ b/doc/src/sgml/contrib.sgml
@@ -124,6 +124,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged;
  &ltree;
  &pageinspect;
  &passwordcheck;
+ &pgaudit;
  &pgbuffercache;
  &pgcrypto;
  &pgfreespacemap;
diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml
index 6268d54..03fea32 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -126,6 +126,7 @@
 <!ENTITY oid2name        SYSTEM "oid2name.sgml">
 <!ENTITY pageinspect     SYSTEM "pageinspect.sgml">
 <!ENTITY passwordcheck   SYSTEM "passwordcheck.sgml">
+<!ENTITY pgaudit         SYSTEM "pgaudit.sgml">
 <!ENTITY pgbuffercache   SYSTEM "pgbuffercache.sgml">
 <!ENTITY pgcrypto        SYSTEM "pgcrypto.sgml">
 <!ENTITY pgfreespacemap  SYSTEM "pgfreespacemap.sgml">
diff --git a/doc/src/sgml/pgaudit.sgml b/doc/src/sgml/pgaudit.sgml
new file mode 100644
index 0000000..b5861f8
--- /dev/null
+++ b/doc/src/sgml/pgaudit.sgml
@@ -0,0 +1,660 @@
+<!-- doc/src/sgml/pgaudit.sgml -->
+
+<sect1 id="pgaudit" xreflabel="pgaudit">
+  <title>pg_audit</title>
+
+  <indexterm zone="pgaudit">
+    <primary>pg_audit</primary>
+  </indexterm>
+
+  <para>
+    The <filename>pg_audit</filename> extension provides detailed session
+    and/or object audit logging via the standard logging facility.  The goal
+    is to provide the tools needed to produce audit logs required to pass any
+    government, financial, or ISO certification audit.
+  </para>
+
+  <para>
+    An audit is an official inspection of an individual's or organization's
+    accounts, typically by an independent body.  The information gathered by
+    <filename>pg_audit</filename> is properly called an audit trail or audit
+    log.  The term audit log is used in this documentation.
+  </para>
+
+  <sect2>
+    <title>Why <literal>pg_audit</>?</title>
+
+    <para>
+      Basic statement logging can be provided by the standard logging facility
+      using <literal>log_statement = all</>.  This is acceptable for monitoring
+      and other usages but does not provide the level of detail generally
+      required for an audit.  It is not enough to have a list of all the
+      operations performed against the database. It must also be possible to
+      find particular statements that are of interest to an auditor.  The
+      standard logging facility shows what the user requested, while
+      <literal>pg_audit</> focuses on the details of what happened while
+      the database was satisfying the request.
+    </para>
+
+    <para>
+      For example, an auditor may want to verify that a particular table was
+      created inside a documented maintenance window.  This might seem like a
+      simple job for grep, but what if you are presented with something like
+      this (intentionally obfuscated) example:
+    </para>
+
+    <programlisting>
+DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+
+    <para>
+      Standard logging will give you this:
+    </para>
+
+    <programlisting>
+LOG:  statement: DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;
+    </programlisting>
+
+    <para>
+      It appears that finding the table of interest may require some knowledge
+      of the code in cases where tables are created dynamically.  This is not
+      ideal since it would be preferrable to just search on the table name.
+      This is where <literal>pg_audit</> comes in.  For the same input,
+      it will produce this output in the log:
+    </para>
+
+    <programlisting>
+AUDIT: SESSION,33,1,FUNCTION,DO,,,"DO $$
+BEGIN
+    EXECUTE 'CREATE TABLE import' || 'ant_table (id INT)';
+END $$;"
+AUDIT: SESSION,33,2,DDL,CREATE TABLE,TABLE,public.important_table,CREATE TABLE important_table (id INT)
+    </programlisting>
+
+    <para>
+      Not only is the <literal>DO</> block logged, but substatement 2 contains
+      the full text of the <literal>CREATE TABLE</> with the statement type,
+      object type, and full-qualified name to make searches easy.
+    </para>
+
+    <para>
+      When logging <literal>SELECT</> and <literal>DML</> statements,
+      <literal>pg_audit</> can be configured to log a separate entry for each
+      relation referenced in a statement.  No parsing is required to find all
+      statements that touch a particular table.  In fact, the goal is that the
+      statement text is provided primarily for deep forensics and should not be
+      the required for an audit.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Usage Considerations</title>
+
+    <para>
+      Depending on settings, it is possible for <literal>pg_audit</literal> to
+      generate an enormous volume of logging.  Be careful to determine
+      exactly what needs to be audit logged in your environment to avoid
+      logging too much.
+    </para>
+
+    <para>
+      For example, when working in an OLAP environment it would probably not be
+      wise to audit log inserts into a large fact table.  The size of the log
+      file will likely be many times the actual data size of the inserts because
+      the log file is expressed as text.  Since logs are generally stored with
+      the OS this may lead to disk space being exhausted very
+      quickly.  In cases where it is not possible to limit audit logging to
+      certain tables, be sure to assess the performance impact while testing
+      and allocate plenty of space on the log volume.  This may also be true for
+      OLTP environments.  Even if the insert volume is not as high, the
+      performance impact of audit logging may still noticeably affect latency.
+    </para>
+
+    <para>
+      To limit the number of relations audit logged for <literal>SELECT</>
+      and <literal>DML</> statments, consider using object audit logging
+      (see <xref linkend="pgaudit-object-audit-logging">).  Object audit logging
+      allows selection of the relations to be logged allowing for reduction
+      of the overall log volume.  However, when new relations are added they
+      must be explicitly added to object audit logging.  A programmatic
+      solution where specified tables are excluded from logging and all others
+      are included may be a good option in this case.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Settings</title>
+
+    <para>
+      Settings may be modified only by a superuser. Allowing normal users to
+      change their settings would defeat the point of an audit log.
+    </para>
+
+    <para>
+      Settings can be specified globally (in
+      <filename>postgresql.conf</filename> or using
+      <literal>ALTER SYSTEM ... SET</>), at the database level (using
+      <literal>ALTER DATABASE ... SET</literal>), or at the role level (using
+      <literal>ALTER ROLE ... SET</literal>).  Note that settings are not
+      inherited through normal role inheritance and <literal>SET ROLE</> will
+      not alter a user's <literal>pg_audit</> settings.  This is a limitation
+      of the roles system and not inherent to <literal>pg_audit</>.
+    </para>
+
+    <para>
+      The <literal>pg_audit</> extension must be loaded in
+      <xref linkend="guc-shared-preload-libraries">.  Otherwise, an error
+      will be raised at load time and no audit logging will occur.
+    </para>
+
+    <variablelist>
+      <varlistentry id="guc-pgaudit-log" xreflabel="pg_audit.log">
+        <term><varname>pg_audit.log</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies which classes of statements will be logged by session
+            audit logging.  Possible values are:
+          </para>
+
+          <itemizedlist>
+            <listitem>
+              <para>
+                <literal>READ</literal> - <literal>SELECT</literal> and
+                <literal>COPY</literal> when the source is a relation or a
+                query.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>WRITE</literal> - <literal>INSERT</literal>,
+                <literal>UPDATE</literal>, <literal>DELETE</literal>,
+                <literal>TRUNCATE</literal>, and <literal>COPY</literal> when the
+                destination is a relation.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>FUNCTION</literal> - Function calls and
+                <literal>DO</literal> blocks.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>ROLE</literal> - Statements related to roles and
+                privileges: <literal>GRANT</literal>,
+                <literal>REVOKE</literal>,
+                <literal>CREATE/ALTER/DROP ROLE</literal>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>DDL</literal> - All <literal>DDL</> that is not included
+                in the <literal>ROLE</> class plus <literal>REINDEX</>.
+              </para>
+            </listitem>
+            <listitem>
+              <para>
+                <literal>MISC</literal> - Miscellaneous commands, e.g.
+                <literal>DISCARD</literal>, <literal>FETCH</literal>,
+                <literal>CHECKPOINT</literal>, <literal>VACUUM</literal>.
+              </para>
+            </listitem>
+          </itemizedlist>
+
+          <para>
+            Multiple classes can be provided using a comma-separated list and
+            classes can be subtracted by prefacing the class with a
+            <literal>-</> sign (see <xref linkend="pgaudit-session-audit-logging">).
+            The default is <literal>none</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-catalog" xreflabel="pg_audit.log_catalog">
+        <term><varname>pg_audit.log_catalog</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_catalog</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that session logging should be enabled in the case where all
+            relations in a statement are in pg_catalog.  Disabling this setting
+            will reduce noise in the log from tools like psql and PgAdmin that query
+            the catalog frequently and heavily. The default is <literal>on</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-notice" xreflabel="pg_audit.log_notice">
+        <term><varname>pg_audit.log_notice</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_notice</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that the audit log messages should be raised as
+            <literal>NOTICE</> instead of <literal>LOG</>.  The primary
+            advantage is that <literal>NOTICE</> messages can be exposed
+            through the user interface.  This setting is used for regression
+            testing and may also be useful to end users for testing.  It is not
+            intended to be used in a production environment as it will leak
+            which statements are being logged to the user. The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-parameter" xreflabel="pg_audit.log_parameter">
+        <term><varname>pg_audit.log_parameter</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_parameter</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies that audit logging should include the parameters that
+            were passed with the statement.  When parameters are present they will
+            be included in CSV format after the statement text. The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-log-relation" xreflabel="pg_audit.log_relation">
+        <term><varname>pg_audit.log_relation</varname> (<type>boolean</type>)
+          <indexterm>
+            <primary><varname>pg_audit.log_relation</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies whether session audit logging should create a separate
+            log entry for each relation (<literal>TABLE</>, <literal>VIEW</>,
+            etc.) referenced in a <literal>SELECT</> or <literal>DML</>
+            statement.  This is a useful shortcut for exhaustive logging
+            without using object audit logging.  The default is
+            <literal>off</>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry id="guc-pgaudit-role" xreflabel="pg_audit.role">
+        <term><varname>pg_audit.role</varname> (<type>string</type>)
+          <indexterm>
+            <primary><varname>pg_audit.role</> configuration parameter</primary>
+          </indexterm>
+        </term>
+        <listitem>
+          <para>
+            Specifies the master role to use for object audit logging.  Muliple
+            audit roles can be defined by granting them to the master role.
+            This allows multiple groups to be in charge of different aspects
+            of audit logging.  There is no default.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </sect2>
+
+  <sect2 id="pgaudit-session-audit-logging">
+    <title>Session Audit Logging</title>
+
+    <para>
+      Session audit logging provides detailed logs of all statements executed
+      by a user in the backend.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Session logging is enabled with the <xref linkend="guc-pgaudit-log">
+        setting.
+
+        Enable session logging for all <literal>DML</> and <literal>DDL</> and
+        log all relations in <literal>DML</> statements:
+          <programlisting>
+set pg_audit.log = 'write, ddl';
+set pg_audit.log_relation = on;
+          </programlisting>
+      </para>
+
+      <para>
+        Enable session logging for all commands except <literal>MISC</> and
+        raise audit log messages as <literal>NOTICE</>:
+          <programlisting>
+set pg_audit.log = 'all, -misc';
+set pg_audit.log_notice = on;
+          </programlisting>
+      </para>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example session audit logging is used for logging
+        <literal>DDL</> and <literal>SELECT</> statements.  Note that the
+        insert statement is not logged since the <literal>WRITE</> class
+        is not enabled
+      </para>
+
+      <para>
+        SQL:
+      </para>
+      <programlisting>
+set pg_audit.log = 'read, ddl';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+insert into account (id, name, password, description)
+             values (1, 'user1', 'HASH1', 'blah, blah');
+
+select *
+    from account;
+      </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.account,create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+AUDIT: SESSION,2,1,READ,SELECT,,,select *
+    from account
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2 id="pgaudit-object-audit-logging">
+    <title>Object Auditing</title>
+
+    <para>
+      Object audit logging logs statements that affect a particular relation.
+      Only <literal>SELECT</>, <literal>INSERT</>, <literal>UPDATE</> and
+      <literal>DELETE</> commands are supported.  <literal>TRUNCATE</> is not
+      included because there is no specific privilege for it.
+    </para>
+
+    <para>
+      Object audit logging is intended to be a finer-grained replacement for
+      <literal>pg_audit.log = 'read, write'</literal>.  As such, it may not
+      make sense to use them in conjunction but one possible scenario would
+      be to use session logging to capture each statement and then supplement
+      that with object logging to get more detail about specific relations.
+    </para>
+
+    <sect3>
+      <title>Configuration</title>
+
+      <para>
+        Object-level audit logging is implemented via the roles system.  The
+        <xref linkend="guc-pgaudit-role"> setting defines the role that
+        will be used for audit logging.  A relation (<literal>TABLE</>,
+        <literal>VIEW</>, etc.) will be audit logged when the audit role has
+        permissions for the command executed or inherits the permissions from
+        another role.  This allows you to effectively have multiple audit roles
+        even though there is a single master role in any context.
+      </para>
+
+      <para>
+      Set <xref linkend="guc-pgaudit-role"> to <literal>auditor</> and
+      grant <literal>SELECT</> and <literal>DELETE</> privileges on the
+      <literal>account</> table.  Any <literal>SELECT</> or
+      <literal>DELETE</> statements on <literal>account</> will now be
+      logged:
+      </para>
+
+      <programlisting>
+set pg_audit.role = 'auditor';
+
+grant select, delete
+   on public.account
+   to auditor;
+      </programlisting>
+    </sect3>
+
+    <sect3>
+      <title>Example</title>
+
+      <para>
+        In this example object audit logging is used to illustrate how a
+        granular approach may be taken towards logging of <literal>SELECT</>
+        and <literal>DML</> statements.  Note that logging on the
+        <literal>account</> table is controlled by column-level permissions,
+        while logging on <literal>account_role_map</> is table-level.
+      </para>
+
+      <para>
+        SQL:
+      </para>
+
+        <programlisting>
+set pg_audit.role = 'auditor';
+
+create table account
+(
+    id int,
+    name text,
+    password text,
+    description text
+);
+
+grant select (password)
+   on public.account
+   to auditor;
+
+select id, name
+  from account;
+
+select password
+  from account;
+
+grant update (name, password)
+   on public.account
+   to auditor;
+
+update account
+   set description = 'yada, yada';
+
+update account
+   set password = 'HASH2';
+
+create table account_role_map
+(
+    account_id int,
+    role_id int
+);
+
+grant select
+   on public.account_role_map
+   to auditor;
+
+select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+        </programlisting>
+
+      <para>
+        Log Output:
+      </para>
+
+      <programlisting>
+AUDIT: OBJECT,1,1,READ,SELECT,TABLE,public.account,select password
+  from account
+AUDIT: OBJECT,2,1,WRITE,UPDATE,TABLE,public.account,update account
+   set password = 'HASH2'
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+AUDIT: OBJECT,3,1,READ,SELECT,TABLE,public.account_role_map,select account.password,
+       account_role_map.role_id
+  from account
+       inner join account_role_map
+            on account.id = account_role_map.account_id
+      </programlisting>
+    </sect3>
+  </sect2>
+
+  <sect2>
+    <title>Format</title>
+
+    <para>
+      Audit entries are written to the standard logging facility and contain
+      the following columns in comma-separated format:
+
+      <note>
+        <para>
+          Output is compliant CSV format only if the log line prefix portion
+          of each log entry is removed.
+        </para>
+      </note>
+
+      <itemizedlist>
+        <listitem>
+          <para>
+            <literal>AUDIT_TYPE</> - <literal>SESSION</> or
+            <literal>OBJECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT_ID</> - Unique statement ID for this session.
+            Each statement ID represents a backend call.  Statement IDs are
+            sequential even if some statements are not logged.  There may be
+            multiple entries for a statement ID when more than one relation
+            is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>SUBSTATEMENT_ID</> - Sequential ID for each
+            substatement within the main statement.  For example, calling
+            a function from a query.  Substatement IDs are continuous
+            even if some substatements are not logged.  There may be multiple
+            entries for a substatement ID when more than one relation is logged.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>CLASS</> - e.g. (<literal>READ</>,
+            <literal>ROLE</>) (see <xref linkend="guc-pgaudit-log">).
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>COMMAND</> - e.g. <literal>ALTER TABLE</>,
+            <literal>SELECT</>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_TYPE</> - <literal>TABLE</>,
+            <literal>INDEX</>, <literal>VIEW</>, etc.
+            Available for <literal>SELECT</>, <literal>DML</> and most
+            <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>OBJECT_NAME</> - The fully-qualified object name
+            (e.g. public.account).  Available for <literal>SELECT</>,
+            <literal>DML</> and most <literal>DDL</> statements.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            <literal>STATEMENT</> - Statement executed on the backend.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+
+    <para>
+      Use <xref linkend="guc-log-line-prefix"> to add any other fields that
+      are needed to satisfy your audit log requirements.  A typical log line
+      prefix might be <literal>'%m %u %d: '</> which would provide the date/time,
+      user name, and database name for each audit log.
+    </para>
+  </sect2>
+
+  <sect2>
+    <title>Caveats</title>
+
+    <itemizedlist>
+      <listitem>
+        <para>
+          Object renames are logged under the name they were renamed to.
+          For example, renaming a table will produce the following result:
+        </para>
+
+        <programlisting>
+ALTER TABLE test RENAME TO test2;
+
+AUDIT: SESSION,36,1,DDL,ALTER TABLE,TABLE,public.test2,ALTER TABLE test RENAME TO test2
+        </programlisting>
+      </listitem>
+
+      <listitem>
+        <para>
+          It is possible to have a command logged more than once.  For example,
+          when a table is created with a primary key specified at creation time
+          the index for the primary key will be logged independently and another
+          audit log will be made for the index under the create entry.  The
+          multiple entries will however be contained within one statement ID.
+        </para>
+      </listitem>
+
+      <listitem>
+        <para>
+          Autovacuum and Autoanalyze are not logged, nor are they intended to be.
+        </para>
+      </listitem>
+
+      <listitem>
+        <para>
+          Statements that are executed after a transaction enters an aborted state
+          will not be audit logged.  However, the statement that caused the error
+          and any subsequent statements executed in the aborted transaction will
+          be logged as ERRORs by the standard logging facility.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </sect2>
+
+  <sect2>
+    <title>Authors</title>
+
+    <para>
+      Abhijit Menon-Sen <email>ams@2ndQuadrant.com</email>, Ian Barwick <email>ian@2ndQuadrant.com</email>, and David Steele <email>david@pgmasters.net</email>.
+    </para>
+  </sect2>
+</sect1>
#64Robert Haas
robertmhaas@gmail.com
In reply to: David Steele (#63)
Re: Auditing extension for PostgreSQL (Take 2)

On Mon, May 11, 2015 at 9:07 PM, David Steele <david@pgmasters.net> wrote:

On 5/1/15 5:58 AM, Sawada Masahiko wrote:

For now, since pg_audit patch has a pg_audit_ddl_command_end()
function which uses part of un-committed "deparsing utility commands"
patch API,
pg_audit patch is completely depend on that patch.
If API name, interface are changed, it would affect for pg_audit patch.
I'm not sure about progress of "deparsing utility command" patch but
you have any idea if that patch is not committed into 9.5 until May
15?

The attached v12 patch removes the code that became redundant with
Alvaro committing the event trigger/deparse work. I've updated the
regression tests to reflect the changes, which were fairly minor and add
additional information to the output. There are no longer any #ifdef'd
code blocks.

This is not a full review, but just a few thoughts...

What happens if the server crashes? Presumably, audit records emitted
just before the crash can be lost, possibly even if the transaction
went on to commit. That's no worse than what is already the case for
regular logging, of course, but it's maybe a bit more relevant here
because of the intended use of this information.

+            if (audit_on_relation(relOid, auditOid, auditPerms))
+            {
+                auditEventStack->auditEvent.granted = true;
+            }

Braces around single-statement blocks are not PostgreSQL style.

I wonder if driving the auditing system off of the required
permissions is really going to work well. That means that decisions
we make about permissions enforcement will have knock-on effects on
auditing. For example, the fact that we check permission on a view
rather than on the underlying tables will (I think) flow through to
how the auditing happens.

+ stackItem->auditEvent.commandTag == T_DoStmt &&

Use IsA(..., DoStmt) for this kind of test. There are many instances
of this pattern in the patch; they should al be fixed.

Using auditLogNotice to switch the log level between LOG and NOTICE is
weird. Switching from LOG to NOTICE is an increase in the logging
level relative to client_min_messages, but a decrease relative to
log_min_messages. With default settings, logging at LOG will go only
to the log (not the client) and logging at NOTICE will go only to the
client (not the log).

The documentation and comments in this patch are, by my estimation,
somewhat below our usual standard. For example:

+       DefineCustomBoolVariable("pg_audit.log_notice",
+                                                        "Raise a
notice when logging",

Granted, there is a fuller explanation in the documentation, but that
doesn't make that explanation particularly good. (One might also
wonder why the log level isn't fully configurable instead of offering
only two choices.)

Here's another example:

+       DefineCustomBoolVariable("pg_audit.log_parameter",
+                                                        "Enable
statement parameter logging",

It would be far better to say "lnclude the values of statement
parameters in auditing messages" or something like that. It is quite
clear that this GUC doesn't enable some sort of general statement
parameter logging facility; it changes the contents of auditing
messages that would have been emitted either way.

I don't want to focus too much on this particular example. The
comments and documentation really deserve a bit more attention
generally than they have gotten thus far. I am not saying they are
bad. I am just saying that, IMHO, they are not as good as we
typically try to make them.

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

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

#65Stephen Frost
sfrost@snowman.net
In reply to: Robert Haas (#64)
Re: Auditing extension for PostgreSQL (Take 2)

Robert, all,

* Robert Haas (robertmhaas@gmail.com) wrote:

On Mon, May 11, 2015 at 9:07 PM, David Steele <david@pgmasters.net> wrote:

The attached v12 patch removes the code that became redundant with
Alvaro committing the event trigger/deparse work. I've updated the
regression tests to reflect the changes, which were fairly minor and add
additional information to the output. There are no longer any #ifdef'd
code blocks.

This is not a full review, but just a few thoughts...

Thanks for that. David and I worked through your suggestions, a number
of my own, and some general cleanup and I've now pushed it.

What happens if the server crashes? Presumably, audit records emitted
just before the crash can be lost, possibly even if the transaction
went on to commit. That's no worse than what is already the case for
regular logging, of course, but it's maybe a bit more relevant here
because of the intended use of this information.

Right, if the server crashes then we may lose information- but there
should be a log somewhere of the crash. I didn't do much in the way of
changes to the documentation, but this is definitely an area where we
should make it very clear what happens.

Braces around single-statement blocks are not PostgreSQL style.

Fixed those and a number of other things, like not having entire
functions in if() blocks.

I wonder if driving the auditing system off of the required
permissions is really going to work well. That means that decisions
we make about permissions enforcement will have knock-on effects on
auditing. For example, the fact that we check permission on a view
rather than on the underlying tables will (I think) flow through to
how the auditing happens.

The checks against the permissions are independent and don't go through
our normal permission checking system, so I'm not too worried about this
aspect. I agree that we need to be vigilant and consider the impact of
changes to the permissions system, but there are also quite a few
regression tests in pg_audit and those should catch a lot of potential
issues.

+ stackItem->auditEvent.commandTag == T_DoStmt &&

Use IsA(..., DoStmt) for this kind of test. There are many instances
of this pattern in the patch; they should al be fixed.

Unfortunately, that's not actually a Node, so we can't just use IsA. We
considered making it one, but that would mean IsA() would return a
T_DoStmt or similar for something that isn't actually a T_DoStmt (it's
an auditEvent of a T_DoStmt). Still, I did go through and look at these
cases and made changes to improve them and clean things up to be neater.

The documentation and comments in this patch are, by my estimation,
somewhat below our usual standard. For example:

+       DefineCustomBoolVariable("pg_audit.log_notice",
+                                                        "Raise a
notice when logging",

This was improved, but I'm sure more can be done. Suggestions welcome.

Granted, there is a fuller explanation in the documentation, but that
doesn't make that explanation particularly good. (One might also
wonder why the log level isn't fully configurable instead of offering
only two choices.)

This was certainly a good point and we added support for choosing the
log level to log it at.

I don't want to focus too much on this particular example. The
comments and documentation really deserve a bit more attention
generally than they have gotten thus far. I am not saying they are
bad. I am just saying that, IMHO, they are not as good as we
typically try to make them.

I've done quite a bit of rework of the comments and will be working on
improving the documentation also.

Thanks!

Stephen